summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÁrni Dagur <arni@dagur.eu>2020-12-19 20:29:38 +0000
committerÁrni Dagur <arni@dagur.eu>2020-12-19 20:29:38 +0000
commit4c5e3d9ea084024f0b88a43963aeb45ce37bfd40 (patch)
tree7ac785e913c775a5b43f43502db86ed95836c188
parent918d4c5e19a27bad3a1c132d3ce2f3b304ee4365 (diff)
parent8303a99ddb3f2dc6465fa324a181081cdd7787c0 (diff)
downloadqutebrowser-4c5e3d9ea084024f0b88a43963aeb45ce37bfd40.tar.gz
qutebrowser-4c5e3d9ea084024f0b88a43963aeb45ce37bfd40.zip
Merge branch 'master' into more-sophisticated-adblock
-rw-r--r--.bumpversion.cfg2
-rw-r--r--.coveragerc1
-rw-r--r--.flake84
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/ci.yml25
-rw-r--r--.github/workflows/recompile-requirements.yml8
-rw-r--r--.gitignore10
-rw-r--r--.mypy.ini5
-rw-r--r--.travis.yml4
-rw-r--r--README.asciidoc9
-rw-r--r--doc/changelog.asciidoc78
-rw-r--r--doc/contributing.asciidoc4
-rw-r--r--doc/faq.asciidoc19
-rw-r--r--doc/help/configuring.asciidoc4
-rw-r--r--doc/help/settings.asciidoc23
-rw-r--r--doc/install.asciidoc95
-rw-r--r--doc/stacktrace.asciidoc2
-rw-r--r--misc/apparmor/usr.bin.qutebrowser90
-rw-r--r--misc/org.qutebrowser.qutebrowser.appdata.xml1
-rw-r--r--misc/requirements/requirements-check-manifest.txt4
-rw-r--r--misc/requirements/requirements-dev.txt14
-rw-r--r--misc/requirements/requirements-flake8.txt4
-rw-r--r--misc/requirements/requirements-mypy.txt10
-rw-r--r--misc/requirements/requirements-mypy.txt-raw3
-rw-r--r--misc/requirements/requirements-pyinstaller.txt2
-rw-r--r--misc/requirements/requirements-pylint.txt6
-rw-r--r--misc/requirements/requirements-pyqt.txt2
-rw-r--r--misc/requirements/requirements-pyqt.txt-raw2
-rw-r--r--misc/requirements/requirements-pyroma.txt2
-rw-r--r--misc/requirements/requirements-sphinx.txt4
-rw-r--r--misc/requirements/requirements-tests.txt33
-rw-r--r--misc/requirements/requirements-tests.txt-raw12
-rw-r--r--misc/requirements/requirements-tox.txt4
-rw-r--r--misc/requirements/requirements-yamllint.txt2
-rw-r--r--misc/userscripts/README.md2
-rwxr-xr-xmisc/userscripts/readability-js46
-rw-r--r--pytest.ini1
-rw-r--r--qutebrowser/__init__.py2
-rw-r--r--qutebrowser/api/cmdutils.py17
-rw-r--r--qutebrowser/api/config.py6
-rw-r--r--qutebrowser/api/hook.py8
-rw-r--r--qutebrowser/app.py16
-rw-r--r--qutebrowser/browser/browsertab.py156
-rw-r--r--qutebrowser/browser/commands.py21
-rw-r--r--qutebrowser/browser/downloads.py27
-rw-r--r--qutebrowser/browser/downloadview.py12
-rw-r--r--qutebrowser/browser/greasemonkey.py21
-rw-r--r--qutebrowser/browser/hints.py127
-rw-r--r--qutebrowser/browser/history.py8
-rw-r--r--qutebrowser/browser/inspector.py18
-rw-r--r--qutebrowser/browser/navigate.py6
-rw-r--r--qutebrowser/browser/network/pac.py7
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py6
-rw-r--r--qutebrowser/browser/qutescheme.py29
-rw-r--r--qutebrowser/browser/shared.py6
-rw-r--r--qutebrowser/browser/signalfilter.py2
-rw-r--r--qutebrowser/browser/urlmarks.py5
-rw-r--r--qutebrowser/browser/webelem.py27
-rw-r--r--qutebrowser/browser/webengine/darkmode.py307
-rw-r--r--qutebrowser/browser/webengine/webengineelem.py21
-rw-r--r--qutebrowser/browser/webengine/webengineinspector.py3
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py14
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py54
-rw-r--r--qutebrowser/browser/webengine/webview.py4
-rw-r--r--qutebrowser/browser/webkit/cache.py4
-rw-r--r--qutebrowser/browser/webkit/cookies.py4
-rw-r--r--qutebrowser/browser/webkit/mhtml.py14
-rw-r--r--qutebrowser/browser/webkit/network/networkmanager.py19
-rw-r--r--qutebrowser/browser/webkit/tabhistory.py4
-rw-r--r--qutebrowser/browser/webkit/webkitelem.py40
-rw-r--r--qutebrowser/browser/webkit/webkitsettings.py4
-rw-r--r--qutebrowser/browser/webkit/webkittab.py18
-rw-r--r--qutebrowser/browser/webkit/webpage.py12
-rw-r--r--qutebrowser/commands/command.py36
-rw-r--r--qutebrowser/commands/runners.py16
-rw-r--r--qutebrowser/commands/userscripts.py12
-rw-r--r--qutebrowser/completion/completionwidget.py6
-rw-r--r--qutebrowser/completion/models/completionmodel.py5
-rw-r--r--qutebrowser/completion/models/histcategory.py6
-rw-r--r--qutebrowser/completion/models/listcategory.py4
-rw-r--r--qutebrowser/completion/models/miscmodels.py10
-rw-r--r--qutebrowser/completion/models/urlmodel.py11
-rw-r--r--qutebrowser/completion/models/util.py4
-rw-r--r--qutebrowser/components/adblock.py14
-rw-r--r--qutebrowser/components/misccommands.py14
-rw-r--r--qutebrowser/components/readlinecommands.py8
-rw-r--r--qutebrowser/config/config.py4
-rw-r--r--qutebrowser/config/configdata.yml51
-rw-r--r--qutebrowser/config/configfiles.py21
-rw-r--r--qutebrowser/config/configtypes.py34
-rw-r--r--qutebrowser/config/qtargs.py94
-rw-r--r--qutebrowser/extensions/interceptors.py14
-rw-r--r--qutebrowser/extensions/loader.py34
-rw-r--r--qutebrowser/html/warning-sessions.html4
-rw-r--r--qutebrowser/keyinput/basekeyparser.py18
-rw-r--r--qutebrowser/keyinput/eventfilter.py4
-rw-r--r--qutebrowser/keyinput/keyutils.py38
-rw-r--r--qutebrowser/keyinput/macros.py14
-rw-r--r--qutebrowser/keyinput/modeman.py11
-rw-r--r--qutebrowser/keyinput/modeparsers.py16
-rw-r--r--qutebrowser/mainwindow/mainwindow.py71
-rw-r--r--qutebrowser/mainwindow/messageview.py4
-rw-r--r--qutebrowser/mainwindow/prompt.py18
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py23
-rw-r--r--qutebrowser/mainwindow/statusbar/command.py10
-rw-r--r--qutebrowser/mainwindow/statusbar/text.py82
-rw-r--r--qutebrowser/mainwindow/statusbar/url.py16
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py28
-rw-r--r--qutebrowser/mainwindow/tabwidget.py8
-rw-r--r--qutebrowser/mainwindow/windowundo.py8
-rw-r--r--qutebrowser/misc/autoupdate.py2
-rw-r--r--qutebrowser/misc/backendproblem.py28
-rw-r--r--qutebrowser/misc/checkpyver.py4
-rw-r--r--qutebrowser/misc/cmdhistory.py8
-rw-r--r--qutebrowser/misc/crashsignal.py13
-rw-r--r--qutebrowser/misc/debugcachestats.py8
-rw-r--r--qutebrowser/misc/earlyinit.py10
-rw-r--r--qutebrowser/misc/guiprocess.py6
-rw-r--r--qutebrowser/misc/httpclient.py5
-rw-r--r--qutebrowser/misc/lineparser.py4
-rw-r--r--qutebrowser/misc/miscwidgets.py14
-rw-r--r--qutebrowser/misc/objects.py14
-rw-r--r--qutebrowser/misc/quitter.py16
-rw-r--r--qutebrowser/misc/savemanager.py5
-rw-r--r--qutebrowser/misc/sessions.py30
-rw-r--r--qutebrowser/misc/throttle.py14
-rw-r--r--qutebrowser/misc/utilcmds.py4
-rw-r--r--qutebrowser/qutebrowser.py4
-rw-r--r--qutebrowser/utils/debug.py70
-rw-r--r--qutebrowser/utils/docutils.py25
-rw-r--r--qutebrowser/utils/javascript.py6
-rw-r--r--qutebrowser/utils/jinja.py16
-rw-r--r--qutebrowser/utils/log.py51
-rw-r--r--qutebrowser/utils/message.py61
-rw-r--r--qutebrowser/utils/objreg.py66
-rw-r--r--qutebrowser/utils/qtutils.py47
-rw-r--r--qutebrowser/utils/standarddir.py36
-rw-r--r--qutebrowser/utils/urlmatch.py20
-rw-r--r--qutebrowser/utils/urlutils.py19
-rw-r--r--qutebrowser/utils/usertypes.py213
-rw-r--r--qutebrowser/utils/utils.py65
-rw-r--r--qutebrowser/utils/version.py79
-rw-r--r--requirements.txt4
-rw-r--r--scripts/dev/check_coverage.py9
-rw-r--r--scripts/dev/recompile_requirements.py43
-rwxr-xr-xscripts/dev/run_vulture.py5
-rw-r--r--scripts/dev/ua_fetch.py70
-rw-r--r--scripts/dev/update_version.py2
-rwxr-xr-xscripts/open_url_in_instance.sh2
-rwxr-xr-xsetup.py3
-rw-r--r--tests/conftest.py2
-rw-r--r--tests/end2end/features/conftest.py1
-rw-r--r--tests/end2end/features/hints.feature6
-rw-r--r--tests/end2end/features/marks.feature10
-rw-r--r--tests/end2end/features/private.feature7
-rw-r--r--tests/end2end/features/qutescheme.feature1
-rw-r--r--tests/end2end/features/urlmarks.feature2
-rw-r--r--tests/end2end/fixtures/quteprocess.py22
-rw-r--r--tests/end2end/test_invocations.py9
-rw-r--r--tests/helpers/fixtures.py17
-rw-r--r--tests/helpers/messagemock.py11
-rw-r--r--tests/helpers/utils.py2
-rw-r--r--tests/unit/api/test_cmdutils.py6
-rw-r--r--tests/unit/browser/test_hints.py7
-rw-r--r--tests/unit/browser/webengine/test_darkmode.py228
-rw-r--r--tests/unit/browser/webkit/test_mhtml.py5
-rw-r--r--tests/unit/commands/test_argparser.py5
-rw-r--r--tests/unit/config/test_configcommands.py23
-rw-r--r--tests/unit/config/test_configfiles.py22
-rw-r--r--tests/unit/config/test_configinit.py10
-rw-r--r--tests/unit/config/test_configtypes.py48
-rw-r--r--tests/unit/config/test_qtargs.py138
-rw-r--r--tests/unit/javascript/stylesheet/test_appendchild.js14
-rw-r--r--tests/unit/misc/test_checkpyver.py11
-rw-r--r--tests/unit/misc/test_editor.py11
-rw-r--r--tests/unit/utils/test_log.py19
-rw-r--r--tests/unit/utils/test_standarddir.py6
-rw-r--r--tests/unit/utils/test_urlmatch.py7
-rw-r--r--tests/unit/utils/test_urlutils.py3
-rw-r--r--tests/unit/utils/test_utils.py13
-rw-r--r--tests/unit/utils/test_version.py56
-rw-r--r--tox.ini1
182 files changed, 2437 insertions, 1750 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 2628059be..fa5e2a66e 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 1.13.1
+current_version = 1.14.0
commit = True
message = Release v{new_version}
tag = True
diff --git a/.coveragerc b/.coveragerc
index 9d43917a3..cb0619b80 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -18,6 +18,7 @@ exclude_lines =
raise utils\.Unreachable
if __name__ == ["']__main__["']:
if typing.TYPE_CHECKING:
+ if TYPE_CHECKING:
\.\.\.
[xml]
diff --git a/.flake8 b/.flake8
index 6f49baf99..7709eacaf 100644
--- a/.flake8
+++ b/.flake8
@@ -39,6 +39,7 @@ exclude = .*,__pycache__,resources.py
# A003: Builtin name for class attribute (needed for overridden methods)
# W503: like break before binary operator
# W504: line break after binary operator
+# FI15: __future__ import "generator_stop" missing
ignore =
B001,B008,B305,
E128,E226,E265,E501,E402,E266,E722,E731,
@@ -48,7 +49,8 @@ ignore =
D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D412,D413,
A003,
W503, W504
-min-version = 3.4.0
+ FI15
+min-version = 3.6.0
max-complexity = 12
per-file-ignores =
qutebrowser/api/hook.py : N801
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 123014908..000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "daily"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 36423aab8..182f935be 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,10 +39,10 @@ jobs:
.tox
~/.cache/pip
key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- - uses: actions/setup-python@v2.1.2
+ - uses: actions/setup-python@v2
with:
python-version: '3.8'
- - uses: actions/setup-node@v2.1.1
+ - uses: actions/setup-node@v2-beta
with:
node-version: '12.x'
if: "matrix.testenv == 'eslint'"
@@ -57,7 +57,7 @@ jobs:
bindir="$HOME/.local/bin"
mkdir -p "$bindir"
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/$scversion/shellcheck-$scversion.linux.x86_64.tar.xz" | tar -xJv --strip-components 1 -C "$bindir" shellcheck-$scversion/shellcheck
- echo "::add-path::$bindir"
+ echo "$bindir" >> "$GITHUB_PATH"
fi
python -m pip install -U pip
python -m pip install -U -r misc/requirements/requirements-tox.txt
@@ -100,11 +100,6 @@ jobs:
fail-fast: false
matrix:
include:
- ### PyQt 5.7.1 (Python 3.5)
- # - testenv: py35-pyqt57
- # os: ubuntu-16.04
- # python: 3.5
- # experimental: true
### PyQt 5.9 (Python 3.6)
- testenv: py36-pyqt59
os: ubuntu-18.04
@@ -132,7 +127,7 @@ jobs:
### PyQt 5.15 (Python 3.9)
- testenv: py39-pyqt515
os: ubuntu-20.04
- python: 3.9-dev
+ python: 3.9
### PyQt 5.15 (Python 3.8, with coverage)
- testenv: py38-pyqt515-cov
os: ubuntu-20.04
@@ -157,7 +152,7 @@ jobs:
~/.cache/pip
key: "${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- name: Set up Python
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2
with:
python-version: "${{ matrix.python }}"
- name: Set up problem matchers
@@ -178,7 +173,7 @@ jobs:
if: "failure()"
- name: Upload coverage
if: "endsWith(matrix.testenv, '-cov')"
- uses: codecov/codecov-action@v1.0.13
+ uses: codecov/codecov-action@v1
with:
name: "${{ matrix.testenv }}"
@@ -212,7 +207,7 @@ jobs:
if: "always() && github.repository_owner == 'qutebrowser'"
steps:
- name: Send success IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.linters.result == 'success' && needs.tests.result == 'success' && needs.tests-docker.result == 'success' && needs.codeql.result == 'success'"
with:
server: chat.freenode.net
@@ -220,7 +215,7 @@ jobs:
nickname: qutebrowser-bot
message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})"
- name: Send failure IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.linters.result == 'failure' || needs.tests.result == 'failure' || needs.tests-docker.result == 'failure' || needs.codeql.result == 'failure'"
with:
server: chat.freenode.net
@@ -229,7 +224,7 @@ jobs:
message: "[${{ github.workflow }}] \u00034FAIL:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})\n
linters: ${{ needs.linters.result }}, tests: ${{ needs.tests.result }}, tests-docker: ${{ needs.tests-docker.result }}, codeql: ${{ needs.codeql.result }}"
- name: Send skipped IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.linters.result == 'skipped' || needs.tests.result == 'skipped' || needs.tests-docker.result == 'skipped' || needs.codeql.result == 'skipped'"
with:
server: chat.freenode.net
@@ -237,7 +232,7 @@ jobs:
nickname: qutebrowser-bot
message: "[${{ github.workflow }}] \u00038Skipped:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})"
- name: Send cancelled IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.linters.result == 'cancelled' || needs.tests.result == 'cancelled' || needs.tests-docker.result == 'cancelled' || needs.codeql.result == 'cancelled'"
with:
server: chat.freenode.net
diff --git a/.github/workflows/recompile-requirements.yml b/.github/workflows/recompile-requirements.yml
index 045f2ee1e..c41f67810 100644
--- a/.github/workflows/recompile-requirements.yml
+++ b/.github/workflows/recompile-requirements.yml
@@ -20,11 +20,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2
with:
python-version: '3.7'
- name: Set up Python 3.8
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Recompile requirements
@@ -60,7 +60,7 @@ jobs:
if: "always() && github.repository == 'qutebrowser/qutebrowser'"
steps:
- name: Send success IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.update.result == 'success'"
with:
server: chat.freenode.net
@@ -68,7 +68,7 @@ jobs:
nickname: qutebrowser-bot
message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})"
- name: Send non-success IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.update.result != 'success'"
with:
server: chat.freenode.net
diff --git a/.gitignore b/.gitignore
index aa5b853f7..50c67dee4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,15 @@
__pycache__
*.py~
*.pyc
-*.swp
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+[._]*.un~
+Session.vim
+Sessionx.vim
+*~
/build
/dist
/qutebrowser.egg-info
diff --git a/.mypy.ini b/.mypy.ini
index 1972e5040..dfef7d3e5 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -1,6 +1,4 @@
[mypy]
-# We also need to support 3.5, but if we'd chose that here, we'd need to deal
-# with conditional imports (like secrets.py).
python_version = 3.6
# --strict
@@ -115,6 +113,9 @@ disallow_untyped_defs = True
[mypy-qutebrowser.browser.webengine.webengineelem]
disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.darkmode]
+disallow_untyped_defs = True
+
[mypy-qutebrowser.keyinput.*]
disallow_untyped_defs = True
diff --git a/.travis.yml b/.travis.yml
index 9a56a756c..b75081477 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
dist: xenial
language: python
-python: 3.5
+python: 3.6
os: linux
-env: TESTENV=py35-pyqt57
+env: TESTENV=py36-pyqt57
install:
- python -m pip install -U pip
diff --git a/README.asciidoc b/README.asciidoc
index 4da2c0edc..903c1415e 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -109,8 +109,7 @@ Requirements
The following software and libraries are required to run qutebrowser:
-* https://www.python.org/[Python] 3.5.2 or newer (3.6 - 3.8 recommended;
- support for 3.5 will be dropped with qutebrowser v2.0.0)
+* https://www.python.org/[Python] 3.6 or newer
* https://www.qt.io/[Qt] 5.7.1 or newer (5.14 recommended; support for < 5.11
will be dropped with qutebrowser v2.0.0) with the following modules:
- QtCore / qtbase
@@ -219,11 +218,11 @@ Active
* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
-* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* https://nyxt.atlas.engineer/[Nyxt browser] (formerly "Next browser", Lisp, Emacs-like but also offers Vim bindings, QtWebKit or GTK+/WebKit2 - note there was a http://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly)
* https://vieb.dev/[Vieb] (JavaScript, Electron)
* Chrome/Chromium addons:
https://vimium.github.io/[Vimium],
+ https://github.com/dcchambers/vb4c[vb4c] (fork of cVim)
* Firefox addons (based on WebExtensions):
https://github.com/tridactyl/tridactyl[Tridactyl],
https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental),
@@ -231,7 +230,6 @@ Active
https://github.com/amedama41/vvimpulation[VVimpulation]
* Addons for Firefox and Chrome:
https://github.com/brookhong/Surfingkeys[Surfingkeys],
- https://github.com/lusakasa/saka-key[Saka Key],
https://krabby.netlify.com/[Krabby],
https://lydell.github.io/LinkHints/[Link Hints] (hinting only)
* Addons for Safari:
@@ -253,6 +251,7 @@ original site is gone but the Arch Linux wiki has some data)
* https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
* https://github.com/conformal/xombrero[xombrero] (C, GTK+ with WebKit1)
* https://github.com/linkdd/cream-browser[Cream Browser] (C, GTK+ with WebKit1)
+* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* Firefox addons (not based on WebExtensions or no recent activity):
http://www.vimperator.org/[Vimperator],
http://bug.5digits.org/pentadactyl/index[Pentadactyl],
@@ -262,7 +261,7 @@ original site is gone but the Arch Linux wiki has some data)
* Chrome/Chromium addons:
https://github.com/k2nr/ViChrome/[ViChrome],
https://github.com/jinzhu/vrome[Vrome],
- https://github.com/lusakasa/saka-key[Saka Key],
+ https://github.com/lusakasa/saka-key[Saka Key] (https://github.com/lusakasa/saka-key/issues/171[unmaintained]),
https://github.com/1995eaton/chromium-vim[cVim],
https://glee.github.io/[GleeBox]
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index c29595d9e..129378b07 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -15,15 +15,54 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
-v1.14.0 (unreleased)
+v2.0.0 (unreleased)
+-------------------
+
+Major changes
+~~~~~~~~~~~~~
+
+- At least Python 3.6 is now required to run qutebrowser, support for Python
+ 3.5 is dropped. Note that Python 3.5 is
+ https://www.python.org/downloads/release/python-3510/[no longer supported
+ upstream] since September 2020.
+
+Changed
+~~~~~~~
+
+- `config.py` files now are required to have either
+ `config.load_autoconfig(False)` (don't load `autoconfig.yml`) or
+ `config.load_autoconfig()` (do load `autoconfig.yml`) in them.
+- The `colors.webpage.darkmode.*` settings are now also supported with older Qt
+ versions (Qt 5.10 to 5.13) rather than just with Qt 5.14 and above.
+- For regexes in the config (`hints.{prev,next}_regexes`), certain patterns
+ which will change meanings in future Python versions are now disallowed. This is
+ the case for character sets starting with a literal `[` or containing literal
+ character sequences `--`, `&&`, `~~`, or `||`. To avoid a warning, remove the
+ duplicate characters or escape them with a backslash.
+
+v1.14.0 (2020-10-15)
--------------------
+Note: The QtWebEngine version bundled with the Windows/macOS
+releases is still based on Qt 5.15.0 (like with qutebrowser v1.12.0 and
+v1.13.0) rather than Qt 5.15.1 because of a
+https://bugreports.qt.io/browse/QTBUG-86752[Qt bug] causing
+frequent renderer process crashes. When Qt 5.15.2 is released
+(planned for November 3rd, 2020), a qutebrowser v1.14.x patch
+release with an updated QtWebEngine will be released.
+
+Furthermore, this release still only contains partial session support for QtWebEngine
+5.15. It's still recommended to run against Qt 5.15 due to the security patches
+contained in it -- for most users, the added workarounds seem to work out fine. A
+rewritten session support will be part of qutebrowser v2.0.0, tentatively planned for the
+end of the year or early 2021.
+
Changed
~~~~~~~
- The `content.media_capture` setting got split up into three more fine-grained
settings, `content.media.audio_capture`, `.video_capture` and
- `.audio_video_capture`. Before this change, anwering "always" to a prompt
+ `.audio_video_capture`. Before this change, answering "always" to a prompt
about e.g. audio capturing would set the `content.media_capture` setting,
which would also allow the same website to capture video on a future visit.
Now every prompt will set the appropriate setting, though existing
@@ -44,8 +83,6 @@ Changed
- `:back` and `:forward` now take an optional index which is completed using
the current tab's history.
- The time a website in a tab was visited is now saved/restored in sessions.
-- New argument `strip` for `:navigate` which removes queries and
- fragments from the current URL.
- When attempting to download a file to a location for which there's already a
still-running download, a confirmation prompt is now displayed.
- `:completion-item-focus` now understands `next-page` and `prev-page` with
@@ -66,12 +103,27 @@ Changed
`--asciidoc-python path/to/python --asciidoc path/to/asciidoc.py`
instead of the former
`--asciidoc path/to/python path/to/asciidoc.py`.
-- The `readability-js` userscript now adds some CSS to better deal
- with images, similarly to what Firefox' reader mode does.
+- Dark mode (`colors.webpage.darkmode.*`) is now supported with Qt 5.15.2 (which
+ is not released yet).
+- The default for the darkmode `policy.images` setting is now set to `smart`
+ which fixes issues with e.g. formulas on Wikipedia.
+- The `readability-js` userscript now adds some CSS to improve the reader mode
+ styling in various scenarios:
+ * Images are now shrinked to the page width, similarly to what Firefox' reader
+ mode does.
+ * Some images ore now displayed as block (rather than inline) which is what
+ Firefox' reader mode does as well.
+ * Blockquotes are now styled more distinctively, again based on the Firefox
+ reader mode.
+ * Code blocks are now easier to distinguish from text and tables have visible
+ cell margins.
+- The `readability-js` userscript now supports hint userscript mode.
Added
~~~~~
+- New argument `strip` for `:navigate` which removes queries and
+ fragments from the current URL.
- `:undo` now has a new `-w` / `--window` argument, which can be used to
restore closed windows (rather than tabs). This is bound to `U` by default.
- `:jseval` can now take `javascript:...` URLs via a new `--url` flag.
@@ -88,6 +140,11 @@ Added
open the directory containing the downloaded file. An entry to do the same
was also added to the context menu.
- Messages are now wrapped when they are too long to be displayed on a single line.
+- New possible `--debug-flag` values:
+ * `wait-renderer-process` waits for a `SIGUSR1` in the renderer process so a
+ debugger can be attached.
+ * `avoid-chromium-init` allows using `--version` without needing a working
+ QtWebEngine/Chromium.
Fixed
~~~~~
@@ -125,18 +182,21 @@ Fixed
could end up in a confusing state. This is now fixed.
- When qutebrowser quits, running downloads are now cancelled properly.
- The site-specific quirk for `web.whatsapp.com` has been updated to work after recent
- WhatsApp-changes.
+ changes in WhatsApp.
- Highlighting in the completion now works properly when UTF-16 surrogate pairs (such as
emoji) are involved.
- When a windowed inspector is clicked, insert mode now isn't entered anymore.
-- When `:undo` to re-open a tab but `tabs.tabs_are_windows` was set between
+- When `:undo` is used to re-open a tab, but `tabs.tabs_are_windows` was set between
closing and undoing the close, qutebrowser crashed. This is now fixed.
+- With QtWebEngine 5.15.0, setting the darkmode image policy to `smart` leads to
+ renderer process crashes. The offending setting value is now ignored with a
+ warning.
- Fixes for the `qute-pass` userscript:
* With newer `gopass` versions, a deprecation notice was copied as
password due to `qute-pass` using it in a deprecated way.
* The `--password-store` argument didn't actually set
`PASSWORD_STORE_DIR` for `pass`, resulting in `qute-pass` finding matches but the
- underlying `pass` not finding matching passwords. This is now fixed.
+ underlying `pass` not finding matching passwords.
v1.13.1 (2020-07-17)
--------------------
diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc
index dbf1e5cc5..b8c9b9010 100644
--- a/doc/contributing.asciidoc
+++ b/doc/contributing.asciidoc
@@ -9,7 +9,7 @@ IMPORTANT: Bandwidth for pull request review is currently quite limited. If you
want to contribute where it's most needed, please consider reviewing or testing
open pull requests.
-I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
+I `&lt;3` footnote:[`<3` in HTML] contributors!
This document contains guidelines for contributing to qutebrowser, as well as
useful hints when doing so.
@@ -111,7 +111,7 @@ unittests and several linters/checkers.
Currently, the following tox environments are available:
* Tests using https://www.pytest.org[pytest]:
- - `py35`, `py36`: Run pytest for python 3.5/3.6 with the system-wide PyQt.
+ - `py36`, `py37`, ...: Run pytest for python 3.6/3.7/... with the system-wide PyQt.
- `py36-pyqt57`, ..., `py36-pyqt59`: Run pytest with the given PyQt version (`py35-*` also works).
- `py36-pyqt59-cov`: Run with coverage support (other Python/PyQt versions work too).
* `flake8`: Run various linting checks via https://pypi.python.org/pypi/flake8[flake8].
diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc
index be946fb83..275f82df2 100644
--- a/doc/faq.asciidoc
+++ b/doc/faq.asciidoc
@@ -322,6 +322,25 @@ certutil -d "sql:${HOME}/.pki/nssdb" -D -n "My Fancy Certificate Nickname"
And then import the new and valid certificates using the procedure
described above.
+Is there a dark mode? How can I filter websites to be darker?::
+There is a total of four possible approaches to get dark websites:
++
+- The `colors.webpage.prefers_color_scheme_dark` setting tells websites that you prefer
+ a dark theme. However, this requires websites to ship an appropriate dark style sheet.
+ The setting requires a restart and QtWebEngine with at least Qt 5.14.
+- The `colors.webpage.darkmode.*` settings enable the dark mode of the underlying
+ Chromium. Those setting require a restart and QtWebEngine with at least Qt 5.14. It's
+ unfortunately not possible (due to limitations in Chromium and/or QtWebEngine) to
+ change them dynamically or to specify a list of excluded websites.
+- The `content.user_stylesheets` setting allows specifying a custom CSS such as
+ https://github.com/alphapapa/solarized-everything-css/[Solarized Everything]. Despite
+ the name, the repository also offers themes other than just Solarized. This approach
+ often yields worse results compared to the above ones, but it's possible to toggle it
+ dynamically using a binding like `:bind ,d 'config-cycle content.user_stylesheets
+ ~/path/to/solarized-everything-css/css/gruvbox/gruvbox-all-sites.css ""'`
+- Finally, qutebrowser's Greasemonkey support should allow for running a
+ https://github.com/darkreader/darkreader/issues/926#issuecomment-575893299[stripped down version]
+ of the Dark Reader extension. This is mostly untested, though.
== Troubleshooting
diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc
index 4f1d872f7..aa1dfc12e 100644
--- a/doc/help/configuring.asciidoc
+++ b/doc/help/configuring.asciidoc
@@ -395,10 +395,10 @@ Pre-built colorschemes
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
- https://gitlab.com/jjzmajic/qutewal[Pywal integration]
-- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
+- https://github.com/arcticicestudio/nord[Nord]: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
- https://github.com/dracula/qutebrowser-dracula-theme[Dracula]
- https://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized]
-- https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[gruvbox]
+- https://github.com/morhetz/gruvbox[gruvbox]: https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[The-Compiler], https://gitlab.com/shaneyost/dots-popos-september-2020/-/blob/master/qutebrowser/config.py[Shane Yost]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 035c7881d..94a884db9 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -1572,6 +1572,7 @@ Default: +pass:[white]+
[[colors.webpage.darkmode.algorithm]]
=== colors.webpage.darkmode.algorithm
Which algorithm to use for modifying how colors are rendered with darkmode.
+The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated like `lightness-hsl` with older QtWebEngine versions.
This setting requires a restart.
@@ -1579,13 +1580,13 @@ Type: <<types,String>>
Valid values:
- * +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value.
+ * +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value. Not available with Qt < 5.14.
* +lightness-hsl+: Modify colors by converting them to the HSL color space and inverting the lightness (i.e. the "L" in HSL).
* +brightness-rgb+: Modify colors by subtracting each of r, g, and b from their maximum value.
Default: +pass:[lightness-cielab]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
+On QtWebEngine, this setting requires Qt 5.10 or newer.
On QtWebKit, this setting is unavailable.
@@ -1600,7 +1601,7 @@ Type: <<types,Float>>
Default: +pass:[0.0]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
+On QtWebEngine, this setting requires Qt 5.10 or newer.
On QtWebKit, this setting is unavailable.
@@ -1628,7 +1629,7 @@ Type: <<types,Bool>>
Default: +pass:[false]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
+On QtWebEngine, this setting requires Qt 5.10 or newer.
On QtWebKit, this setting is unavailable.
@@ -1643,7 +1644,7 @@ Type: <<types,Bool>>
Default: +pass:[false]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
+On QtWebEngine, this setting requires Qt 5.10 or newer.
On QtWebKit, this setting is unavailable.
@@ -1665,7 +1666,7 @@ On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.policy.images]]
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
-WARNING: On Qt 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt].
+With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt]. With QtWebEngine 5.10, this is not available at all. In those cases, the 'smart' setting is ignored and treated like 'never'.
This setting requires a restart.
@@ -1675,11 +1676,11 @@ Valid values:
* +always+: Apply dark mode filter to all images.
* +never+: Never apply dark mode filter to any images.
- * +smart+: Apply dark mode based on image content.
+ * +smart+: Apply dark mode based on image content. Not available with Qt 5.10 / 5.15.0.
-Default: +pass:[never]+
+Default: +pass:[smart]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
+On QtWebEngine, this setting requires Qt 5.10 or newer.
On QtWebKit, this setting is unavailable.
@@ -4081,14 +4082,14 @@ The following placeholders are defined:
* `{perc}`: Percentage as a string like `[10%]`.
* `{perc_raw}`: Raw percentage, e.g. `10`.
* `{current_title}`: Title of the current web page.
-* `{title_sep}`: The string ` - ` if a title is set, empty otherwise.
+* `{title_sep}`: The string `" - "` if a title is set, empty otherwise.
* `{index}`: Index of this tab.
* `{aligned_index}`: Index of this tab padded with spaces to have the same
width.
* `{id}`: Internal tab ID of this tab.
* `{scroll_pos}`: Page scroll position.
* `{host}`: Host of the current web page.
-* `{backend}`: Either ''webkit'' or ''webengine''
+* `{backend}`: Either `webkit` or `webengine`
* `{private}`: Indicates when private mode is enabled.
* `{current_url}`: URL of the current web page.
* `{protocol}`: Protocol (http/https/...) of the current web page.
diff --git a/doc/install.asciidoc b/doc/install.asciidoc
index 0f9a4c399..9c71bf2b5 100644
--- a/doc/install.asciidoc
+++ b/doc/install.asciidoc
@@ -27,22 +27,41 @@ On Debian / Ubuntu
How to install qutebrowser depends a lot on the version of Debian/Ubuntu you're
running.
+[[ubuntu1604]]
Ubuntu 16.04 LTS / Linux Mint 18
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or
-QtWebEngine). However, it comes with Python 3.5, so you can
-<<tox,install qutebrowser in a virtualenv>>.
+QtWebEngine). It also comes with Python 3.5 which is not supported anymore since
+qutebrowser v2.0.0.
-You'll need some basic libraries to use the virtualenv-installed PyQt:
+You should be able to install a newer Python (3.6+) using the
+https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or
+https://github.com/pyenv/pyenv[pyenv], and then proceed to
+<<tox,install qutebrowser in a virtualenv>>. However, this is currently untested. If you
+got this setup to work successfully, please submit a pull request to adjust these
+instructions!
+
+Note you'll need some basic libraries to use the virtualenv-installed PyQt:
----
-# apt install libglib2.0-0 libgl1 libfontconfig1 libx11-xcb1 libxi6 libxrender1 libdbus-1-3
+# apt install --no-install-recommends git ca-certificates python3 python3-venv asciidoc libglib2.0-0 libgl1 libfontconfig1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1 libxkbcommon-x11-0 libdbus-1-3 libyaml-dev gcc python3-dev
----
+// FIXME not needed anymore?
+// libxi6 libxrender1 libegl1-mesa
+
Debian Stretch
~~~~~~~~~~~~~~
+WARNING: Debian Stretch packages Qt 5.7 which is very old (based on a Chromium
+from March 2016 with security fixes from November 2016) and insecure. It is also
+https://www.debian.org/releases/stretch/amd64/release-notes/ch-information.en.html#browser-security[not covered]
+by Debian's security patches. Support for it will be dropped in qutebrowser
+v2.0.0, preliminarily planned for December 2020. It is recommended to
+<<tox,install qutebrowser in a virtualenv>> with a newer PyQt/Qt binary
+instead.
+
Debian Stretch comes with QtWebEngine in the repositories. This makes it possible
to install qutebrowser via the Debian package.
@@ -70,6 +89,14 @@ qutebrowser package.
Debian Buster / Ubuntu 18.04 LTS / Linux Mint 19 (or newer)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+WARNING: Debian Buster packages Qt 5.11 which is very old (based on a Chromium
+from March 2018 with security fixes from November 2018) and insecure. It is also
+https://www.debian.org/releases/buster/amd64/release-notes/ch-information.en.html#browser-security[not covered]
+by Debian's security patches. Support for it will be dropped in qutebrowser
+v2.0.0, preliminarily planned for December 2020. It is recommended to
+<<tox,install qutebrowser in a virtualenv>> with a newer PyQt/Qt binary
+instead.
+
With those distributions, qutebrowser is in the official repositories, and you
can install it with apt:
@@ -80,18 +107,18 @@ can install it with apt:
Additional hints
~~~~~~~~~~~~~~~~
-- Alternatively, you can <<tox,install qutebrowser in a virtualenv>> to get a newer
- QtWebEngine version.
- If running from git, run the following to generate the documentation for the
`:help` command:
+
----
-# apt install --no-install-recommends asciidoc source-highlight
+# apt install --no-install-recommends asciidoc
$ python3 scripts/asciidoc2html.py
----
-- If you prefer using QtWebKit, there's an up-to-date version available in
- https://packages.debian.org/buster/libqt5webkit5[Debian Testing].
+- If you prefer using QtWebKit, there's QtWebKit 5.212 available in
+ https://packages.debian.org/buster/libqt5webkit5[Debian Testing]. Note
+ however that it is based on an upstream WebKit from September 2016 with known
+ security issues and no sandboxing or process isolation.
- If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
+
----
@@ -141,7 +168,8 @@ $ cd ..
$ rm -r qutebrowser-git
----
-or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
+or you could use an AUR helper like https://github.com/Jguer/yay/[yay], e.g.
+`yay -S qutebrowser-git`.
If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
@@ -181,12 +209,6 @@ with:
# xbps-install qutebrowser
----
-It's currently recommended to install `python3-PyQt5-webengine` and
-`python3-PyQt5-opengl`, then start with `--backend webengine` to use the new
-backend.
-
-Since the v1.0 release, qutebrowser uses QtWebEngine by default.
-
On NixOS
--------
@@ -197,18 +219,11 @@ it with:
$ nix-env -i qutebrowser
----
-It's recommended to install `qt5.qtwebengine` and start with
-`--backend webengine` to use the new backend.
-
-Since the v1.0 release, qutebrowser uses QtWebEngine by default.
-
On openSUSE
-----------
There are prebuilt RPMs available at https://software.opensuse.org/download.html?project=network&package=qutebrowser[OBS].
-To use the QtWebEngine backend, install `libqt5-qtwebengine`.
-
On Slackware
------------
@@ -248,7 +263,7 @@ qutebrowser is available
https://flathub.org/apps/details/org.qutebrowser.qutebrowser[on Flathub]
as `org.qutebrowser.qutebrowser`.
-WARNING: As of July 2020, the Flatpak package is severely outdated (qutebrowser
+WARNING: As of October 2020, the Flatpak package is severely outdated (qutebrowser
v1.7.0 from July 2019) and, among other issues, misses fixes for a
(low-severity) https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-4rcq-jv2f-898j[security issue].
It's recommended to <<tox,install qutebrowser in a virtualenv>> instead, which
@@ -350,10 +365,14 @@ qutebrowser from source.
==== Homebrew
----
-$ brew install qt5
+$ brew install qt
+(build PyQt and PyQtWebEngine from source)
$ pip3 install qutebrowser
----
+NOTE: Homebrew does not package PyQtWebEngine (Python wrappers for
+QtWebEngine), so you will need to build that from sources manually.
+
Since the v1.0 release, qutebrowser uses QtWebEngine by default.
Homebrew's builds of Qt and PyQt don't come with QtWebKit (and `--with-qtwebkit`
@@ -364,12 +383,11 @@ https://github.com/annulen/webkit/wiki/Building-QtWebKit-on-OS-X[manually].
Packagers
---------
-There are example .desktop and icon files provided. They would go in the
-standard location for your distro (`/usr/share/applications` and
-`/usr/share/pixmaps` for example).
-
-The normal `setup.py install` doesn't install these files, so you'll have to do
-it as part of the packaging process.
+qutebrowser ships with a
+https://github.com/qutebrowser/qutebrowser/blob/master/misc/Makefile[Makefile]
+intended for packagers. This installs system-wide files in a proper locations,
+so it should be preferred to the usual `setup.py install` or `pip install`
+invocation.
// The tox anchor is so that old links remain compatible.
// When switching to Sphinx, that should be changed.
@@ -405,6 +423,10 @@ $ cd qutebrowser
Installing dependencies (including Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Using a Qt installed via virtualenv needs a couple of system-wide libraries.
+See the <<ubuntu1604,Ubuntu 16.04 section>> for details about which libraries
+are required.
+
Then run the install script:
----
@@ -418,9 +440,8 @@ This installs all needed Python dependencies in a `.venv` subfolder
This comes with an up-to-date Qt/PyQt including a pre-compiled QtWebEngine
binary, but has a few caveats:
-- Make sure your `python3` is Python 3.5 or newer, otherwise you'll get a "No
- matching distribution found" error. Note that qutebrowser itself also requires
- this.
+- Make sure your `python3` is Python 3.6 or newer, otherwise you'll get a "No
+ matching distribution found" error and/or qutebrowser will not run.
- It only works on 64-bit x86 systems, with other architectures you'll get the
same error.
- It comes with a QtWebEngine compiled without proprietary codec support (such
@@ -433,6 +454,12 @@ You can specify a Qt/PyQt version with the `--pyqt-version` flag, see
`mkenv.py --help` for a list of available versions. By default, the latest
version which plays well with qutebrowser is used.
+NOTE: If qutebrowser fails to start with a _"This application failed to start
+because no Qt platform plugin could be initialized."_ message, most likely a
+system-wide library is missing. Run qutebrowser again after
+`export QT_DEBUG_PLUGINS=1` and keep attention to a
+_QLibraryPrivate::loadPlugin failed on ..._ line for details.
+
Installing dependencies (system-wide Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc
index f295a5305..3d95aa25e 100644
--- a/doc/stacktrace.asciidoc
+++ b/doc/stacktrace.asciidoc
@@ -35,7 +35,7 @@ https://www.linuxmint.com/rel_tessa_mate_whatsnew.php[Linux Mint]) and install
the debug packages:
----
-# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebengine-dbg libqt5webengine5-dbgsym
+# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebengine-dbg libqt5webengine5-dbgsym libqt5webenginecore5-dbgsym
----
or with the QtWebKit backend:
diff --git a/misc/apparmor/usr.bin.qutebrowser b/misc/apparmor/usr.bin.qutebrowser
index b993e0058..3d27be697 100644
--- a/misc/apparmor/usr.bin.qutebrowser
+++ b/misc/apparmor/usr.bin.qutebrowser
@@ -1,41 +1,79 @@
-# AppArmor profile for qutebrowser
-# Tested on Debian jessie
-
#include <tunables/global>
profile qutebrowser /usr/{local/,}bin/qutebrowser {
-
#include <abstractions/base>
+ #include <abstractions/python>
+ #include <abstractions/audio>
+ #include <abstractions/dri-common>
+ #include <abstractions/mesa>
+ #include <abstractions/X>
+ #include <abstractions/wayland>
+ #include <abstractions/qt5>
+ #include <abstractions/fonts>
+
+ #include <abstractions/dbus-session-strict>
#include <abstractions/nameservice>
#include <abstractions/openssl>
#include <abstractions/ssl_certs>
- #include <abstractions/audio>
- #include <abstractions/fonts>
- #include <abstractions/kde>
+
+ #include <abstractions/freedesktop.org>
#include <abstractions/user-download>
- #include <abstractions/X>
+ #include <abstractions/user-tmp>
- capability dac_override,
- /usr/{local/,}bin/ r,
- /usr/{local/,}bin/qutebrowser rix,
- /usr/bin/python3.? r,
+ # not nice but required for chromium sandbox
+ capability sys_admin,
+ capability sys_chroot,
+ capability sys_ptrace,
- /usr/lib/python3/ mr,
- /usr/lib/python3/** mr,
- /usr/lib/python3.?/ r,
- /usr/lib/python3.?/** mr,
- /usr/local/lib/python3.?/** r,
+ /dev/ r,
+ /dev/video* r,
+ /etc/mime.types r,
+ /usr/bin/ r,
+ /usr/bin/ldconfig ix,
+ /usr/bin/uname ix,
+ /usr/bin/qutebrowser rix,
+ /usr/lib/qt/libexec/QtWebEngineProcess mrix,
+ /usr/share/pdf.js/** r,
+ /usr/share/qt/translations/qtwebengine_locales/* r,
+ /usr/share/qt/qtwebengine_dictionaries r,
+ /usr/share/qt/qtwebengine_dictionaries/* r,
+ /usr/share/qt/resources/* r,
- /proc/*/mounts r,
- owner /tmp/** rwkl,
- owner /run/user/*/ rw,
- owner /run/user/*/** krw,
+ owner @{HOME}/ r,
+ owner /dev/shm/.org.chromium* rw,
+ owner @{HOME}/.cache/{qtshadercache,qutebrowser}/** rwlk,
+ owner @{HOME}/.cache/qtshadercache** rwl,
+ owner @{HOME}/.config/qutebrowser/** rwlk,
+ owner @{HOME}/.local/share/.org.chromium.Chromium* rw,
+ owner @{HOME}/.local/share/mime/generic-icons r,
+ owner @{HOME}/.local/share/qutebrowser/ r,
+ owner @{HOME}/.local/share/qutebrowser/** rwkl,
+ owner @{HOME}/.pki/nssdb/* rwk,
+ owner @{HOME}/#[0-9]* rwm,
+ owner /run/user/*/qutebrowser/ rw,
+ owner /run/user/*/qutebrowser/* rw,
+ owner /run/user/*/qutebrowser*slave-socket rwl,
+ owner /run/user/*/#* rw,
- @{HOME}/.config/qutebrowser/** krw,
- @{HOME}/.local/share/qutebrowser/** krw,
- @{HOME}/.cache/qutebrowser/** krw,
- @{HOME}/.gstreamer-0.10/* r,
+ # qt/kde
+ @{PROC} r,
+ @{PROC}/sys/fs/inotify/max_user_watches r,
+ @{PROC}/sys/kernel/random/boot_id r,
+ @{PROC}/sys/kernel/core_pattern r,
+ @{PROC}/sys/kernel/yama/ptrace_scope r,
+ /sys/{class,bus}/ r,
+ /sys/bus/pci/devices/ r,
+ /sys/devices/**/{class,config,device,resource,revision,removable,uevent} r,
+ /sys/devices/**/{vendor,subsystem_device,subsystem_vendor} r,
-}
+ owner @{PROC}/@{pid}/{fd,stat,task,mounts}/ r,
+ owner @{PROC}/@{pid}/stat r,
+ owner @{PROC}/@{pid}/task/@{pid}/status r,
+ owner @{PROC}/@{pid}/{setgroups,gid_map,oom_score_adj,uid_map} rw,
+ owner @{PROC}/@{pid}/{oom_score_adj,uid_map} rw,
+
+ # allow execution of userscripts
+ /usr/share/qutebrowser/userscripts/* Ux,
+}
diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml
index e246ea4d7..f0885b8ed 100644
--- a/misc/org.qutebrowser.qutebrowser.appdata.xml
+++ b/misc/org.qutebrowser.qutebrowser.appdata.xml
@@ -44,6 +44,7 @@
</content_rating>
<releases>
<!-- Add new releases here -->
+<release version="1.14.0" date="2020-10-15"/>
<release version="1.13.1" date="2020-07-17"/>
<release version="1.13.0" date="2020-06-26"/>
<release version="1.12.0" date="2020-06-01"/>
diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt
index 4cc00982d..9993cf4dd 100644
--- a/misc/requirements/requirements-check-manifest.txt
+++ b/misc/requirements/requirements-check-manifest.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-check-manifest==0.42
-pep517==0.8.2
+check-manifest==0.44
+pep517==0.9.1
toml==0.10.1
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index 798fecad6..e885b3335 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -1,20 +1,20 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-bump2version==1.0.0
+bump2version==1.0.1
certifi==2020.6.20
-cffi==1.14.2
+cffi==1.14.3
chardet==3.0.4
-colorama==0.4.3
-cryptography==3.1
+colorama==0.4.4
+cryptography==3.2
cssutils==1.0.2
github3.py==1.3.0
-hunter==3.2.2
+hunter==3.3.1
idna==2.10
jwcrypto==0.8
manhole==1.6.0
packaging==20.4
pycparser==2.20
-Pympler==0.8
+Pympler==0.9
pyparsing==2.4.7
PyQt-builder==1.5.0
python-dateutil==2.8.1
@@ -23,4 +23,4 @@ sip==5.4.0
six==1.15.0
toml==0.10.1
uritemplate==3.0.1
-# urllib3==1.25.10
+# urllib3==1.25.11
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index afd4f0bd6..d020c02a2 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -1,10 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==20.2.0
-flake8==3.8.3
+flake8==3.8.4
flake8-bugbear==20.1.4
flake8-builtins==1.5.3
-flake8-comprehensions==3.2.3
+flake8-comprehensions==3.3.0
flake8-copyright==0.2.2
flake8-debugger==3.2.1
flake8-deprecated==1.3
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index 863f48c6f..ff3d430e8 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -1,15 +1,15 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-diff-cover==4.0.0
+diff-cover==4.0.1
inflect==4.1.0
Jinja2==2.11.2
jinja2-pluralize==0.3.0
-lxml==4.5.2
+lxml==4.6.1
MarkupSafe==1.1.1
-mypy==0.782
+mypy==0.790
mypy-extensions==0.4.3
pluggy==0.13.1
-Pygments==2.7.1
--e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5_stubs
+Pygments==2.7.2
+-e git+https://github.com/stlehmann/PyQt5-stubs.git@811462b34ee151b898289ae8f1de8af30c690c55#egg=PyQt5_stubs
typed-ast==1.4.1
typing-extensions==3.7.4.3
diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw
index d3b0dc4ca..7c888ed1e 100644
--- a/misc/requirements/requirements-mypy.txt-raw
+++ b/misc/requirements/requirements-mypy.txt-raw
@@ -2,6 +2,3 @@ mypy
lxml # For HTML reports
diff-cover
-e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5-stubs
-
-# remove @commit-id for scm installs
-#@ replace: @.*# @master#
diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt
index 06405d96a..7941d2772 100644
--- a/misc/requirements/requirements-pyinstaller.txt
+++ b/misc/requirements/requirements-pyinstaller.txt
@@ -2,4 +2,4 @@
altgraph==0.17
pyinstaller==4.0
-pyinstaller-hooks-contrib==2020.8
+pyinstaller-hooks-contrib==2020.9
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index 08c1d2c10..75a0e8e0f 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -2,9 +2,9 @@
astroid==2.3.3 # rq.filter: < 2.4
certifi==2020.6.20
-cffi==1.14.2
+cffi==1.14.3
chardet==3.0.4
-cryptography==3.1
+cryptography==3.2
github3.py==1.3.0
idna==2.10
isort==4.3.21
@@ -19,5 +19,5 @@ requests==2.24.0
six==1.15.0
typed-ast==1.4.1 ; python_version<"3.8"
uritemplate==3.0.1
-# urllib3==1.25.10
+# urllib3==1.25.11
wrapt==1.11.2
diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt
index 88a04230d..148e8d8bb 100644
--- a/misc/requirements/requirements-pyqt.txt
+++ b/misc/requirements/requirements-pyqt.txt
@@ -2,4 +2,4 @@
PyQt5==5.15.1
PyQt5-sip==12.8.1
-PyQtWebEngine==5.15.1
+PyQtWebEngine==5.15.0
diff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw
index 9c6afbf16..83ebc7671 100644
--- a/misc/requirements/requirements-pyqt.txt-raw
+++ b/misc/requirements/requirements-pyqt.txt-raw
@@ -1,2 +1,2 @@
PyQt5
-PyQtWebEngine
+PyQtWebEngine!=5.15.1
diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt
index c6e751e19..1a2dbde7f 100644
--- a/misc/requirements/requirements-pyroma.txt
+++ b/misc/requirements/requirements-pyroma.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.16
-Pygments==2.7.1
+Pygments==2.7.2
pyroma==2.6
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index 7b43a72a1..baeea4d40 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -10,7 +10,7 @@ imagesize==1.2.0
Jinja2==2.11.2
MarkupSafe==1.1.1
packaging==20.4
-Pygments==2.7.1
+Pygments==2.7.2
pyparsing==2.4.7
pytz==2020.1
requests==2.24.0
@@ -23,4 +23,4 @@ sphinxcontrib-htmlhelp==1.0.3
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.4
-urllib3==1.25.10
+urllib3==1.25.11
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 789c176e6..066f4c4db 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -2,23 +2,25 @@
apipkg==1.5
attrs==20.2.0
-beautifulsoup4==4.9.1
+beautifulsoup4==4.9.3
certifi==2020.6.20
chardet==3.0.4
cheroot==8.4.5
click==7.1.2
-# colorama==0.4.3
+# colorama==0.4.4
coverage==5.3
EasyProcess==0.3
execnet==1.7.1
+filelock==3.0.12
Flask==1.1.2
glob2==0.7
-hunter==3.2.2
-hypothesis==5.35.3 ; python_version>="3.6"
+hunter==3.3.1
+hypothesis==5.38.0
+icdiff==1.9.1
idna==2.10
-iniconfig==1.0.1
+iniconfig==1.1.1
itsdangerous==1.1.0
-jaraco.functools==3.0.1 ; python_version>="3.6"
+jaraco.functools==3.0.1
# Jinja2==2.11.2
Mako==1.1.3
manhole==1.6.0
@@ -28,20 +30,23 @@ packaging==20.4
parse==1.18.0
parse-type==0.5.2
pluggy==0.13.1
+pprintpp==0.4.0
py==1.9.0
py-cpuinfo==7.0.0
-Pygments==2.7.1
+Pygments==2.7.2
pyparsing==2.4.7
-pytest==6.0.2
+pytest==6.1.1
pytest-bdd==4.0.1
pytest-benchmark==3.2.3
+pytest-clarity==0.3.0a0
pytest-cov==2.10.1
pytest-forked==1.3.0
+pytest-icdiff==0.5
pytest-instafail==0.4.2
pytest-mock==3.3.1
pytest-qt==3.3.0
pytest-repeat==0.8.0
-pytest-rerunfailures==9.1
+pytest-rerunfailures==9.1.1
pytest-xdist==2.1.0
pytest-xvfb==2.0.0
PyVirtualDisplay==1.3.2
@@ -50,11 +55,9 @@ requests-file==1.5.1
six==1.15.0
sortedcontainers==2.2.2
soupsieve==2.0.1
-tldextract==2.2.3
+termcolor==1.1.0
+tldextract==3.0.2
toml==0.10.1
-urllib3==1.25.10
-vulture==2.1 ; python_version>="3.6"
+urllib3==1.25.11
+vulture==2.1
Werkzeug==1.0.1
-jaraco.functools==2.0; python_version<"3.6"
-vulture==1.6; python_version<"3.6"
-hypothesis<5.34.0; python_version<"3.6"
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index 73f58461c..fd346d475 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -27,17 +27,11 @@ pytest-xvfb
PyVirtualDisplay
# To run on multiple cores with -n
pytest-xdist
+# For nicer output
+pytest-icdiff
+pytest-clarity
# Needed to test misc/userscripts/qute-lastpass
tldextract
-#@ markers: jaraco.functools python_version>="3.6"
-#@ add: jaraco.functools==2.0; python_version<"3.6"
-
-#@ markers: vulture python_version>="3.6"
-#@ add: vulture==1.6; python_version<"3.6"
-
-#@ markers: hypothesis python_version>="3.6"
-#@ add: hypothesis<5.34.0; python_version<"3.6"
-
#@ ignore: Jinja2, MarkupSafe, colorama
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index c7f99a2da..a77333637 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -9,7 +9,7 @@ py==1.9.0
pyparsing==2.4.7
six==1.15.0
toml==0.10.1
-tox==3.20.0
+tox==3.20.1
tox-pip-version==0.0.7
tox-venv==0.4.0
-virtualenv==20.0.31
+virtualenv==20.1.0
diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt
index 0ea42bf7c..5f80fd5d5 100644
--- a/misc/requirements/requirements-yamllint.txt
+++ b/misc/requirements/requirements-yamllint.txt
@@ -2,4 +2,4 @@
pathspec==0.8.0
PyYAML==5.3.1
-yamllint==1.24.2
+yamllint==1.25.0
diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md
index d2519a672..0e3a5ffc3 100644
--- a/misc/userscripts/README.md
+++ b/misc/userscripts/README.md
@@ -63,6 +63,8 @@ The following userscripts can be found on their own repositories.
snippets on web pages to the clipboard via hints.
- [Qute-Translate](https://github.com/AckslD/Qute-Translate): Translate URLs or
selections via Google Translate.
+- [qute-snippets](https://github.com/Aledosim/qute-snippets): Bind text snippets to a keyword
+ and retrieve they when you want.
[Zotero]: https://www.zotero.org/
[Pocket]: https://getpocket.com/
diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js
index 207b29b4a..e189e5ee4 100755
--- a/misc/userscripts/readability-js
+++ b/misc/userscripts/readability-js
@@ -54,6 +54,35 @@ const HEADER = `
figure img {
display: block;
}
+ table,
+ th,
+ td {
+ border: 1px solid currentColor;
+ border-collapse: collapse;
+ padding: 6px;
+ vertical-align: top;
+ }
+ table {
+ margin: 5px;
+ }
+ pre {
+ padding: 16px;
+ overflow: auto;
+ line-height: 1.45;
+ background-color: #dddddd;
+ }
+ code {
+ padding: .2em .4em;
+ margin: 0;
+ background-color: #dddddd;
+ }
+ blockquote {
+ border-inline-start: 2px solid #333333 !important;
+ padding: 0;
+ padding-inline-start: 16px;
+ margin-inline-start: 24px;
+ border-radius: 5px;
+ }
</style>
<!-- This icon is licensed under the Mozilla Public License 2.0 (available at: https://www.mozilla.org/en-US/MPL/2.0/).
The original icon can be found here: https://dxr.mozilla.org/mozilla-central/source/browser/themes/shared/reader/readerMode.svg -->
@@ -68,13 +97,24 @@ const HEADER = `
</head>`;
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)){
+if (!fs.existsSync(scriptsDir)) {
fs.mkdirSync(scriptsDir);
}
-JSDOM.fromFile(process.env.QUTE_HTML, domOpts).then(dom => {
+let getDOM, domOpts, target;
+// When hinting, use the selected hint instead of the current page
+if (process.env.QUTE_MODE === 'hints') {
+ getDOM = JSDOM.fromURL;
+ target = process.env.QUTE_URL;
+}
+else {
+ getDOM = JSDOM.fromFile;
+ domOpts = {url: process.env.QUTE_URL, contentType: "text/html; charset=utf-8"};
+ target = process.env.QUTE_HTML;
+}
+
+getDOM(target, 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/pytest.ini b/pytest.ini
index 51411e11e..0b4fecf37 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -76,6 +76,7 @@ qt_log_ignore =
^QPaintDevice: Cannot destroy paint device that is being painted
^DirectWrite: CreateFontFaceFromHDC\(\) failed .*
^Attribute Qt::AA_ShareOpenGLContexts must be set before QCoreApplication is created\.
+ ^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not de-queue request, failed to report HostNotFoundError
xfail_strict = true
filterwarnings =
error
diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py
index 34799df17..b5b4b8c7c 100644
--- a/qutebrowser/__init__.py
+++ b/qutebrowser/__init__.py
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2020 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
-__version__ = "1.13.1"
+__version__ = "1.14.0"
__version_info__ = tuple(int(part) for part in __version__.split('.'))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py
index 5d74991c1..d0d69a04c 100644
--- a/qutebrowser/api/cmdutils.py
+++ b/qutebrowser/api/cmdutils.py
@@ -45,12 +45,12 @@ Possible values:
value.
- A python enum type: All members of the enum are possible values.
- A ``typing.Union`` of multiple types above: Any of these types are valid
- values, e.g., ``typing.Union[str, int]``.
+ values, e.g., ``Union[str, int]``.
"""
import inspect
-import typing
+from typing import Any, Callable, Iterable
from qutebrowser.utils import qtutils
from qutebrowser.commands import command, cmdexc
@@ -91,8 +91,7 @@ def check_overflow(arg: int, ctype: str) -> None:
"representation.".format(ctype))
-def check_exclusive(flags: typing.Iterable[bool],
- names: typing.Iterable[str]) -> None:
+def check_exclusive(flags: Iterable[bool], names: Iterable[str]) -> None:
"""Check if only one flag is set with exclusive flags.
Raise a CommandError if not.
@@ -113,7 +112,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
def __init__(self, *,
instance: str = None,
name: str = None,
- **kwargs: typing.Any) -> None:
+ **kwargs: Any) -> None:
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
@@ -128,7 +127,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
# The arguments to pass to Command.
self._kwargs = kwargs
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
"""Register the command before running the function.
Gets called when a function should be decorated.
@@ -175,7 +174,7 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name
def foo(bar: str):
...
- For ``typing.Union`` types, the given ``choices`` are only checked if other
+ For ``Union`` types, the given ``choices`` are only checked if other
types (like ``int``) don't match.
The following arguments are supported for ``@cmdutils.argument``:
@@ -197,11 +196,11 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name
trailing underscores stripped and underscores replaced by dashes.
"""
- def __init__(self, argname: str, **kwargs: typing.Any) -> None:
+ def __init__(self, argname: str, **kwargs: Any) -> None:
self._argname = argname # The name of the argument to handle.
self._kwargs = kwargs # Valid ArgInfo members.
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
funcname = func.__name__
if self._argname not in inspect.signature(func).parameters:
diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py
index 3b84a999c..fb363d858 100644
--- a/qutebrowser/api/config.py
+++ b/qutebrowser/api/config.py
@@ -19,7 +19,7 @@
"""Access to the qutebrowser configuration."""
-import typing
+from typing import cast, Any
from PyQt5.QtCore import QUrl
@@ -35,9 +35,9 @@ from qutebrowser.config import config
#: This also supports setting configuration values::
#:
#: config.val.content.javascript.enabled = False
-val = typing.cast('config.ConfigContainer', None)
+val = cast('config.ConfigContainer', None)
-def get(name: str, url: QUrl = None) -> typing.Any:
+def get(name: str, url: QUrl = None) -> Any:
"""Get a value from the config based on a string name."""
return config.instance.get(name, url)
diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py
index 9bd14a8a1..4eadb2a99 100644
--- a/qutebrowser/api/hook.py
+++ b/qutebrowser/api/hook.py
@@ -22,13 +22,13 @@
"""Hooks for extensions."""
import importlib
-import typing
+from typing import Callable
from qutebrowser.extensions import loader
-def _add_module_info(func: typing.Callable) -> loader.ModuleInfo:
+def _add_module_info(func: Callable) -> loader.ModuleInfo:
"""Add module info to the given function."""
module = importlib.import_module(func.__module__)
return loader.add_module_info(module)
@@ -48,7 +48,7 @@ class init:
message.info("Extension initialized.")
"""
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
info = _add_module_info(func)
if info.init_hook is not None:
raise ValueError("init hook is already registered!")
@@ -86,7 +86,7 @@ class config_changed:
def __init__(self, option_filter: str = None) -> None:
self._filter = option_filter
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
info = _add_module_info(func)
info.config_changed_hooks.append((self._filter, func))
return func
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 20459b890..27f03fd54 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -82,6 +82,8 @@ def run(args):
if args.temp_basedir:
args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-')
+ log.init.debug("Main process PID: {}".format(os.getpid()))
+
log.init.debug("Initializing directories...")
standarddir.init(args)
utils.preload_resources()
@@ -553,15 +555,15 @@ class Application(QApplication):
def event(self, e):
"""Handle macOS FileOpen events."""
- if e.type() == QEvent.FileOpen:
- url = e.url()
- if url.isValid():
- open_url(url, no_raise=True)
- else:
- message.error("Invalid URL: {}".format(url.errorString()))
- else:
+ if e.type() != QEvent.FileOpen:
return super().event(e)
+ url = e.url()
+ if url.isValid():
+ open_url(url, no_raise=True)
+ else:
+ message.error("Invalid URL: {}".format(url.errorString()))
+
return True
def __repr__(self):
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index f7d951b33..58c434a42 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -22,7 +22,8 @@
import enum
import itertools
import functools
-import typing
+from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional,
+ Sequence, Set, Type, Union)
import attr
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt,
@@ -32,9 +33,10 @@ from PyQt5.QtWidgets import QWidget, QApplication, QDialog
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtNetwork import QNetworkAccessManager
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from PyQt5.QtWebKit import QWebHistory
- from PyQt5.QtWebEngineWidgets import QWebEngineHistory
+ from PyQt5.QtWebKitWidgets import QWebPage
+ from PyQt5.QtWebEngineWidgets import QWebEngineHistory, QWebEnginePage
import pygments
import pygments.lexers
@@ -48,7 +50,7 @@ from qutebrowser.misc import miscwidgets, objects, sessions
from qutebrowser.browser import eventfilter, inspector
from qutebrowser.qt import sip
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser import webelem
from qutebrowser.browser.inspector import AbstractWebInspector
@@ -71,7 +73,7 @@ def create(win_id: int,
mode_manager = modeman.instance(win_id)
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
- tab_class = webenginetab.WebEngineTab # type: typing.Type[AbstractTab]
+ tab_class: Type[AbstractTab] = webenginetab.WebEngineTab
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkittab
tab_class = webkittab.WebKitTab
@@ -100,13 +102,23 @@ class UnsupportedOperationError(WebTabError):
"""Raised when an operation is not supported with the given backend."""
-TerminationStatus = enum.Enum('TerminationStatus', [
- 'normal',
- 'abnormal', # non-zero exit status
- 'crashed', # e.g. segfault
- 'killed',
- 'unknown',
-])
+class TerminationStatus(enum.Enum):
+
+ """How a QtWebEngine renderer process terminated.
+
+ Also see QWebEnginePage::RenderProcessTerminationStatus
+ """
+
+ #: Unknown render process status value gotten from Qt.
+ unknown = -1
+ #: The render process terminated normally.
+ normal = 0
+ #: The render process terminated with with a non-zero exit status.
+ abnormal = 1
+ #: The render process crashed, for example because of a segmentation fault.
+ crashed = 2
+ #: The render process was killed, for example by SIGKILL or task manager kill.
+ killed = 3
@attr.s
@@ -131,19 +143,17 @@ class TabData:
splitter: InspectorSplitter used to show inspector inside the tab.
"""
- keep_icon = attr.ib(False) # type: bool
- viewing_source = attr.ib(False) # type: bool
- inspector = attr.ib(None) # type: typing.Optional[AbstractWebInspector]
- open_target = attr.ib(
- usertypes.ClickTarget.normal) # type: usertypes.ClickTarget
- override_target = attr.ib(
- None) # type: typing.Optional[usertypes.ClickTarget]
- pinned = attr.ib(False) # type: bool
- fullscreen = attr.ib(False) # type: bool
- netrc_used = attr.ib(False) # type: bool
- input_mode = attr.ib(usertypes.KeyMode.normal) # type: usertypes.KeyMode
- last_navigation = attr.ib(None) # type: usertypes.NavigationRequest
- splitter = attr.ib(None) # type: miscwidgets.InspectorSplitter
+ keep_icon: bool = attr.ib(False)
+ viewing_source: bool = attr.ib(False)
+ inspector: Optional['AbstractWebInspector'] = attr.ib(None)
+ open_target: usertypes.ClickTarget = attr.ib(usertypes.ClickTarget.normal)
+ override_target: Optional[usertypes.ClickTarget] = attr.ib(None)
+ pinned: bool = attr.ib(False)
+ fullscreen: bool = attr.ib(False)
+ netrc_used: bool = attr.ib(False)
+ input_mode: usertypes.KeyMode = attr.ib(usertypes.KeyMode.normal)
+ last_navigation: usertypes.NavigationRequest = attr.ib(None)
+ splitter: miscwidgets.InspectorSplitter = attr.ib(None)
def should_show_icon(self) -> bool:
return (config.val.tabs.favicons.show == 'always' or
@@ -154,13 +164,11 @@ class AbstractAction:
"""Attribute ``action`` of AbstractTab for Qt WebActions."""
- # The class actions are defined on (QWeb{Engine,}Page)
- action_class = None # type: type
- # The type of the actions (QWeb{Engine,}Page.WebAction)
- action_base = None # type: type
+ action_class: Type[Union['QWebPage', 'QWebEnginePage']]
+ action_base: Type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']]
def __init__(self, tab: 'AbstractTab') -> None:
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._tab = tab
def exit_fullscreen(self) -> None:
@@ -211,7 +219,7 @@ class AbstractPrinting:
"""Attribute ``printing`` of AbstractTab for printing the page."""
def __init__(self, tab: 'AbstractTab') -> None:
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._tab = tab
def check_pdf_support(self) -> None:
@@ -243,7 +251,7 @@ class AbstractPrinting:
raise NotImplementedError
def to_printer(self, printer: QPrinter,
- callback: typing.Callable[[bool], None] = None) -> None:
+ callback: Callable[[bool], None] = None) -> None:
"""Print the tab.
Args:
@@ -295,13 +303,13 @@ class AbstractSearch(QObject):
#: Signal emitted when an existing search was cleared.
cleared = pyqtSignal()
- _Callback = typing.Callable[[bool], None]
+ _Callback = Callable[[bool], None]
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
- self.text = None # type: typing.Optional[str]
+ self._widget = cast(QWidget, None)
+ self.text: Optional[str] = None
self.search_displayed = False
def _is_case_sensitive(self, ignore_case: usertypes.IgnoreCase) -> bool:
@@ -364,7 +372,7 @@ class AbstractZoom(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
# Whether zoom was changed from the default.
self._default_zoom_changed = False
self._init_neighborlist()
@@ -384,9 +392,8 @@ class AbstractZoom(QObject):
It is a NeighborList with the zoom levels."""
levels = config.val.zoom.levels
- self._neighborlist = usertypes.NeighborList(
- levels, mode=usertypes.NeighborList.Modes.edge
- ) # type: usertypes.NeighborList[float]
+ self._neighborlist: usertypes.NeighborList[float] = usertypes.NeighborList(
+ levels, mode=usertypes.NeighborList.Modes.edge)
self._neighborlist.fuzzyval = config.val.zoom.default
def apply_offset(self, offset: int) -> float:
@@ -440,9 +447,9 @@ class SelectionState(enum.Enum):
NOTE: Names need to line up with SelectionState in caret.js!
"""
- none = 1
- normal = 2
- line = 3
+ none = enum.auto()
+ normal = enum.auto()
+ line = enum.auto()
class AbstractCaret(QObject):
@@ -455,14 +462,15 @@ class AbstractCaret(QObject):
follow_selected_done = pyqtSignal()
def __init__(self,
+ tab: 'AbstractTab',
mode_manager: modeman.ModeManager,
parent: QWidget = None) -> None:
super().__init__(parent)
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._mode_manager = mode_manager
mode_manager.entered.connect(self._on_mode_entered)
mode_manager.left.connect(self._on_mode_left)
- # self._tab is set by subclasses so mypy knows its concrete type.
+ self._tab = tab
def _on_mode_entered(self, mode: usertypes.KeyMode) -> None:
raise NotImplementedError
@@ -521,7 +529,7 @@ class AbstractCaret(QObject):
def drop_selection(self) -> None:
raise NotImplementedError
- def selection(self, callback: typing.Callable[[str], None]) -> None:
+ def selection(self, callback: Callable[[str], None]) -> None:
raise NotImplementedError
def reverse_selection(self) -> None:
@@ -551,7 +559,7 @@ class AbstractScroller(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
if 'log-scroll-pos' in objects.debug_flags:
self.perc_changed.connect(self._log_scroll_pos_change)
@@ -619,11 +627,6 @@ class AbstractHistoryPrivate:
"""Private API related to the history."""
- def __init__(self, tab: 'AbstractTab'):
- self._tab = tab
- self._history = typing.cast(
- typing.Union['QWebHistory', 'QWebEngineHistory'], None)
-
def serialize(self) -> bytes:
"""Serialize into an opaque format understood by self.deserialize."""
raise NotImplementedError
@@ -632,7 +635,7 @@ class AbstractHistoryPrivate:
"""Deserialize from a format produced by self.serialize."""
raise NotImplementedError
- def load_items(self, items: typing.Sequence) -> None:
+ def load_items(self, items: Sequence) -> None:
"""Deserialize from a list of WebHistoryItems."""
raise NotImplementedError
@@ -643,14 +646,13 @@ class AbstractHistory:
def __init__(self, tab: 'AbstractTab') -> None:
self._tab = tab
- self._history = typing.cast(
- typing.Union['QWebHistory', 'QWebEngineHistory'], None)
- self.private_api = AbstractHistoryPrivate(tab)
+ self._history = cast(Union['QWebHistory', 'QWebEngineHistory'], None)
+ self.private_api = AbstractHistoryPrivate()
def __len__(self) -> int:
raise NotImplementedError
- def __iter__(self) -> typing.Iterable:
+ def __iter__(self) -> Iterable:
raise NotImplementedError
def _check_count(self, count: int) -> None:
@@ -687,16 +689,16 @@ class AbstractHistory:
def can_go_forward(self) -> bool:
raise NotImplementedError
- def _item_at(self, i: int) -> typing.Any:
+ def _item_at(self, i: int) -> Any:
raise NotImplementedError
- def _go_to_item(self, item: typing.Any) -> None:
+ def _go_to_item(self, item: Any) -> None:
raise NotImplementedError
- def back_items(self) -> typing.List[typing.Any]:
+ def back_items(self) -> List[Any]:
raise NotImplementedError
- def forward_items(self) -> typing.List[typing.Any]:
+ def forward_items(self) -> List[Any]:
raise NotImplementedError
@@ -704,15 +706,13 @@ class AbstractElements:
"""Finding and handling of elements on the page."""
- _MultiCallback = typing.Callable[
- [typing.Sequence['webelem.AbstractWebElement']], None]
- _SingleCallback = typing.Callable[
- [typing.Optional['webelem.AbstractWebElement']], None]
- _ErrorCallback = typing.Callable[[Exception], None]
+ _MultiCallback = Callable[[Sequence['webelem.AbstractWebElement']], None]
+ _SingleCallback = Callable[[Optional['webelem.AbstractWebElement']], None]
+ _ErrorCallback = Callable[[Exception], None]
- def __init__(self) -> None:
- self._widget = typing.cast(QWidget, None)
- # self._tab is set by subclasses so mypy knows its concrete type.
+ def __init__(self, tab: 'AbstractTab') -> None:
+ self._widget = cast(QWidget, None)
+ self._tab = tab
def find_css(self, selector: str,
callback: _MultiCallback,
@@ -772,7 +772,7 @@ class AbstractAudio(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent)
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._tab = tab
def set_muted(self, muted: bool, override: bool = False) -> None:
@@ -803,7 +803,7 @@ class AbstractTabPrivate:
def __init__(self, mode_manager: modeman.ModeManager,
tab: 'AbstractTab') -> None:
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._tab = tab
self._mode_manager = mode_manager
@@ -821,7 +821,7 @@ class AbstractTabPrivate:
return
def _auto_insert_mode_cb(
- elem: typing.Optional['webelem.AbstractWebElement']
+ elem: Optional['webelem.AbstractWebElement']
) -> None:
"""Called from JS after finding the focused element."""
if elem is None:
@@ -836,7 +836,7 @@ class AbstractTabPrivate:
def clear_ssl_errors(self) -> None:
raise NotImplementedError
- def networkaccessmanager(self) -> typing.Optional[QNetworkAccessManager]:
+ def networkaccessmanager(self) -> Optional[QNetworkAccessManager]:
"""Get the QNetworkAccessManager for this tab.
This is only implemented for QtWebKit.
@@ -928,7 +928,7 @@ class AbstractTab(QWidget):
# Note that we remember hosts here, without scheme/port:
# QtWebEngine/Chromium also only remembers hostnames, and certificates are
# for a given hostname anyways.
- _insecure_hosts = set() # type: typing.Set[str]
+ _insecure_hosts: Set[str] = set()
def __init__(self, *, win_id: int,
mode_manager: modeman.ModeManager,
@@ -948,12 +948,12 @@ class AbstractTab(QWidget):
self.data = TabData()
self._layout = miscwidgets.WrapperLayout(self)
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._progress = 0
self._load_status = usertypes.LoadStatus.none
self._tab_event_filter = eventfilter.TabEventFilter(
self, parent=self)
- self.backend = None # type: typing.Optional[usertypes.Backend]
+ self.backend: Optional[usertypes.Backend] = None
# If true, this tab has been requested to be removed (or is removed).
self.pending_removal = False
@@ -1156,7 +1156,7 @@ class AbstractTab(QWidget):
self.send_event(release_evt)
def dump_async(self,
- callback: typing.Callable[[str], None], *,
+ callback: Callable[[str], None], *,
plain: bool = False) -> None:
"""Dump the current page's html asynchronously.
@@ -1168,8 +1168,8 @@ class AbstractTab(QWidget):
def run_js_async(
self,
code: str,
- callback: typing.Callable[[typing.Any], None] = None, *,
- world: typing.Union[usertypes.JsWorld, int] = None
+ callback: Callable[[Any], None] = None, *,
+ world: Union[usertypes.JsWorld, int] = None
) -> None:
"""Run javascript async.
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 13f57e7dc..5a65384f3 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -22,7 +22,7 @@
import os.path
import shlex
import functools
-import typing
+from typing import cast, Callable, Dict, Union
from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery
@@ -600,15 +600,14 @@ class CommandDispatcher:
widget = self._current_widget()
url = self._current_url()
- handlers = {
+ handlers: Dict[str, Callable] = {
'prev': functools.partial(navigate.prevnext, prev=True),
'next': functools.partial(navigate.prevnext, prev=False),
'up': navigate.path_up,
- 'decrement': functools.partial(navigate.incdec,
- inc_or_dec='decrement'),
- 'increment': functools.partial(navigate.incdec,
- inc_or_dec='increment'),
- } # type: typing.Dict[str, typing.Callable]
+ 'strip': navigate.strip,
+ 'decrement': functools.partial(navigate.incdec, inc_or_dec='decrement'),
+ 'increment': functools.partial(navigate.incdec, inc_or_dec='increment'),
+ }
try:
if where in ['prev', 'next']:
@@ -949,7 +948,7 @@ class CommandDispatcher:
@cmdutils.argument('index', choices=['last', 'stack-next', 'stack-prev'],
completion=miscmodels.tab_focus)
@cmdutils.argument('count', value=cmdutils.Value.count)
- def tab_focus(self, index: typing.Union[str, int] = None,
+ def tab_focus(self, index: Union[str, int] = None,
count: int = None, no_last: bool = False) -> None:
"""Select the tab given as argument/[count].
@@ -993,7 +992,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['+', '-'])
@cmdutils.argument('count', value=cmdutils.Value.count)
- def tab_move(self, index: typing.Union[str, int] = None,
+ def tab_move(self, index: Union[str, int] = None,
count: int = None) -> None:
"""Move the current tab according to the argument and [count].
@@ -1431,7 +1430,7 @@ class CommandDispatcher:
query = QUrlQuery()
query.addQueryItem('level', level)
if plain:
- query.addQueryItem('plain', typing.cast(str, None))
+ query.addQueryItem('plain', cast(str, None))
if logfilter:
try:
@@ -1652,7 +1651,7 @@ class CommandDispatcher:
url: bool = False,
quiet: bool = False,
*,
- world: typing.Union[usertypes.JsWorld, int] = None) -> None:
+ world: Union[usertypes.JsWorld, int] = None) -> None:
"""Evaluate a JavaScript string.
Args:
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index 3c3932c5f..5430cde20 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -28,7 +28,7 @@ import functools
import pathlib
import tempfile
import enum
-import typing
+from typing import Any, Dict, IO, List, MutableSequence, Optional, Union
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel, QUrl)
@@ -49,7 +49,7 @@ class ModelRole(enum.IntEnum):
# Remember the last used directory
-last_used_directory = None # type: typing.Optional[str]
+last_used_directory: Optional[str] = None
# All REFRESH_INTERVAL milliseconds, speeds will be recalculated and downloads
# redrawn.
@@ -228,7 +228,7 @@ def suggested_fn_from_title(url_path, title=None):
ext_whitelist = [".html", ".htm", ".php", ""]
_, ext = os.path.splitext(url_path)
- suggested_fn = None # type: typing.Optional[str]
+ suggested_fn: Optional[str] = None
if ext.lower() in ext_whitelist and title:
suggested_fn = utils.sanitize_filename(title, shorten=True)
if not suggested_fn.lower().endswith((".html", ".htm")):
@@ -355,8 +355,7 @@ class DownloadItemStats(QObject):
self.speed = 0
self._last_done = 0
samples = int(self.SPEED_AVG_WINDOW * (1000 / _REFRESH_INTERVAL))
- self._speed_avg = collections.deque(
- maxlen=samples) # type: typing.MutableSequence[float]
+ self._speed_avg: MutableSequence[float] = collections.deque(maxlen=samples)
def update_speed(self):
"""Recalculate the current download speed.
@@ -459,12 +458,14 @@ class AbstractDownloadItem(QObject):
self.basename = '???'
self.successful = False
- self.fileobj = UnsupportedAttribute(
- ) # type: typing.Union[UnsupportedAttribute, typing.IO[bytes], None]
- self.raw_headers = UnsupportedAttribute(
- ) # type: typing.Union[UnsupportedAttribute, typing.Dict[bytes,bytes]]
+ self.fileobj: Union[
+ UnsupportedAttribute, IO[bytes], None
+ ] = UnsupportedAttribute()
+ self.raw_headers: Union[
+ UnsupportedAttribute, Dict[bytes, bytes]
+ ] = UnsupportedAttribute()
- self._filename = None # type: typing.Optional[str]
+ self._filename: Optional[str] = None
self._dead = False
def __repr__(self):
@@ -877,7 +878,7 @@ class AbstractDownloadManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
- self.downloads = [] # type: typing.List[AbstractDownloadItem]
+ self.downloads: List[AbstractDownloadItem] = []
self._update_timer = usertypes.Timer(self, 'download-update')
self._update_timer.timeout.connect(self._update_gui)
self._update_timer.setInterval(_REFRESH_INTERVAL)
@@ -1251,7 +1252,7 @@ class DownloadModel(QAbstractListModel):
item = self[index.row()]
if role == Qt.DisplayRole:
- data = str(item) # type: typing.Any
+ data: Any = str(item)
elif role == Qt.ForegroundRole:
data = item.get_status_color('fg')
elif role == Qt.BackgroundRole:
@@ -1297,7 +1298,7 @@ class TempDownloadManager:
"""
def __init__(self):
- self.files = [] # type: typing.MutableSequence[typing.IO[bytes]]
+ self.files: MutableSequence[IO[bytes]] = []
self._tmpdir = None
def cleanup(self):
diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py
index 178fb5357..bff4a8c93 100644
--- a/qutebrowser/browser/downloadview.py
+++ b/qutebrowser/browser/downloadview.py
@@ -20,7 +20,7 @@
"""The ListView to display downloads in."""
import functools
-import typing
+from typing import Callable, MutableSequence, Tuple, Union
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
@@ -54,10 +54,10 @@ def update_geometry(obj):
QTimer.singleShot(0, _update_geometry)
-_ActionListType = typing.MutableSequence[
- typing.Union[
- typing.Tuple[None, None], # separator
- typing.Tuple[str, typing.Callable[[], None]],
+_ActionListType = MutableSequence[
+ Union[
+ Tuple[None, None], # separator
+ Tuple[str, Callable[[], None]],
]
]
@@ -142,7 +142,7 @@ class DownloadView(QListView):
item: The DownloadItem to get the actions for, or None.
"""
model = self.model()
- actions = [] # type: _ActionListType
+ actions: _ActionListType = []
if item is None:
pass
elif item.done:
diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py
index 89d720682..5a5d55a6d 100644
--- a/qutebrowser/browser/greasemonkey.py
+++ b/qutebrowser/browser/greasemonkey.py
@@ -26,7 +26,7 @@ import fnmatch
import functools
import glob
import textwrap
-import typing
+from typing import cast, List, Sequence
import attr
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
@@ -39,7 +39,7 @@ from qutebrowser.browser import downloads
from qutebrowser.misc import objects
-gm_manager = typing.cast('GreasemonkeyManager', None)
+gm_manager = cast('GreasemonkeyManager', None)
def _scripts_dir():
@@ -54,10 +54,10 @@ class GreasemonkeyScript:
def __init__(self, properties, code, # noqa: C901 pragma: no mccabe
filename=None):
self._code = code
- self.includes = [] # type: typing.Sequence[str]
- self.matches = [] # type: typing.Sequence[str]
- self.excludes = [] # type: typing.Sequence[str]
- self.requires = [] # type: typing.Sequence[str]
+ self.includes: Sequence[str] = []
+ self.matches: Sequence[str] = []
+ self.excludes: Sequence[str] = []
+ self.requires: Sequence[str] = []
self.description = None
self.namespace = None
self.run_at = None
@@ -259,11 +259,10 @@ class GreasemonkeyManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
- self._run_start = [] # type: typing.List[GreasemonkeyScript]
- self._run_end = [] # type: typing.List[GreasemonkeyScript]
- self._run_idle = [] # type: typing.List[GreasemonkeyScript]
- self._in_progress_dls = [
- ] # type: typing.List[downloads.AbstractDownloadItem]
+ self._run_start: List[GreasemonkeyScript] = []
+ self._run_end: List[GreasemonkeyScript] = []
+ self._run_idle: List[GreasemonkeyScript] = []
+ self._in_progress_dls: List[downloads.AbstractDownloadItem] = []
self.load_scripts()
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index daf38a755..f914f3085 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -20,16 +20,17 @@
"""A HintManager to draw hints over links."""
import collections
-import typing
import functools
import os
import re
import html
import enum
from string import ascii_lowercase
+from typing import (TYPE_CHECKING, Callable, Dict, Iterable, Iterator, List, Mapping,
+ MutableSequence, Optional, Sequence, Set)
import attr
-from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, configexc
@@ -38,14 +39,30 @@ from qutebrowser.browser import webelem, history
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:
+if TYPE_CHECKING:
from qutebrowser.browser import browsertab
-Target = enum.Enum('Target', ['normal', 'current', 'tab', 'tab_fg', 'tab_bg',
- 'window', 'yank', 'yank_primary', 'run', 'fill',
- 'hover', 'download', 'userscript', 'spawn',
- 'delete', 'right_click'])
+class Target(enum.Enum):
+
+ """What action to take on a hint."""
+
+ normal = enum.auto()
+ current = enum.auto()
+ tab = enum.auto()
+ tab_fg = enum.auto()
+ tab_bg = enum.auto()
+ window = enum.auto()
+ yank = enum.auto()
+ yank_primary = enum.auto()
+ run = enum.auto()
+ fill = enum.auto()
+ hover = enum.auto()
+ download = enum.auto()
+ userscript = enum.auto()
+ spawn = enum.auto()
+ delete = enum.auto()
+ right_click = enum.auto()
class HintingError(Exception):
@@ -164,22 +181,22 @@ class HintContext:
group: The group of web elements to hint.
"""
- 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: str) -> typing.Sequence[str]:
+ all_labels: List[HintLabel] = attr.ib(attr.Factory(list))
+ labels: Dict[str, HintLabel] = attr.ib(attr.Factory(dict))
+ target: Target = attr.ib(None)
+ baseurl: QUrl = attr.ib(None)
+ to_follow: str = attr.ib(None)
+ rapid: bool = attr.ib(False)
+ first_run: bool = attr.ib(True)
+ add_history: bool = attr.ib(False)
+ filterstr: str = attr.ib(None)
+ args: List[str] = attr.ib(attr.Factory(list))
+ tab: 'browsertab.AbstractTab' = attr.ib(None)
+ group: str = attr.ib(None)
+ hint_mode: str = attr.ib(None)
+ first: bool = attr.ib(False)
+
+ def get_args(self, urlstr: str) -> Sequence[str]:
"""Get the arguments, with {hint-url} replaced by the given URL."""
args = []
for arg in self.args:
@@ -336,8 +353,8 @@ class HintActions:
commandrunner.run_safely('spawn ' + ' '.join(args))
-_ElemsType = typing.Sequence[webelem.AbstractWebElement]
-_HintStringsType = typing.MutableSequence[str]
+_ElemsType = Sequence[webelem.AbstractWebElement]
+_HintStringsType = MutableSequence[str]
class HintManager(QObject):
@@ -353,7 +370,7 @@ class HintManager(QObject):
_tab_id: The tab ID this HintManager is associated with.
Signals:
- See HintActions
+ set_text: Request for the statusbar to change its text.
"""
HINT_TEXTS = {
@@ -375,11 +392,13 @@ class HintManager(QObject):
Target.delete: "Delete an element",
}
+ set_text = pyqtSignal(str)
+
def __init__(self, win_id: int, parent: QObject = None) -> None:
"""Constructor."""
super().__init__(parent)
self._win_id = win_id
- self._context = None # type: typing.Optional[HintContext]
+ self._context: Optional[HintContext] = None
self._word_hinter = WordHinter()
self._actions = HintActions(win_id)
@@ -402,10 +421,8 @@ class HintManager(QObject):
for label in self._context.all_labels:
label.cleanup()
- text = self._get_text()
- message_bridge = objreg.get('message-bridge', scope='window',
- window=self._win_id)
- message_bridge.maybe_reset_text(text)
+ self.set_text.emit('')
+
self._context = None
def _hint_strings(self, elems: _ElemsType) -> _HintStringsType:
@@ -511,12 +528,10 @@ class HintManager(QObject):
Return:
A list of shuffled hint strings.
"""
- buckets = [
- [] for i in range(length)
- ] # type: typing.Sequence[_HintStringsType]
+ buckets: Sequence[_HintStringsType] = [[] for i in range(length)]
for i, hint in enumerate(hints):
buckets[i % len(buckets)].append(hint)
- result = [] # type: _HintStringsType
+ result: _HintStringsType = []
for bucket in buckets:
result += bucket
return result
@@ -541,7 +556,7 @@ class HintManager(QObject):
A hint string.
"""
base = len(chars)
- hintstr = [] # type: typing.MutableSequence[str]
+ hintstr: MutableSequence[str] = []
remainder = 0
while True:
remainder = number % base
@@ -636,9 +651,7 @@ class HintManager(QObject):
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
- message_bridge = objreg.get('message-bridge', scope='window',
- window=self._win_id)
- message_bridge.set_text(self._get_text())
+ self.set_text.emit(self._get_text())
if self._context.first:
self._fire(strings[0])
@@ -771,7 +784,7 @@ class HintManager(QObject):
error_cb=lambda err: message.error(str(err)),
only_visible=True)
- def _get_hint_mode(self, mode: typing.Optional[str]) -> str:
+ def _get_hint_mode(self, mode: Optional[str]) -> str:
"""Get the hinting mode to use based on a mode argument."""
if mode is None:
return config.val.hints.mode
@@ -783,7 +796,7 @@ class HintManager(QObject):
raise cmdutils.CommandError("Invalid mode: {}".format(e))
return mode
- def current_mode(self) -> typing.Optional[str]:
+ def current_mode(self) -> Optional[str]:
"""Return the currently active hinting mode (or None otherwise)."""
if self._context is None:
return None
@@ -794,7 +807,7 @@ class HintManager(QObject):
self,
keystr: str = "",
filterstr: str = "",
- visible: typing.Mapping[str, HintLabel] = None
+ visible: Mapping[str, HintLabel] = None
) -> None:
"""Handle the auto_follow option."""
assert self._context is not None
@@ -856,7 +869,7 @@ class HintManager(QObject):
pass
self._handle_auto_follow(keystr=keystr)
- def filter_hints(self, filterstr: typing.Optional[str]) -> None:
+ def filter_hints(self, filterstr: Optional[str]) -> None:
"""Filter displayed hints according to a text.
Args:
@@ -1027,7 +1040,7 @@ class WordHinter:
def __init__(self) -> None:
# will be initialized on first use.
- self.words = set() # type: typing.Set[str]
+ self.words: Set[str] = set()
self.dictionary = None
def ensure_initialized(self) -> None:
@@ -1059,10 +1072,10 @@ class WordHinter:
def extract_tag_words(
self, elem: webelem.AbstractWebElement
- ) -> typing.Iterator[str]:
+ ) -> Iterator[str]:
"""Extract tag words form the given element."""
- _extractor_type = typing.Callable[[webelem.AbstractWebElement], str]
- attr_extractors = {
+ _extractor_type = Callable[[webelem.AbstractWebElement], str]
+ attr_extractors: Mapping[str, _extractor_type] = {
"alt": lambda elem: elem["alt"],
"name": lambda elem: elem["name"],
"title": lambda elem: elem["title"],
@@ -1070,7 +1083,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"],
@@ -1086,8 +1099,8 @@ class WordHinter:
def tag_words_to_hints(
self,
- words: typing.Iterable[str]
- ) -> typing.Iterator[str]:
+ words: Iterable[str]
+ ) -> Iterator[str]:
"""Take words and transform them to proper hints if possible."""
for candidate in words:
if not candidate:
@@ -1098,20 +1111,20 @@ class WordHinter:
if 4 < match.end() - match.start() < 8:
yield candidate[match.start():match.end()].lower()
- def any_prefix(self, hint: str, existing: typing.Iterable[str]) -> bool:
+ def any_prefix(self, hint: str, existing: Iterable[str]) -> bool:
return any(hint.startswith(e) or e.startswith(hint) for e in existing)
def filter_prefixes(
self,
- hints: typing.Iterable[str],
- existing: typing.Iterable[str]
- ) -> typing.Iterator[str]:
+ hints: Iterable[str],
+ existing: Iterable[str]
+ ) -> 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: webelem.AbstractWebElement,
- existing: typing.Iterable[str],
- fallback: typing.Iterable[str]) -> typing.Optional[str]:
+ existing: Iterable[str],
+ fallback: Iterable[str]) -> 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)
@@ -1135,7 +1148,7 @@ class WordHinter:
"""
self.ensure_initialized()
hints = []
- used_hints = set() # type: typing.Set[str]
+ used_hints: Set[str] = set()
words = iter(self.words)
for elem in elems:
hint = self.new_hint_for(elem, used_hints, words)
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
index b7221dc15..89061cebf 100644
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -22,7 +22,7 @@
import os
import time
import contextlib
-import typing
+from typing import cast, Mapping, MutableSequence
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal
from PyQt5.QtWidgets import QProgressDialog, QApplication
@@ -35,7 +35,7 @@ from qutebrowser.misc import objects, sql
# increment to indicate that HistoryCompletion must be regenerated
_USER_VERSION = 2
-web_history = typing.cast('WebHistory', None)
+web_history = cast('WebHistory', None)
class HistoryProgress:
@@ -208,11 +208,11 @@ class WebHistory(sql.SqlTable):
return any(pattern.matches(url) for pattern in patterns)
def _rebuild_completion(self):
- data = {
+ data: Mapping[str, MutableSequence[str]] = {
'url': [],
'title': [],
'last_atime': []
- } # type: typing.Mapping[str, typing.MutableSequence[str]]
+ }
# select the latest entry for each url
q = sql.Query('SELECT url, title, max(atime) AS atime FROM History '
'WHERE NOT redirect and url NOT LIKE "qute://back%" '
diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py
index 390762ae0..d8fa1b6f0 100644
--- a/qutebrowser/browser/inspector.py
+++ b/qutebrowser/browser/inspector.py
@@ -21,8 +21,8 @@
import base64
import binascii
-import typing
import enum
+from typing import cast, Optional
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent
@@ -65,11 +65,11 @@ class Position(enum.Enum):
"""Where the inspector is shown."""
- right = 1
- left = 2
- top = 3
- bottom = 4
- window = 5
+ right = enum.auto()
+ left = enum.auto()
+ top = enum.auto()
+ bottom = enum.auto()
+ window = enum.auto()
class Error(Exception):
@@ -119,10 +119,10 @@ class AbstractWebInspector(QWidget):
win_id: int,
parent: QWidget = None) -> None:
super().__init__(parent)
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._layout = miscwidgets.WrapperLayout(self)
self._splitter = splitter
- self._position = None # type: typing.Optional[Position]
+ self._position: Optional[Position] = None
self._win_id = win_id
self._event_filter = _EventFilter(parent=self)
@@ -163,7 +163,7 @@ class AbstractWebInspector(QWidget):
modeman.enter(self._win_id, usertypes.KeyMode.insert,
reason='Inspector clicked', only_if_normal=True)
- def set_position(self, position: typing.Optional[Position]) -> None:
+ def set_position(self, position: Optional[Position]) -> None:
"""Set the position of the inspector.
If the position is None, the last known position is used.
diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py
index ddc100d14..b852ab29e 100644
--- a/qutebrowser/browser/navigate.py
+++ b/qutebrowser/browser/navigate.py
@@ -21,7 +21,7 @@
import re
import posixpath
-import typing
+from typing import Optional, Set
from PyQt5.QtCore import QUrl
@@ -97,9 +97,9 @@ def incdec(url, count, inc_or_dec):
window: Open the link in a new window.
"""
urlutils.ensure_valid(url)
- segments = (
+ segments: Optional[Set[str]] = (
set(config.val.url.incdec_segments)
- ) # type: typing.Optional[typing.Set[str]]
+ )
if segments is None:
segments = {'path', 'query'}
diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py
index 6ae01c7d8..3b5686a03 100644
--- a/qutebrowser/browser/network/pac.py
+++ b/qutebrowser/browser/network/pac.py
@@ -21,7 +21,7 @@
import sys
import functools
-import typing
+from typing import Optional
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo,
@@ -66,7 +66,7 @@ def _js_slot(*args):
return self._error_con.callAsConstructor([e])
# pylint: enable=protected-access
- deco = pyqtSlot(*args, result=QJSValue) # type: ignore[arg-type]
+ deco = pyqtSlot(*args, result=QJSValue)
return deco(new_method)
return _decorator
@@ -251,8 +251,7 @@ class PACFetcher(QObject):
url.setScheme(url.scheme()[len(pac_prefix):])
self._pac_url = url
- self._manager = QNetworkAccessManager(
- ) # type: typing.Optional[QNetworkAccessManager]
+ self._manager: Optional[QNetworkAccessManager] = QNetworkAccessManager()
self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
self._pac = None
self._error_message = None
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index 0bafeeaf9..44e6c45d5 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -23,7 +23,7 @@ import io
import os.path
import shutil
import functools
-import typing
+from typing import Dict, IO, Optional
import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl
@@ -92,8 +92,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
reply: The QNetworkReply to download.
"""
super().__init__(manager=manager, parent=manager)
- self.fileobj = None # type: typing.Optional[typing.IO[bytes]]
- self.raw_headers = {} # type: typing.Dict[bytes, bytes]
+ self.fileobj: Optional[IO[bytes]] = None
+ self.raw_headers: Dict[bytes, bytes] = {}
self._autoclose = True
self._retry_info = None
diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py
index b661f533d..d36bb746a 100644
--- a/qutebrowser/browser/qutescheme.py
+++ b/qutebrowser/browser/qutescheme.py
@@ -31,15 +31,8 @@ import time
import textwrap
import urllib
import collections
-import base64
-import typing
-from typing import TypeVar, Callable, Union, Tuple
-
-try:
- import secrets
-except ImportError:
- # New in Python 3.6
- secrets = None # type: ignore[assignment]
+import secrets
+from typing import TypeVar, Callable, Dict, List, Optional, Union, Sequence, Tuple
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
@@ -112,7 +105,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
def __init__(self, name):
self._name = name
- self._function = None # type: typing.Optional[typing.Callable]
+ self._function: Optional[Callable] = None
def __call__(self, function: _Handler) -> _Handler:
self._function = function
@@ -125,7 +118,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
return self._function(*args, **kwargs)
-def data_for_url(url: QUrl) -> typing.Tuple[str, bytes]:
+def data_for_url(url: QUrl) -> Tuple[str, bytes]:
"""Get the data to show for the given URL.
Args:
@@ -199,8 +192,7 @@ def qute_bookmarks(_url: QUrl) -> _HandlerRet:
@add_handler('tabs')
def qute_tabs(_url: QUrl) -> _HandlerRet:
"""Handler for qute://tabs. Display information about all open tabs."""
- tabs = collections.defaultdict(
- list) # type: typing.Dict[str, typing.List[typing.Tuple[str, str]]]
+ tabs: Dict[str, List[Tuple[str, str]]] = collections.defaultdict(list)
for win_id, window in objreg.window_registry.items():
if sip.isdeleted(window):
continue
@@ -221,7 +213,7 @@ def qute_tabs(_url: QUrl) -> _HandlerRet:
def history_data(
start_time: float,
offset: int = None
-) -> typing.Sequence[typing.Dict[str, typing.Union[str, int]]]:
+) -> Sequence[Dict[str, Union[str, int]]]:
"""Return history data.
Arguments:
@@ -355,7 +347,7 @@ def qute_gpl(_url: QUrl) -> _HandlerRet:
return 'text/html', utils.read_file('html/license.html')
-def _asciidoc_fallback_path(html_path: str) -> typing.Optional[str]:
+def _asciidoc_fallback_path(html_path: str) -> Optional[str]:
"""Fall back to plaintext asciidoc if the HTML is unavailable."""
path = html_path.replace('.html', '.asciidoc')
try:
@@ -449,12 +441,7 @@ def qute_settings(url: QUrl) -> _HandlerRet:
# Requests to qute://settings/set should only be allowed from
# qute://settings. As an additional security precaution, we generate a CSRF
# token to use here.
- if secrets:
- csrf_token = secrets.token_urlsafe()
- else:
- # On Python < 3.6, from secrets.py
- token = base64.urlsafe_b64encode(os.urandom(32))
- csrf_token = token.rstrip(b'=').decode('ascii')
+ csrf_token = secrets.token_urlsafe()
src = jinja.render('settings.html', title='settings',
configdata=configdata,
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 715487def..fb213ed62 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -22,7 +22,7 @@
import os
import html
import netrc
-import typing
+from typing import Callable, Mapping
from PyQt5.QtCore import QUrl
@@ -134,13 +134,13 @@ def javascript_alert(url, js_msg, abort_on, *, escape_msg=True):
# Needs to line up with the values allowed for the
# content.javascript.log setting.
-_JS_LOGMAP = {
+_JS_LOGMAP: Mapping[str, Callable[[str], None]] = {
'none': lambda arg: None,
'debug': log.js.debug,
'info': log.js.info,
'warning': log.js.warning,
'error': log.js.error,
-} # type: typing.Mapping[str, typing.Callable[[str], None]]
+}
def javascript_log_message(level, source, line, msg):
diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py
index b70deb165..348a7a2ff 100644
--- a/qutebrowser/browser/signalfilter.py
+++ b/qutebrowser/browser/signalfilter.py
@@ -50,7 +50,7 @@ class SignalFilter(QObject):
"""Factory for partial _filter_signals functions.
Args:
- signal: The pyqtSignal to filter.
+ signal: The pyqtBoundSignal to filter.
tab: The WebView to create filters for.
Return:
diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py
index 18fd15771..46d9d450d 100644
--- a/qutebrowser/browser/urlmarks.py
+++ b/qutebrowser/browser/urlmarks.py
@@ -30,7 +30,7 @@ import os.path
import html
import functools
import collections
-import typing
+from typing import MutableMapping
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
@@ -78,8 +78,7 @@ class UrlMarkManager(QObject):
"""Initialize and read quickmarks."""
super().__init__(parent)
- self.marks = collections.OrderedDict(
- ) # type: typing.MutableMapping[str, str]
+ self.marks: MutableMapping[str, str] = collections.OrderedDict()
self._init_lineparser()
for line in self._lineparser:
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index e79b5145e..7a888daeb 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -19,7 +19,7 @@
"""Generic web element related code."""
-import typing
+from typing import cast, TYPE_CHECKING, Iterator, Optional, Set, Union
import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint
@@ -29,11 +29,11 @@ from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser import browsertab
-JsValueType = typing.Union[int, float, str, None]
+JsValueType = Union[int, float, str, None]
class Error(Exception):
@@ -80,7 +80,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
def __delitem__(self, key: str) -> None:
raise NotImplementedError
- def __iter__(self) -> typing.Iterator[str]:
+ def __iter__(self) -> Iterator[str]:
raise NotImplementedError
def __len__(self) -> int:
@@ -88,8 +88,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
def __repr__(self) -> str:
try:
- html = utils.compact_text(
- self.outer_xml(), 500) # type: typing.Optional[str]
+ html: Optional[str] = utils.compact_text(self.outer_xml(), 500)
except Error:
html = None
return utils.get_repr(self, html=html)
@@ -102,7 +101,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Get the geometry for this element."""
raise NotImplementedError
- def classes(self) -> typing.Set[str]:
+ def classes(self) -> Set[str]:
"""Get a set of classes assigned to this element."""
raise NotImplementedError
@@ -282,7 +281,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Remove target from link."""
raise NotImplementedError
- def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]:
+ def resolve_url(self, baseurl: QUrl) -> Optional[QUrl]:
"""Resolve the URL in the element's src/href attribute.
Args:
@@ -357,16 +356,12 @@ class AbstractWebElement(collections.abc.MutableMapping):
else:
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
- modifiers = typing.cast(Qt.KeyboardModifiers,
- target_modifiers[click_target])
+ modifiers = cast(Qt.KeyboardModifiers, target_modifiers[click_target])
events = [
- QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
- Qt.NoModifier),
- QMouseEvent(QEvent.MouseButtonPress, pos, button, button,
- modifiers),
- QMouseEvent(QEvent.MouseButtonRelease, pos, button, Qt.NoButton,
- modifiers),
+ QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, Qt.NoModifier),
+ QMouseEvent(QEvent.MouseButtonPress, pos, button, button, modifiers),
+ QMouseEvent(QEvent.MouseButtonRelease, pos, button, Qt.NoButton, modifiers),
]
for evt in events:
diff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py
new file mode 100644
index 000000000..bf0319702
--- /dev/null
+++ b/qutebrowser/browser/webengine/darkmode.py
@@ -0,0 +1,307 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+
+"""Get darkmode arguments to pass to Qt.
+
+Overview of blink setting names based on the Qt version:
+
+Qt 5.10
+-------
+
+First implementation, called "high contrast mode".
+
+- highContrastMode (kOff/kSimpleInvertForTesting/kInvertBrightness/kInvertLightness)
+- highContrastGrayscale (bool)
+- highContrastContrast (float)
+- highContractImagePolicy (kFilterAll/kFilterNone)
+
+Qt 5.11, 5.12, 5.13
+-------------------
+
+New "smart" image policy.
+
+- Mode/Grayscale/Contrast as above
+- highContractImagePolicy (kFilterAll/kFilterNone/kFilterSmart [new!])
+
+Qt 5.14
+-------
+
+Renamed to "darkMode".
+
+- darkMode (kOff/kSimpleInvertForTesting/kInvertBrightness/kInvertLightness/
+ kInvertLightnessLAB [new!])
+- darkModeGrayscale (bool)
+- darkModeContrast (float)
+- darkModeImagePolicy (kFilterAll/kFilterNone/kFilterSmart)
+- darkModePagePolicy (kFilterAll/kFilterByBackground) [new!]
+- darkModeTextBrightnessThreshold (int) [new!]
+- darkModeBackgroundBrightnessThreshold (int) [new!]
+- darkModeImageGrayscale (float) [new!]
+
+Qt 5.15.0 and 5.15.1
+--------------------
+
+"darkMode" split into "darkModeEnabled" and "darkModeInversionAlgorithm".
+
+- darkModeEnabled (bool) [new!]
+- darkModeInversionAlgorithm (kSimpleInvertForTesting/kInvertBrightness/
+ kInvertLightness/kInvertLightnessLAB)
+- Rest (except darkMode) as above.
+- NOTE: smart image policy is broken with Qt 5.15.0!
+
+Qt 5.15.2
+---------
+
+Prefix changed to "forceDarkMode".
+
+- As with Qt 5.15.0 / .1, but with "forceDarkMode" as prefix.
+"""
+
+import enum
+from typing import Any, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union
+
+try:
+ from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION
+except ImportError: # pragma: no cover
+ # Added in PyQt 5.13
+ PYQT_WEBENGINE_VERSION = None # type: ignore[assignment]
+
+from qutebrowser.config import config
+from qutebrowser.utils import usertypes, qtutils, utils, log
+
+
+class Variant(enum.Enum):
+
+ """A dark mode variant."""
+
+ unavailable = enum.auto()
+ qt_510 = enum.auto()
+ qt_511_to_513 = enum.auto()
+ qt_514 = enum.auto()
+ qt_515_0 = enum.auto()
+ qt_515_1 = enum.auto()
+ qt_515_2 = enum.auto()
+
+
+# Mapping from a colors.webpage.darkmode.algorithm setting value to
+# Chromium's DarkModeInversionAlgorithm enum values.
+_ALGORITHMS = {
+ # 0: kOff (not exposed)
+ # 1: kSimpleInvertForTesting (not exposed)
+ 'brightness-rgb': 2, # kInvertBrightness
+ 'lightness-hsl': 3, # kInvertLightness
+ 'lightness-cielab': 4, # kInvertLightnessLAB
+}
+# kInvertLightnessLAB is not available with Qt < 5.14
+_ALGORITHMS_BEFORE_QT_514 = _ALGORITHMS.copy()
+_ALGORITHMS_BEFORE_QT_514['lightness-cielab'] = _ALGORITHMS['lightness-hsl']
+
+# Mapping from a colors.webpage.darkmode.policy.images setting value to
+# Chromium's DarkModeImagePolicy enum values.
+_IMAGE_POLICIES = {
+ 'always': 0, # kFilterAll
+ 'never': 1, # kFilterNone
+ 'smart': 2, # kFilterSmart
+}
+# Image policy smart is not available with Qt 5.10
+_IMAGE_POLICIES_QT_510 = _IMAGE_POLICIES.copy()
+_IMAGE_POLICIES_QT_510['smart'] = _IMAGE_POLICIES['never']
+
+# Mapping from a colors.webpage.darkmode.policy.page setting value to
+# Chromium's DarkModePagePolicy enum values.
+_PAGE_POLICIES = {
+ 'always': 0, # kFilterAll
+ 'smart': 1, # kFilterByBackground
+}
+
+_BOOLS = {
+ True: 'true',
+ False: 'false',
+}
+
+_DarkModeSettingsType = Iterable[
+ Tuple[
+ str, # qutebrowser option name
+ str, # darkmode setting name
+ # Mapping from the config value to a string (or something convertable
+ # to a string) which gets passed to Chromium.
+ Optional[Mapping[Any, Union[str, int]]],
+ ],
+]
+
+_DarkModeDefinitionType = Tuple[_DarkModeSettingsType, Set[str]]
+
+_QT_514_SETTINGS = [
+ ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'darkModeContrast', None),
+ ('grayscale.all', 'darkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'darkModeTextBrightnessThreshold', None),
+ ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None),
+ ('grayscale.images', 'darkModeImageGrayscale', None),
+]
+
+# Our defaults for policy.images are different from Chromium's, so we mark it as
+# mandatory setting - except on Qt 5.15.0 where we don't, so we don't get the
+# workaround warning below if the setting wasn't explicitly customized.
+
+_DARK_MODE_DEFINITIONS: Mapping[Variant, _DarkModeDefinitionType] = {
+ Variant.unavailable: ([], set()),
+
+ Variant.qt_515_2: ([
+ # 'darkMode' renamed to 'forceDarkMode'
+ ('enabled', 'forceDarkModeEnabled', _BOOLS),
+ ('algorithm', 'forceDarkModeInversionAlgorithm', _ALGORITHMS),
+
+ ('policy.images', 'forceDarkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'forceDarkModeContrast', None),
+ ('grayscale.all', 'forceDarkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'forceDarkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'forceDarkModeTextBrightnessThreshold', None),
+ (
+ 'threshold.background',
+ 'forceDarkModeBackgroundBrightnessThreshold',
+ None
+ ),
+ ('grayscale.images', 'forceDarkModeImageGrayscale', None),
+ ], {'enabled', 'policy.images'}),
+
+ Variant.qt_515_1: ([
+ # 'policy.images' mandatory again
+ ('enabled', 'darkModeEnabled', _BOOLS),
+ ('algorithm', 'darkModeInversionAlgorithm', _ALGORITHMS),
+
+ ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'darkModeContrast', None),
+ ('grayscale.all', 'darkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'darkModeTextBrightnessThreshold', None),
+ ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None),
+ ('grayscale.images', 'darkModeImageGrayscale', None),
+ ], {'enabled', 'policy.images'}),
+
+ Variant.qt_515_0: ([
+ # 'policy.images' not mandatory because it's broken
+ ('enabled', 'darkModeEnabled', _BOOLS),
+ ('algorithm', 'darkModeInversionAlgorithm', _ALGORITHMS),
+
+ ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'darkModeContrast', None),
+ ('grayscale.all', 'darkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'darkModeTextBrightnessThreshold', None),
+ ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None),
+ ('grayscale.images', 'darkModeImageGrayscale', None),
+ ], {'enabled'}),
+
+ Variant.qt_514: ([
+ ('algorithm', 'darkMode', _ALGORITHMS), # new: kInvertLightnessLAB
+
+ ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'darkModeContrast', None),
+ ('grayscale.all', 'darkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'darkModeTextBrightnessThreshold', None),
+ ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None),
+ ('grayscale.images', 'darkModeImageGrayscale', None),
+ ], {'algorithm', 'policy.images'}),
+
+ Variant.qt_511_to_513: ([
+ ('algorithm', 'highContrastMode', _ALGORITHMS_BEFORE_QT_514),
+
+ ('policy.images', 'highContrastImagePolicy', _IMAGE_POLICIES), # new: smart
+ ('contrast', 'highContrastContrast', None),
+ ('grayscale.all', 'highContrastGrayscale', _BOOLS),
+ ], {'algorithm', 'policy.images'}),
+
+ Variant.qt_510: ([
+ ('algorithm', 'highContrastMode', _ALGORITHMS_BEFORE_QT_514),
+
+ ('policy.images', 'highContrastImagePolicy', _IMAGE_POLICIES_QT_510),
+ ('contrast', 'highContrastContrast', None),
+ ('grayscale.all', 'highContrastGrayscale', _BOOLS),
+ ], {'algorithm'}),
+}
+
+
+def _variant() -> Variant:
+ """Get the dark mode variant based on the underlying Qt version."""
+ if PYQT_WEBENGINE_VERSION is not None:
+ # Available with Qt >= 5.13
+ if PYQT_WEBENGINE_VERSION >= 0x050f02:
+ return Variant.qt_515_2
+ elif PYQT_WEBENGINE_VERSION == 0x050f01:
+ return Variant.qt_515_1
+ elif PYQT_WEBENGINE_VERSION == 0x050f00:
+ return Variant.qt_515_0
+ elif PYQT_WEBENGINE_VERSION >= 0x050e00:
+ return Variant.qt_514
+ elif PYQT_WEBENGINE_VERSION >= 0x050d00:
+ return Variant.qt_511_to_513
+ raise utils.Unreachable(hex(PYQT_WEBENGINE_VERSION))
+
+ # If we don't have PYQT_WEBENGINE_VERSION, we'll need to assume based on the Qt
+ # version.
+ assert not qtutils.version_check( # type: ignore[unreachable]
+ '5.13', compiled=False)
+
+ if qtutils.version_check('5.11', compiled=False):
+ return Variant.qt_511_to_513
+ elif qtutils.version_check('5.10', compiled=False):
+ return Variant.qt_510
+
+ return Variant.unavailable
+
+
+def settings() -> Iterator[Tuple[str, str]]:
+ """Get necessary blink settings to configure dark mode for QtWebEngine."""
+ if not config.val.colors.webpage.darkmode.enabled:
+ return
+
+ variant = _variant()
+ setting_defs, mandatory_settings = _DARK_MODE_DEFINITIONS[variant]
+
+ for setting, key, mapping in setting_defs:
+ # To avoid blowing up the commandline length, we only pass modified
+ # settings to Chromium, as our defaults line up with Chromium's.
+ # However, we always pass enabled/algorithm to make sure dark mode gets
+ # actually turned on.
+ value = config.instance.get(
+ 'colors.webpage.darkmode.' + setting,
+ fallback=setting in mandatory_settings)
+ if isinstance(value, usertypes.Unset):
+ continue
+
+ if (setting == 'policy.images' and value == 'smart' and
+ variant == Variant.qt_515_0):
+ # WORKAROUND for
+ # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211
+ log.init.warning("Ignoring colors.webpage.darkmode.policy.images = smart "
+ "because of Qt 5.15.0 bug")
+ continue
+
+ if mapping is not None:
+ value = mapping[value]
+
+ yield key, str(value)
diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py
index e524b36d2..649339c50 100644
--- a/qutebrowser/browser/webengine/webengineelem.py
+++ b/qutebrowser/browser/webengine/webengineelem.py
@@ -19,7 +19,8 @@
"""QtWebEngine specific part of the web element API."""
-import typing
+from typing import (
+ TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Set, Tuple, Union)
from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop
from PyQt5.QtGui import QMouseEvent
@@ -29,7 +30,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineSettings
from qutebrowser.utils import log, javascript, urlutils, usertypes, utils
from qutebrowser.browser import webelem
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser.webengine import webenginetab
@@ -37,11 +38,11 @@ class WebEngineElement(webelem.AbstractWebElement):
"""A web element for QtWebEngine, using JS under the hood."""
- def __init__(self, js_dict: typing.Dict[str, typing.Any],
+ def __init__(self, js_dict: Dict[str, Any],
tab: 'webenginetab.WebEngineTab') -> None:
super().__init__(tab)
# Do some sanity checks on the data we get from JS
- js_dict_types = {
+ js_dict_types: Dict[str, Union[type, Tuple[type, ...]]] = {
'id': int,
'text': str,
'value': (str, int, float),
@@ -52,7 +53,7 @@ class WebEngineElement(webelem.AbstractWebElement):
'attributes': dict,
'is_content_editable': bool,
'caret_position': (int, type(None)),
- } # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]]
+ }
assert set(js_dict.keys()).issubset(js_dict_types.keys())
for name, typ in js_dict_types.items():
if name in js_dict and not isinstance(js_dict[name], typ):
@@ -97,14 +98,14 @@ class WebEngineElement(webelem.AbstractWebElement):
utils.unused(key)
log.stub()
- def __iter__(self) -> typing.Iterator[str]:
+ def __iter__(self) -> Iterator[str]:
return iter(self._js_dict['attributes'])
def __len__(self) -> int:
return len(self._js_dict['attributes'])
def _js_call(self, name: str, *args: webelem.JsValueType,
- callback: typing.Callable[[typing.Any], None] = None) -> None:
+ callback: Callable[[Any], None] = None) -> None:
"""Wrapper to run stuff from webelem.js."""
if self._tab.is_deleted():
raise webelem.OrphanedError("Tab containing element vanished")
@@ -118,7 +119,7 @@ class WebEngineElement(webelem.AbstractWebElement):
log.stub()
return QRect()
- def classes(self) -> typing.Set[str]:
+ def classes(self) -> Set[str]:
"""Get a list of classes assigned to this element."""
return set(self._js_dict['class_name'].split())
@@ -150,7 +151,7 @@ class WebEngineElement(webelem.AbstractWebElement):
composed: bool = False) -> None:
self._js_call('dispatch_event', event, bubbles, cancelable, composed)
- def caret_position(self) -> typing.Optional[int]:
+ def caret_position(self) -> Optional[int]:
"""Get the text caret position for the current element.
If the element is not a text element, None is returned.
@@ -256,7 +257,7 @@ class WebEngineElement(webelem.AbstractWebElement):
QEventLoop.ExcludeSocketNotifiers |
QEventLoop.ExcludeUserInputEvents)
- def reset_setting(_arg: typing.Any) -> None:
+ def reset_setting(_arg: Any) -> None:
"""Set the JavascriptCanOpenWindows setting to its old value."""
assert view is not None
try:
diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py
index f84415c65..afe0d2b48 100644
--- a/qutebrowser/browser/webengine/webengineinspector.py
+++ b/qutebrowser/browser/webengine/webengineinspector.py
@@ -20,7 +20,6 @@
"""Customized QWebInspector for QtWebEngine."""
import os
-import typing
import pathlib
from PyQt5.QtCore import QUrl, QLibraryInfo
@@ -118,7 +117,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
pak = data_path / 'resources' / 'qtwebengine_devtools_resources.pak'
if not pak.exists():
raise inspector.Error("QtWebEngine devtools resources not found, "
- "please install the qt5-webengine-devtools "
+ "please install the qt5-qtwebengine-devtools "
"Fedora package.")
def inspect(self, page: QWebEnginePage) -> None: # type: ignore[override]
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index 336540ba0..d93f72f56 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -26,7 +26,7 @@ Module attributes:
import os
import operator
-import typing
+from typing import cast, Any, List, Optional, Tuple, Union
from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
@@ -39,11 +39,11 @@ from qutebrowser.utils import (utils, standarddir, qtutils, message, log,
urlmatch, usertypes)
# The default QWebEngineProfile
-default_profile = typing.cast(QWebEngineProfile, None)
+default_profile = cast(QWebEngineProfile, None)
# The QWebEngineProfile used for private (off-the-record) windows
-private_profile = None # type: typing.Optional[QWebEngineProfile]
+private_profile: Optional[QWebEngineProfile] = None
# The global WebEngineSettings object
-global_settings = typing.cast('WebEngineSettings', None)
+global_settings = cast('WebEngineSettings', None)
parsed_user_agent = None
@@ -183,7 +183,7 @@ class WebEngineSettings(websettings.AbstractSettings):
}
def set_unknown_url_scheme_policy(
- self, policy: typing.Union[str, usertypes.Unset]) -> bool:
+ self, policy: Union[str, usertypes.Unset]) -> bool:
"""Set the UnknownUrlSchemePolicy to use.
Return:
@@ -448,10 +448,10 @@ def _init_site_specific_quirks():
def _init_devtools_settings():
"""Make sure the devtools always get images/JS permissions."""
- settings = [
+ settings: List[Tuple[str, Any]] = [
('content.javascript.enabled', True),
('content.images', True)
- ] # type: typing.List[typing.Tuple[str, typing.Any]]
+ ]
if qtutils.version_check('5.11'):
settings.append(('content.cookies.accept', 'all'))
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index a139f3d2f..f105bf2f4 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -23,13 +23,13 @@ import math
import functools
import re
import html as html_utils
-import typing
+from typing import cast, Optional, Union
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
QTimer, QObject)
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication, QWidget
-from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
+from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript, QWebEngineHistory
from qutebrowser.config import configdata, config
from qutebrowser.browser import (browsertab, eventfilter, shared, webelem,
@@ -41,7 +41,6 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
from qutebrowser.misc import miscwidgets, objects, quitter
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, objreg, jinja, debug)
-from qutebrowser.keyinput import modeman
from qutebrowser.qt import sip
@@ -356,12 +355,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
"""QtWebEngine implementations related to moving the cursor/selection."""
- def __init__(self,
- tab: 'WebEngineTab',
- mode_manager: modeman.ModeManager,
- parent: QWidget = None) -> None:
- super().__init__(mode_manager, parent)
- self._tab = tab
+ _tab: 'WebEngineTab'
def _flags(self):
"""Get flags to pass to JS."""
@@ -674,6 +668,10 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""History-related methods which are not part of the extension API."""
+ def __init__(self, tab: 'WebEngineTab') -> None:
+ self._tab = tab
+ self._history = cast(QWebEngineHistory, None)
+
def serialize(self):
if not qtutils.version_check('5.9', compiled=False):
# WORKAROUND for
@@ -782,9 +780,7 @@ class WebEngineElements(browsertab.AbstractElements):
"""QtWebEngine implemementations related to elements on the page."""
- def __init__(self, tab: 'WebEngineTab') -> None:
- super().__init__()
- self._tab = tab
+ _tab: 'WebEngineTab'
def _js_cb_multiple(self, callback, error_cb, js_elems):
"""Handle found elements coming from JS and call the real callback.
@@ -931,7 +927,7 @@ class _WebEnginePermissions(QObject):
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
try:
self._options.update({
@@ -1087,7 +1083,7 @@ class _WebEngineScripts(QObject):
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._greasemonkey = greasemonkey.gm_manager
def connect_signals(self):
@@ -1380,7 +1376,7 @@ class WebEngineTab(browsertab.AbstractTab):
self.backend = usertypes.Backend.QtWebEngine
self._child_event_filter = None
self._saved_zoom = None
- self._reload_url = None # type: typing.Optional[QUrl]
+ self._reload_url: Optional[QUrl] = None
self._scripts.init()
def _set_widget(self, widget):
@@ -1447,9 +1443,9 @@ class WebEngineTab(browsertab.AbstractTab):
self._widget.page().toHtml(callback)
def run_js_async(self, code, callback=None, *, world=None):
- world_id_type = typing.Union[QWebEngineScript.ScriptWorldId, int]
+ world_id_type = Union[QWebEngineScript.ScriptWorldId, int]
if world is None:
- world_id = QWebEngineScript.ApplicationWorld # type: world_id_type
+ world_id: world_id_type = QWebEngineScript.ApplicationWorld
elif isinstance(world, int):
world_id = world
if not 0 <= world_id <= qtutils.MAX_WORLD_ID:
@@ -1545,9 +1541,7 @@ class WebEngineTab(browsertab.AbstractTab):
authenticator.setPassword(answer.password)
else:
try:
- sip.assign( # type: ignore[attr-defined]
- authenticator,
- QAuthenticator())
+ sip.assign(authenticator, QAuthenticator())
except AttributeError:
self._show_error_page(url, "Proxy authentication required")
@@ -1568,8 +1562,7 @@ class WebEngineTab(browsertab.AbstractTab):
if not netrc_success and answer is None:
log.network.debug("Aborting auth")
try:
- sip.assign( # type: ignore[attr-defined]
- authenticator, QAuthenticator())
+ sip.assign(authenticator, QAuthenticator())
except AttributeError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
@@ -1585,6 +1578,11 @@ class WebEngineTab(browsertab.AbstractTab):
super()._on_load_started()
self.data.netrc_used = False
+ @pyqtSlot('qint64')
+ def _on_renderer_process_pid_changed(self, pid):
+ log.webview.debug("Renderer process PID for tab {}: {}"
+ .format(self.tab_id, pid))
+
@pyqtSlot(QWebEnginePage.RenderProcessTerminationStatus, int)
def _on_render_process_terminated(self, status, exitcode):
"""Show an error when the renderer process terminated."""
@@ -1857,11 +1855,15 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished)
+ try:
+ page.renderProcessPidChanged.connect(self._on_renderer_process_pid_changed)
+ except AttributeError:
+ # Added in Qt 5.15.0
+ pass
+
self.before_load_started.connect(self._on_before_load_started)
- self.shutting_down.connect(
- self.abort_questions) # type: ignore[arg-type]
- self.load_started.connect(
- self.abort_questions) # type: ignore[arg-type]
+ self.shutting_down.connect(self.abort_questions)
+ self.load_started.connect(self.abort_questions)
# pylint: disable=protected-access
self.audio._connect_signals()
diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py
index 40ac12f11..934fe2dee 100644
--- a/qutebrowser/browser/webengine/webview.py
+++ b/qutebrowser/browser/webengine/webview.py
@@ -19,7 +19,7 @@
"""The main browser widget for QtWebEngine."""
-import typing
+from typing import Optional
from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION
from PyQt5.QtGui import QPalette
@@ -70,7 +70,7 @@ class WebEngineView(QWebEngineView):
The above bug got introduced in Qt 5.11.0 and fixed in 5.12.0.
"""
- proxy = self.focusProxy() # type: typing.Optional[QWidget]
+ proxy: Optional[QWidget] = self.focusProxy()
if 'lost-focusproxy' in objects.debug_flags:
proxy = None
diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py
index 7202ebd23..dd774ea5a 100644
--- a/qutebrowser/browser/webkit/cache.py
+++ b/qutebrowser/browser/webkit/cache.py
@@ -19,7 +19,7 @@
"""HTTP network cache."""
-import typing
+from typing import cast
import os.path
from PyQt5.QtNetwork import QNetworkDiskCache
@@ -28,7 +28,7 @@ from qutebrowser.config import config
from qutebrowser.utils import utils, qtutils, standarddir
-diskcache = typing.cast('DiskCache', None)
+diskcache = cast('DiskCache', None)
class DiskCache(QNetworkDiskCache):
diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py
index 4b2070f1d..9cc28cf69 100644
--- a/qutebrowser/browser/webkit/cookies.py
+++ b/qutebrowser/browser/webkit/cookies.py
@@ -19,7 +19,7 @@
"""Handling of HTTP cookies."""
-import typing
+from typing import Sequence
from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar
from PyQt5.QtCore import pyqtSignal, QDateTime
@@ -93,7 +93,7 @@ class CookieJar(RAMCookieJar):
def parse_cookies(self):
"""Parse cookies from lineparser and store them."""
- cookies = [] # type: typing.Sequence[QNetworkCookie]
+ cookies: Sequence[QNetworkCookie] = []
for line in self._lineparser:
line_cookies = QNetworkCookie.parseCookies(line)
cookies += line_cookies # type: ignore[operator]
diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py
index a045e10f2..6e2442575 100644
--- a/qutebrowser/browser/webkit/mhtml.py
+++ b/qutebrowser/browser/webkit/mhtml.py
@@ -33,7 +33,7 @@ import email.encoders
import email.mime.multipart
import email.message
import quopri
-import typing
+from typing import MutableMapping, Set, Tuple
import attr
from PyQt5.QtCore import QUrl
@@ -90,10 +90,7 @@ def _get_css_imports_cssutils(data, inline=False):
"""
try:
import cssutils
- except (ImportError, re.error):
- # Catching re.error because cssutils in earlier releases (<= 1.0) is
- # broken on Python 3.5
- # See https://bitbucket.org/cthedot/cssutils/issues/52
+ except ImportError:
return None
# We don't care about invalid CSS data, this will only litter the log
@@ -189,7 +186,7 @@ class MHTMLWriter:
self.root_content = root_content
self.content_location = content_location
self.content_type = content_type
- self._files = {} # type: typing.MutableMapping[QUrl, _File]
+ self._files: MutableMapping[QUrl, _File] = {}
def add_file(self, location, content, content_type=None,
transfer_encoding=E_QUOPRI):
@@ -244,8 +241,7 @@ class MHTMLWriter:
return msg
-_PendingDownloadType = typing.Set[
- typing.Tuple[QUrl, downloads.AbstractDownloadItem]]
+_PendingDownloadType = Set[Tuple[QUrl, downloads.AbstractDownloadItem]]
class _Downloader:
@@ -268,7 +264,7 @@ class _Downloader:
self.target = target
self.writer = None
self.loaded_urls = {tab.url()}
- self.pending_downloads = set() # type: _PendingDownloadType
+ self.pending_downloads: _PendingDownloadType = set()
self._finished_file = False
self._used = False
diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py
index 1def7ad44..14c47b1f9 100644
--- a/qutebrowser/browser/webkit/network/networkmanager.py
+++ b/qutebrowser/browser/webkit/network/networkmanager.py
@@ -21,7 +21,7 @@
import collections
import html
-import typing
+from typing import TYPE_CHECKING, Dict, MutableMapping, Optional, Sequence
import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
@@ -40,12 +40,12 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme)
from qutebrowser.misc import objects
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.mainwindow import prompt
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
-_proxy_auth_cache = {} # type: typing.Dict[ProxyId, prompt.AuthInfo]
+_proxy_auth_cache: Dict['ProxyId', 'prompt.AuthInfo'] = {}
@attr.s(frozen=True)
@@ -123,8 +123,7 @@ def init():
QSslSocket.setDefaultCiphers(good_ciphers)
-_SavedErrorsType = typing.MutableMapping[urlutils.HostTupleType,
- typing.Sequence[QSslError]]
+_SavedErrorsType = MutableMapping[urlutils.HostTupleType, Sequence[QSslError]]
class NetworkManager(QNetworkAccessManager):
@@ -173,10 +172,8 @@ class NetworkManager(QNetworkAccessManager):
self._set_cache()
self.sslErrors.connect( # type: ignore[attr-defined]
self.on_ssl_errors)
- self._rejected_ssl_errors = collections.defaultdict(
- list) # type: _SavedErrorsType
- self._accepted_ssl_errors = collections.defaultdict(
- list) # type: _SavedErrorsType
+ self._rejected_ssl_errors: _SavedErrorsType = collections.defaultdict(list)
+ self._accepted_ssl_errors: _SavedErrorsType = collections.defaultdict(list)
self.authenticationRequired.connect( # type: ignore[attr-defined]
self.on_authentication_required)
self.proxyAuthenticationRequired.connect( # type: ignore[attr-defined]
@@ -241,8 +238,8 @@ class NetworkManager(QNetworkAccessManager):
log.network.debug("Certificate errors: {!r}".format(
' / '.join(str(err) for err in errors)))
try:
- host_tpl = urlutils.host_tuple(
- reply.url()) # type: typing.Optional[urlutils.HostTupleType]
+ host_tpl: Optional[urlutils.HostTupleType] = urlutils.host_tuple(
+ reply.url())
except ValueError:
host_tpl = None
is_accepted = False
diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py
index f293edacd..f0673036c 100644
--- a/qutebrowser/browser/webkit/tabhistory.py
+++ b/qutebrowser/browser/webkit/tabhistory.py
@@ -19,7 +19,7 @@
"""Utilities related to QWebHistory."""
-import typing
+from typing import Any, List, Mapping
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
@@ -81,7 +81,7 @@ def serialize(items):
"""
data = QByteArray()
stream = QDataStream(data, QIODevice.ReadWrite)
- user_data = [] # type: typing.List[typing.Mapping[str, typing.Any]]
+ user_data: List[Mapping[str, Any]] = []
current_idx = None
diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py
index e73d6a9e8..2f3562b7f 100644
--- a/qutebrowser/browser/webkit/webkitelem.py
+++ b/qutebrowser/browser/webkit/webkitelem.py
@@ -19,7 +19,7 @@
"""QtWebKit specific part of the web element API."""
-import typing
+from typing import cast, TYPE_CHECKING, Iterator, List, Optional, Set
from PyQt5.QtCore import QRect, Qt
from PyQt5.QtWebKit import QWebElement, QWebSettings
@@ -29,7 +29,7 @@ from qutebrowser.config import config
from qutebrowser.utils import log, utils, javascript, usertypes
from qutebrowser.browser import webelem
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser.webkit import webkittab
@@ -42,6 +42,8 @@ class WebKitElement(webelem.AbstractWebElement):
"""A wrapper around a QWebElement."""
+ _tab: 'webkittab.WebKitTab'
+
def __init__(self, elem: QWebElement, tab: 'webkittab.WebKitTab') -> None:
super().__init__(tab)
if isinstance(elem, self.__class__):
@@ -80,7 +82,7 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished()
return self._elem.hasAttribute(key)
- def __iter__(self) -> typing.Iterator[str]:
+ def __iter__(self) -> Iterator[str]:
self._check_vanished()
yield from self._elem.attributeNames()
@@ -101,7 +103,7 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished()
return self._elem.geometry()
- def classes(self) -> typing.Set[str]:
+ def classes(self) -> Set[str]:
self._check_vanished()
return set(self._elem.classes())
@@ -174,21 +176,16 @@ class WebKitElement(webelem.AbstractWebElement):
this.dispatchEvent(event);
""".format(javascript.to_js(text)))
- def _parent(self) -> typing.Optional['WebKitElement']:
+ def _parent(self) -> Optional['WebKitElement']:
"""Get the parent element of this element."""
self._check_vanished()
- elem = typing.cast(typing.Optional[QWebElement],
- self._elem.parent())
+ elem = cast(Optional[QWebElement], self._elem.parent())
if elem is None or elem.isNull():
return None
- if typing.TYPE_CHECKING:
- # pylint: disable=used-before-assignment
- assert isinstance(self._tab, webkittab.WebKitTab)
-
return WebKitElement(elem, tab=self._tab)
- def _rect_on_view_js(self) -> typing.Optional[QRect]:
+ def _rect_on_view_js(self) -> Optional[QRect]:
"""Javascript implementation for rect_on_view."""
# FIXME:qtwebengine maybe we can reuse this?
rects = self._elem.evaluateJavaScript("this.getClientRects()")
@@ -217,29 +214,32 @@ class WebKitElement(webelem.AbstractWebElement):
height *= zoom
rect = QRect(int(rect["left"]), int(rect["top"]),
int(width), int(height))
- frame = self._elem.webFrame()
+
+ frame = cast(Optional[QWebFrame], self._elem.webFrame())
while frame is not None:
# Translate to parent frames' position (scroll position
# is taken care of inside getClientRects)
rect.translate(frame.geometry().topLeft())
frame = frame.parentFrame()
+
return rect
return None
- def _rect_on_view_python(self,
- elem_geometry: typing.Optional[QRect]) -> QRect:
+ def _rect_on_view_python(self, elem_geometry: Optional[QRect]) -> QRect:
"""Python implementation for rect_on_view."""
if elem_geometry is None:
geometry = self._elem.geometry()
else:
geometry = elem_geometry
- frame = self._elem.webFrame()
rect = QRect(geometry)
+
+ frame = cast(Optional[QWebFrame], self._elem.webFrame())
while frame is not None:
rect.translate(frame.geometry().topLeft())
rect.translate(frame.scrollPosition() * -1)
- frame = frame.parentFrame()
+ frame = cast(Optional[QWebFrame], frame.parentFrame())
+
return rect
def rect_on_view(self, *, elem_geometry: QRect = None,
@@ -332,7 +332,7 @@ class WebKitElement(webelem.AbstractWebElement):
return all([visible_on_screen, visible_in_frame])
def remove_blank_target(self) -> None:
- elem = self # type: typing.Optional[WebKitElement]
+ elem: Optional[WebKitElement] = self
for _ in range(5):
if elem is None:
break
@@ -377,7 +377,7 @@ class WebKitElement(webelem.AbstractWebElement):
super()._click_fake_event(click_target)
-def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]:
+def get_child_frames(startframe: QWebFrame) -> List[QWebFrame]:
"""Get all children recursively of a given QWebFrame.
Loosely based on http://blog.nextgenetics.net/?e=64
@@ -391,7 +391,7 @@ def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]:
results = []
frames = [startframe]
while frames:
- new_frames = [] # type: typing.List[QWebFrame]
+ new_frames: List[QWebFrame] = []
for frame in frames:
results.append(frame)
new_frames += frame.childFrames()
diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py
index 0db1a738d..ec30267d4 100644
--- a/qutebrowser/browser/webkit/webkitsettings.py
+++ b/qutebrowser/browser/webkit/webkitsettings.py
@@ -24,7 +24,7 @@ Module attributes:
constants.
"""
-import typing
+from typing import cast
import os.path
from PyQt5.QtCore import QUrl
@@ -39,7 +39,7 @@ from qutebrowser.browser import shared
# The global WebKitSettings object
-global_settings = typing.cast('WebKitSettings', None)
+global_settings = cast('WebKitSettings', None)
parsed_user_agent = None
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index cad9badee..1008169a0 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -22,12 +22,13 @@
import re
import functools
import xml.etree.ElementTree
+from typing import cast, Iterable
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
-from PyQt5.QtWebKit import QWebSettings
+from PyQt5.QtWebKit import QWebSettings, QWebHistory, QWebElement
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab, shared
@@ -200,8 +201,7 @@ class WebKitCaret(browsertab.AbstractCaret):
tab: 'WebKitTab',
mode_manager: modeman.ModeManager,
parent: QWidget = None) -> None:
- super().__init__(mode_manager, parent)
- self._tab = tab
+ super().__init__(tab, mode_manager, parent)
self._selection_state = browsertab.SelectionState.none
@pyqtSlot(usertypes.KeyMode)
@@ -622,6 +622,10 @@ class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""History-related methods which are not part of the extension API."""
+ def __init__(self, tab: 'WebKitTab') -> None:
+ self._tab = tab
+ self._history = cast(QWebHistory, None)
+
def serialize(self):
return qtutils.serialize(self._history)
@@ -636,6 +640,7 @@ class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate):
qtutils.deserialize_stream(stream, self._history)
for i, data in enumerate(user_data):
self._history.itemAt(i).setUserData(data)
+
cur_data = self._history.currentItem().userData()
if cur_data is not None:
if 'zoom' in cur_data:
@@ -687,9 +692,7 @@ class WebKitElements(browsertab.AbstractElements):
"""QtWebKit implemementations related to elements on the page."""
- def __init__(self, tab: 'WebKitTab') -> None:
- super().__init__()
- self._tab = tab
+ _tab: 'WebKitTab'
def find_css(self, selector, callback, error_cb, *, only_visible=False):
utils.unused(error_cb)
@@ -700,7 +703,8 @@ class WebKitElements(browsertab.AbstractElements):
elems = []
frames = webkitelem.get_child_frames(mainframe)
for f in frames:
- for elem in f.findAllElements(selector):
+ frame_elems = cast(Iterable[QWebElement], f.findAllElements(selector))
+ for elem in frame_elems:
elems.append(webkitelem.WebKitElement(elem, tab=self._tab))
if only_visible:
diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py
index 9055bff24..956b9be9d 100644
--- a/qutebrowser/browser/webkit/webpage.py
+++ b/qutebrowser/browser/webkit/webpage.py
@@ -21,7 +21,7 @@
import html
import functools
-import typing
+from typing import cast
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices
@@ -353,11 +353,11 @@ class BrowserPage(QWebPage):
self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser)
- url = frame.url().adjusted(typing.cast(QUrl.FormattingOptions,
- QUrl.RemoveUserInfo |
- QUrl.RemovePath |
- QUrl.RemoveQuery |
- QUrl.RemoveFragment))
+ url = frame.url().adjusted(cast(QUrl.FormattingOptions,
+ QUrl.RemoveUserInfo |
+ QUrl.RemovePath |
+ QUrl.RemoveQuery |
+ QUrl.RemoveFragment))
question = shared.feature_permission(
url=url,
option=options[feature], msg=messages[feature],
diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py
index 2672fcd68..61b44d555 100644
--- a/qutebrowser/commands/command.py
+++ b/qutebrowser/commands/command.py
@@ -23,6 +23,7 @@ import inspect
import collections
import traceback
import typing
+from typing import Any, MutableMapping, MutableSequence, Tuple, Union
import attr
@@ -116,13 +117,11 @@ class Command:
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
default=argparser.SUPPRESS, nargs=0,
help=argparser.SUPPRESS)
- self.opt_args = collections.OrderedDict(
- ) # type: typing.MutableMapping[str, typing.Tuple[str, str]]
+ self.opt_args: MutableMapping[str, Tuple[str, str]] = collections.OrderedDict()
self.namespace = None
self._count = None
- self.pos_args = [
- ] # type: typing.MutableSequence[typing.Tuple[str, str]]
- self.flags_with_args = [] # type: typing.MutableSequence[str]
+ self.pos_args: MutableSequence[Tuple[str, str]] = []
+ self.flags_with_args: MutableSequence[str] = []
self._has_vararg = False
# This is checked by future @cmdutils.argument calls so they fail
@@ -406,22 +405,19 @@ class Command:
raise TypeError("{}: Legacy tuple type annotation!".format(
self.name))
- if hasattr(typing, 'UnionMeta'):
- # Python 3.5.2
- # pylint: disable=no-member,useless-suppression
- is_union = isinstance(
- typ, typing.UnionMeta) # type: ignore[attr-defined]
- else:
- is_union = getattr(typ, '__origin__', None) is typing.Union
+ try:
+ origin = typing.get_origin(typ) # type: ignore[attr-defined]
+ except AttributeError:
+ # typing.get_origin was added in Python 3.8
+ origin = getattr(typ, '__origin__', None)
- if is_union:
- # this is... slightly evil, I know
+ if origin is Union:
try:
- types = list(typ.__args__)
+ types = list(typing.get_args(typ)) # type: ignore[attr-defined]
except AttributeError:
- # Python 3.5.2
- types = list(typ.__union_params__)
- # pylint: enable=no-member,useless-suppression
+ # typing.get_args was added in Python 3.8
+ types = list(typ.__args__)
+
if param.default is not inspect.Parameter.empty:
types.append(type(param.default))
choices = self.get_arg_info(param).choices
@@ -497,8 +493,8 @@ class Command:
Return:
An (args, kwargs) tuple.
"""
- args = [] # type: typing.Any
- kwargs = {} # type: typing.MutableMapping[str, typing.Any]
+ args: Any = []
+ kwargs: MutableMapping[str, Any] = {}
signature = inspect.signature(self.handler)
for i, param in enumerate(signature.parameters.values()):
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
index 76ae1d64f..c195a8be9 100644
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -21,8 +21,8 @@
import traceback
import re
-import typing
import contextlib
+from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping
import attr
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
@@ -34,9 +34,9 @@ from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split, objects
from qutebrowser.keyinput import macros, modeman
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.mainwindow import tabbedbrowser
-_ReplacementFunction = typing.Callable[['tabbedbrowser.TabbedBrowser'], str]
+_ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str]
last_command = {}
@@ -64,9 +64,9 @@ def _url(tabbed_browser):
raise cmdutils.CommandError(msg)
-def _init_variable_replacements() -> typing.Mapping[str, _ReplacementFunction]:
+def _init_variable_replacements() -> Mapping[str, _ReplacementFunction]:
"""Return a dict from variable replacements to fns processing them."""
- replacements = {
+ replacements: Dict[str, _ReplacementFunction] = {
'url': lambda tb: _url(tb).toString(
QUrl.FullyEncoded | QUrl.RemovePassword),
'url:pretty': lambda tb: _url(tb).toString(
@@ -88,7 +88,7 @@ def _init_variable_replacements() -> typing.Mapping[str, _ReplacementFunction]:
'title': lambda tb: tb.widget.page_title(tb.widget.currentIndex()),
'clipboard': lambda _: utils.get_clipboard(),
'primary': lambda _: utils.get_clipboard(selection=True),
- } # type: typing.Dict[str, _ReplacementFunction]
+ }
for key in list(replacements):
modified_key = '{' + key + '}'
@@ -108,7 +108,7 @@ def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
- values = {} # type: typing.MutableMapping[str, str]
+ values: MutableMapping[str, str] = {}
args = []
def repl_cb(matchobj):
@@ -332,7 +332,7 @@ class CommandRunner(AbstractCommandRunner):
self._win_id = win_id
@contextlib.contextmanager
- def _handle_error(self, safely: bool) -> typing.Iterator[None]:
+ def _handle_error(self, safely: bool) -> Iterator[None]:
"""Show exceptions as errors if safely=True is given."""
try:
yield
diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py
index 485161600..ce25d7d28 100644
--- a/qutebrowser/commands/userscripts.py
+++ b/qutebrowser/commands/userscripts.py
@@ -22,7 +22,7 @@
import os
import os.path
import tempfile
-import typing
+from typing import cast, Any, MutableMapping, Tuple
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
@@ -60,7 +60,7 @@ class _QtFIFOReader(QObject):
fd = os.open(filepath, os.O_RDWR | os.O_NONBLOCK)
# pylint: enable=no-member,useless-suppression
self._fifo = os.fdopen(fd, 'r')
- self._notifier = QSocketNotifier(typing.cast(sip.voidptr, fd),
+ self._notifier = QSocketNotifier(cast(sip.voidptr, fd),
QSocketNotifier.Read, self)
self._notifier.activated.connect( # type: ignore[attr-defined]
self.read_line)
@@ -117,10 +117,10 @@ class _BaseUserscriptRunner(QObject):
self._cleaned_up = False
self._filepath = None
self._proc = None
- self._env = {} # type: typing.MutableMapping[str, str]
+ self._env: MutableMapping[str, str] = {}
self._text_stored = False
self._html_stored = False
- self._args = () # type: typing.Tuple[typing.Any, ...]
+ self._args: Tuple[Any, ...] = ()
self._kwargs = {}
def store_text(self, text):
@@ -267,7 +267,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
return
self._reader = _QtFIFOReader(self._filepath)
- self._reader.got_line.connect(self.got_cmd) # type: ignore[arg-type]
+ self._reader.got_line.connect(self.got_cmd)
@pyqtSlot()
def on_proc_finished(self):
@@ -426,7 +426,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False,
commandrunner = runners.CommandRunner(win_id, parent=tb)
if utils.is_posix:
- runner = _POSIXUserscriptRunner(tb) # type: _BaseUserscriptRunner
+ runner: _BaseUserscriptRunner = _POSIXUserscriptRunner(tb)
elif utils.is_windows: # pragma: no cover
runner = _WindowsUserscriptRunner(tb)
else: # pragma: no cover
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
index 50d5bdf62..4f51ecd4b 100644
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -23,7 +23,7 @@ Defines a CompletionView which uses CompletionFiterModel and CompletionModel
subclasses to provide completions.
"""
-import typing
+from typing import TYPE_CHECKING, Optional
from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory, QWidget
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
@@ -32,7 +32,7 @@ from qutebrowser.config import config, stylesheet
from qutebrowser.completion import completiondelegate
from qutebrowser.utils import utils, usertypes, debug, log
from qutebrowser.api import cmdutils
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.mainwindow.statusbar import command
@@ -115,7 +115,7 @@ class CompletionView(QTreeView):
win_id: int,
parent: QWidget = None) -> None:
super().__init__(parent)
- self.pattern = None # type: typing.Optional[str]
+ self.pattern: Optional[str] = None
self._win_id = win_id
self._cmd = cmd
self._active = False
diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py
index 1bd2a808f..7d65d4439 100644
--- a/qutebrowser/completion/models/completionmodel.py
+++ b/qutebrowser/completion/models/completionmodel.py
@@ -19,7 +19,7 @@
"""A model that proxies access to one or more completion categories."""
-import typing
+from typing import MutableSequence
from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel
@@ -43,8 +43,7 @@ class CompletionModel(QAbstractItemModel):
def __init__(self, *, column_widths=(30, 70, 0), parent=None):
super().__init__(parent)
self.column_widths = column_widths
- self._categories = [
- ] # type: typing.MutableSequence[QAbstractItemModel]
+ self._categories: MutableSequence[QAbstractItemModel] = []
def _cat_from_idx(self, index):
"""Return the category pointed to by the given index.
diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py
index 464caa19e..e7ccd3505 100644
--- a/qutebrowser/completion/models/histcategory.py
+++ b/qutebrowser/completion/models/histcategory.py
@@ -19,7 +19,7 @@
"""A completion category that queries the SQL history store."""
-import typing
+from typing import Optional
from PyQt5.QtSql import QSqlQueryModel
from PyQt5.QtWidgets import QWidget
@@ -40,12 +40,12 @@ class HistoryCategory(QSqlQueryModel):
"""Create a new History completion category."""
super().__init__(parent=parent)
self.name = "History"
- self._query = None # type: typing.Optional[sql.Query]
+ self._query: Optional[sql.Query] = None
# advertise that this model filters by URL and title
self.columns_to_filter = [0, 1]
self.delete_func = delete_func
- self._empty_prefix = None # type: typing.Optional[str]
+ self._empty_prefix: Optional[str] = None
def _atime_expr(self):
"""If max_items is set, return an expression to limit the query."""
diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py
index 6995071ed..f0cc21da0 100644
--- a/qutebrowser/completion/models/listcategory.py
+++ b/qutebrowser/completion/models/listcategory.py
@@ -20,7 +20,7 @@
"""Completion category that uses a list of tuples as a data source."""
import re
-import typing
+from typing import Iterable, Tuple
from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp
from PyQt5.QtGui import QStandardItem, QStandardItemModel
@@ -36,7 +36,7 @@ class ListCategory(QSortFilterProxyModel):
def __init__(self,
name: str,
- items: typing.Iterable[typing.Tuple[str, ...]],
+ items: Iterable[Tuple[str, ...]],
sort: bool = True,
delete_func: util.DeleteFuncType = None,
parent: QWidget = None):
diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py
index 9cf2d5fd6..925f95bbb 100644
--- a/qutebrowser/completion/models/miscmodels.py
+++ b/qutebrowser/completion/models/miscmodels.py
@@ -20,7 +20,7 @@
"""Functions that return miscellaneous completion models."""
import datetime
-import typing
+from typing import List, Sequence, Tuple
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, utils
@@ -53,7 +53,7 @@ def helptopic(*, info):
def quickmark(*, info=None):
"""A CompletionModel filled with all quickmarks."""
- def delete(data: typing.Sequence[str]) -> None:
+ def delete(data: Sequence[str]) -> None:
"""Delete a quickmark from the completion menu."""
name = data[0]
quickmark_manager = objreg.get('quickmark-manager')
@@ -71,7 +71,7 @@ def quickmark(*, info=None):
def bookmark(*, info=None):
"""A CompletionModel filled with all bookmarks."""
- def delete(data: typing.Sequence[str]) -> None:
+ def delete(data: Sequence[str]) -> None:
"""Delete a bookmark from the completion menu."""
urlstr = data[0]
log.completion.debug('Deleting bookmark {}'.format(urlstr))
@@ -121,7 +121,7 @@ def _buffer(*, win_id_filter=lambda _win_id: True, add_win_id=True):
tabs_are_windows = config.val.tabs.tabs_are_windows
# list storing all single-tabbed windows when tabs_are_windows
- windows = [] # type: typing.List[typing.Tuple[str, str, str]]
+ windows: List[Tuple[str, str, str]] = []
for win_id in objreg.window_registry:
if not win_id_filter(win_id):
@@ -131,7 +131,7 @@ def _buffer(*, win_id_filter=lambda _win_id: True, add_win_id=True):
window=win_id)
if tabbed_browser.is_shutting_down:
continue
- tabs = [] # type: typing.List[typing.Tuple[str, str, str]]
+ tabs: List[Tuple[str, str, str]] = []
for idx in range(tabbed_browser.widget.count()):
tab = tabbed_browser.widget.widget(idx)
tab_str = ("{}/{}".format(win_id, idx + 1) if add_win_id
diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py
index ff83a598a..ba0857d4c 100644
--- a/qutebrowser/completion/models/urlmodel.py
+++ b/qutebrowser/completion/models/urlmodel.py
@@ -19,10 +19,9 @@
"""Function to return the url completion model for the `open` command."""
-import typing
+from typing import Dict, Sequence
-if typing.TYPE_CHECKING:
- from PyQt5.QtCore import QAbstractItemModel
+from PyQt5.QtCore import QAbstractItemModel
from qutebrowser.completion.models import (completionmodel, listcategory,
histcategory)
@@ -41,14 +40,14 @@ def _delete_history(data):
history.web_history.delete_url(urlstr)
-def _delete_bookmark(data: typing.Sequence[str]) -> None:
+def _delete_bookmark(data: Sequence[str]) -> None:
urlstr = data[_URLCOL]
log.completion.debug('Deleting bookmark {}'.format(urlstr))
bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.delete(urlstr)
-def _delete_quickmark(data: typing.Sequence[str]) -> None:
+def _delete_quickmark(data: Sequence[str]) -> None:
name = data[_TEXTCOL]
quickmark_manager = objreg.get('quickmark-manager')
log.completion.debug('Deleting quickmark {}'.format(name))
@@ -77,7 +76,7 @@ def url(*, info):
if k != 'DEFAULT']
# pylint: enable=bad-config-option
categories = config.val.completion.open_categories
- models = {} # type: typing.Dict[str, QAbstractItemModel]
+ models: Dict[str, QAbstractItemModel] = {}
if searchengines and 'searchengines' in categories:
models['searchengines'] = listcategory.ListCategory(
diff --git a/qutebrowser/completion/models/util.py b/qutebrowser/completion/models/util.py
index a0dda334a..7f4f6d19d 100644
--- a/qutebrowser/completion/models/util.py
+++ b/qutebrowser/completion/models/util.py
@@ -19,13 +19,13 @@
"""Utility functions for completion models."""
-import typing
+from typing import Callable, Sequence
from qutebrowser.utils import usertypes
from qutebrowser.misc import objects
-DeleteFuncType = typing.Callable[[typing.Sequence[str]], None]
+DeleteFuncType = Callable[[Sequence[str]], None]
def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py
index e7e2c3573..d68a3feb0 100644
--- a/qutebrowser/components/adblock.py
+++ b/qutebrowser/components/adblock.py
@@ -23,8 +23,8 @@ import os.path
import posixpath
import zipfile
import logging
-import typing
import pathlib
+from typing import cast, IO, List, Set
from PyQt5.QtCore import QUrl
@@ -57,7 +57,7 @@ def _guess_zip_filename(zf: zipfile.ZipFile) -> str:
raise FileNotFoundError("No hosts file found in zip")
-def get_fileobj(byte_io: typing.IO[bytes]) -> typing.IO[bytes]:
+def get_fileobj(byte_io: IO[bytes]) -> IO[bytes]:
"""Get a usable file object to read the hosts file from."""
byte_io.seek(0) # rewind downloaded file
if zipfile.is_zipfile(byte_io):
@@ -101,8 +101,8 @@ class HostBlocker:
) -> None:
self.enabled = _should_be_used()
self._has_basedir = has_basedir
- self._blocked_hosts = set() # type: typing.Set[str]
- self._config_blocked_hosts = set() # type: typing.Set[str]
+ self._blocked_hosts: Set[str] = set()
+ self._config_blocked_hosts: Set[str] = set()
self._local_hosts_file = str(data_dir / "blocked-hosts")
self.update_files()
@@ -137,7 +137,7 @@ class HostBlocker:
)
info.block()
- def _read_hosts_line(self, raw_line: bytes) -> typing.Set[str]:
+ def _read_hosts_line(self, raw_line: bytes) -> Set[str]:
"""Read hosts from the given line.
Args:
@@ -173,7 +173,7 @@ class HostBlocker:
return filtered_hosts
- def _read_hosts_file(self, filename: str, target: typing.Set[str]) -> bool:
+ def _read_hosts_file(self, filename: str, target: Set[str]) -> bool:
"""Read hosts from the given filename.
Args:
@@ -225,7 +225,7 @@ class HostBlocker:
dl.initiate()
return dl
- def _merge_file(self, byte_io: typing.IO[bytes]) -> None:
+ def _merge_file(self, byte_io: IO[bytes]) -> None:
"""Read and merge host files.
Args:
diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py
index ff9a21070..19ad126c1 100644
--- a/qutebrowser/components/misccommands.py
+++ b/qutebrowser/components/misccommands.py
@@ -23,7 +23,7 @@ import os
import signal
import functools
import logging
-import typing
+from typing import Optional
try:
import hunter
@@ -41,7 +41,7 @@ from qutebrowser.completion.models import miscmodels
@cmdutils.register(name='reload')
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
-def reloadpage(tab: typing.Optional[apitypes.Tab],
+def reloadpage(tab: Optional[apitypes.Tab],
force: bool = False) -> None:
"""Reload the current/[count]th tab.
@@ -55,7 +55,7 @@ def reloadpage(tab: typing.Optional[apitypes.Tab],
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
-def stop(tab: typing.Optional[apitypes.Tab]) -> None:
+def stop(tab: Optional[apitypes.Tab]) -> None:
"""Stop loading in the current/[count]th tab.
Args:
@@ -97,7 +97,7 @@ def _print_pdf(tab: apitypes.Tab, filename: str) -> None:
@cmdutils.register(name='print')
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
@cmdutils.argument('pdf', flag='f', metavar='file')
-def printpage(tab: typing.Optional[apitypes.Tab],
+def printpage(tab: Optional[apitypes.Tab],
preview: bool = False, *,
pdf: str = None) -> None:
"""Print the current/[count]th tab.
@@ -163,7 +163,7 @@ def insert_text(tab: apitypes.Tab, text: str) -> None:
Args:
text: The text to insert.
"""
- def _insert_text_cb(elem: typing.Optional[apitypes.WebElement]) -> None:
+ def _insert_text_cb(elem: Optional[apitypes.WebElement]) -> None:
if elem is None:
message.error("No element focused!")
return
@@ -195,7 +195,7 @@ def click_element(tab: apitypes.Tab, filter_: str, value: str, *,
target: How to open the clicked element (normal/tab/tab-bg/window).
force_event: Force generating a fake click event.
"""
- def single_cb(elem: typing.Optional[apitypes.WebElement]) -> None:
+ def single_cb(elem: Optional[apitypes.WebElement]) -> None:
"""Click a single element."""
if elem is None:
message.error("No element found with id {}!".format(value))
@@ -236,7 +236,7 @@ def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -> None:
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
-def tab_mute(tab: typing.Optional[apitypes.Tab]) -> None:
+def tab_mute(tab: Optional[apitypes.Tab]) -> None:
"""Mute/Unmute the current/[count]th tab.
Args:
diff --git a/qutebrowser/components/readlinecommands.py b/qutebrowser/components/readlinecommands.py
index 076bb9055..ea8f12edf 100644
--- a/qutebrowser/components/readlinecommands.py
+++ b/qutebrowser/components/readlinecommands.py
@@ -19,7 +19,7 @@
"""Bridge to provide readline-like shortcuts for QLineEdits."""
-import typing
+from typing import Iterable, Optional, MutableMapping
from PyQt5.QtWidgets import QApplication, QLineEdit
@@ -35,9 +35,9 @@ class _ReadlineBridge:
"""
def __init__(self) -> None:
- self._deleted = {} # type: typing.MutableMapping[QLineEdit, str]
+ self._deleted: MutableMapping[QLineEdit, str] = {}
- def _widget(self) -> typing.Optional[QLineEdit]:
+ def _widget(self) -> Optional[QLineEdit]:
"""Get the currently active QLineEdit."""
w = QApplication.instance().focusWidget()
if isinstance(w, QLineEdit):
@@ -86,7 +86,7 @@ class _ReadlineBridge:
def kill_line(self) -> None:
self._dispatch('end', mark=True, delete=True)
- def _rubout(self, delim: typing.Iterable[str]) -> None:
+ def _rubout(self, delim: Iterable[str]) -> None:
"""Delete backwards using the characters in delim as boundaries."""
widget = self._widget()
if widget is None:
diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py
index 007b44404..8611e46ab 100644
--- a/qutebrowser/config/config.py
+++ b/qutebrowser/config/config.py
@@ -23,7 +23,7 @@ import copy
import contextlib
import functools
import typing
-from typing import Any
+from typing import Any, Tuple, MutableMapping
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
@@ -33,7 +33,6 @@ from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils
if typing.TYPE_CHECKING:
- from typing import Tuple, MutableMapping
from qutebrowser.config import configcache, configfiles
from qutebrowser.misc import savemanager
@@ -283,6 +282,7 @@ class Config(QObject):
self._init_values()
self.yaml_loaded = False
self.config_py_loaded = False
+ self.warn_autoconfig = True
def _init_values(self) -> None:
"""Populate the self._values dict."""
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index c43847fe9..460f3bc41 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -575,12 +575,20 @@ content.headers.user_agent:
- qutebrowser_version
completions:
# See https://techblog.willshouse.com/2012/01/03/most-common-user-agents/
- - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
- like Gecko) Chrome/83.0.4103.61 Safari/537.36"
- - Chrome 83 Win10
+ #
+ # To update the following list of user agents, run the script
+ # 'ua_fetch.py'
+ # Vim-protip: Place your cursor below this comment and run
+ # :r!python scripts/dev/ua_fetch.py
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
- Gecko) Chrome/83.0.4103.61 Safari/537.36"
- - Chrome 83 Linux
+ Gecko) Chrome/86.0.4240.75 Safari/537.36"
+ - Chrome 86 Linux
+ - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
+ like Gecko) Chrome/86.0.4240.75 Safari/537.36"
+ - Chrome 86 Win10
+ - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
+ (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36"
+ - Chrome 86 macOS
supports_pattern: true
desc: |
User agent to send.
@@ -1863,14 +1871,14 @@ tabs.title.format:
* `{perc}`: Percentage as a string like `[10%]`.
* `{perc_raw}`: Raw percentage, e.g. `10`.
* `{current_title}`: Title of the current web page.
- * `{title_sep}`: The string ` - ` if a title is set, empty otherwise.
+ * `{title_sep}`: The string `" - "` if a title is set, empty otherwise.
* `{index}`: Index of this tab.
* `{aligned_index}`: Index of this tab padded with spaces to have the same
width.
* `{id}`: Internal tab ID of this tab.
* `{scroll_pos}`: Page scroll position.
* `{host}`: Host of the current web page.
- * `{backend}`: Either ''webkit'' or ''webengine''
+ * `{backend}`: Either `webkit` or `webengine`
* `{private}`: Indicates when private mode is enabled.
* `{current_url}`: URL of the current web page.
* `{protocol}`: Protocol (http/https/...) of the current web page.
@@ -2740,18 +2748,21 @@ colors.webpage.darkmode.enabled:
above.
restart: true
backend:
- QtWebEngine: Qt 5.14
+ QtWebEngine: Qt 5.10
QtWebKit: false
colors.webpage.darkmode.algorithm:
default: lightness-cielab
- desc: "Which algorithm to use for modifying how colors are rendered with
- darkmode."
+ desc: >-
+ Which algorithm to use for modifying how colors are rendered with darkmode.
+
+ The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated
+ like `lightness-hsl` with older QtWebEngine versions.
type:
name: String
valid_values:
- lightness-cielab: Modify colors by converting them to CIELAB color
- space and inverting the L value.
+ space and inverting the L value. Not available with Qt < 5.14.
- lightness-hsl: Modify colors by converting them to the HSL color space
and inverting the lightness (i.e. the "L" in HSL).
- brightness-rgb: Modify colors by subtracting each of r, g, and b from
@@ -2761,7 +2772,7 @@ colors.webpage.darkmode.algorithm:
# Chromium's automated tests
restart: true
backend:
- QtWebEngine: Qt 5.14
+ QtWebEngine: Qt 5.10
QtWebKit: false
colors.webpage.darkmode.contrast:
@@ -2777,27 +2788,29 @@ colors.webpage.darkmode.contrast:
`lightness-hsl` or `brightness-rgb`.
restart: true
backend:
- QtWebEngine: Qt 5.14
+ QtWebEngine: Qt 5.10
QtWebKit: false
colors.webpage.darkmode.policy.images:
- default: never
+ default: smart
type:
name: String
valid_values:
- always: Apply dark mode filter to all images.
- never: Never apply dark mode filter to any images.
- - smart: Apply dark mode based on image content.
+ - smart: "Apply dark mode based on image content. Not available with Qt
+ 5.10 / 5.15.0."
desc: >-
Which images to apply dark mode to.
- WARNING: On Qt 5.15.0, this setting can cause frequent renderer process
+ With QtWebEngine 5.15.0, this setting can cause frequent renderer process
crashes due to a
https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug
- in Qt].
+ in Qt]. With QtWebEngine 5.10, this is not available at all. In those
+ cases, the 'smart' setting is ignored and treated like 'never'.
restart: true
backend:
- QtWebEngine: Qt 5.14
+ QtWebEngine: Qt 5.10
QtWebKit: false
colors.webpage.darkmode.policy.page:
@@ -2860,7 +2873,7 @@ colors.webpage.darkmode.grayscale.all:
`lightness-hsl` or `brightness-rgb`.
restart: true
backend:
- QtWebEngine: Qt 5.14
+ QtWebEngine: Qt 5.10
QtWebKit: false
colors.webpage.darkmode.grayscale.images:
diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py
index a1b0e75bd..ae05a2861 100644
--- a/qutebrowser/config/configfiles.py
+++ b/qutebrowser/config/configfiles.py
@@ -566,12 +566,21 @@ class ConfigAPI:
def finalize(self) -> None:
"""Do work which needs to be done after reading config.py."""
+ if self._config.warn_autoconfig:
+ desc = configexc.ConfigErrorDesc(
+ "autoconfig loading not specified",
+ ("Your config.py should call either `config.load_autoconfig()`"
+ " (to load settings configured via the GUI) or "
+ "`config.load_autoconfig(False)` (to not do so)"))
+ self.errors.append(desc)
self._config.update_mutables()
- def load_autoconfig(self) -> None:
+ def load_autoconfig(self, load_config: bool = True) -> None:
"""Load the autoconfig.yml file which is used for :set/:bind/etc."""
- with self._handle_error('reading', 'autoconfig.yml'):
- read_autoconfig()
+ self._config.warn_autoconfig = False
+ if load_config:
+ with self._handle_error('reading', 'autoconfig.yml'):
+ read_autoconfig()
def get(self, name: str, pattern: str = None) -> typing.Any:
"""Get a setting value from the config, optionally with a pattern."""
@@ -689,12 +698,12 @@ class ConfigPyWriter:
"still loaded.")
yield self._line("# Remove it to not load settings done via the "
"GUI.")
- yield self._line("config.load_autoconfig()")
+ yield self._line("config.load_autoconfig(True)")
yield ''
else:
- yield self._line("# Uncomment this to still load settings "
+ yield self._line("# Change the argument to True to still load settings "
"configured via autoconfig.yml")
- yield self._line("# config.load_autoconfig()")
+ yield self._line("config.load_autoconfig(False)")
yield ''
def _gen_options(self) -> typing.Iterator[str]:
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index 75148947e..81c47590d 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -47,7 +47,6 @@ import html
import codecs
import os.path
import itertools
-import warnings
import functools
import operator
import json
@@ -1319,30 +1318,19 @@ class Regex(BaseType):
def _compile_regex(self, pattern: str) -> typing.Pattern[str]:
"""Check if the given regex is valid.
- This is more complicated than it could be since there's a warning on
- invalid escapes with newer Python versions, and we want to catch that
- case and treat it as invalid.
+ Some semi-invalid regexes can also raise warnings - we also treat them as
+ invalid.
"""
- with warnings.catch_warnings(record=True) as recorded_warnings:
- warnings.simplefilter('always')
- try:
+ try:
+ with log.py_warning_filter('error', category=FutureWarning):
compiled = re.compile(pattern, self.flags)
- except re.error as e:
- raise configexc.ValidationError(
- pattern, "must be a valid regex - " + str(e))
- except RuntimeError: # pragma: no cover
- raise configexc.ValidationError(
- pattern, "must be a valid regex - recursion depth "
- "exceeded")
-
- assert recorded_warnings is not None
-
- for w in recorded_warnings:
- if (issubclass(w.category, DeprecationWarning) and
- str(w.message).startswith('bad escape')):
- raise configexc.ValidationError(
- pattern, "must be a valid regex - " + str(w.message))
- warnings.warn(w.message)
+ except (re.error, FutureWarning) as e:
+ raise configexc.ValidationError(
+ pattern, "must be a valid regex - " + str(e))
+ except RuntimeError: # pragma: no cover
+ raise configexc.ValidationError(
+ pattern, "must be a valid regex - recursion depth "
+ "exceeded")
return compiled
diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py
index 0c517a14c..868f4d669 100644
--- a/qutebrowser/config/qtargs.py
+++ b/qutebrowser/config/qtargs.py
@@ -61,90 +61,6 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]:
return argv
-def _darkmode_settings() -> typing.Iterator[typing.Tuple[str, str]]:
- """Get necessary blink settings to configure dark mode for QtWebEngine."""
- if not config.val.colors.webpage.darkmode.enabled:
- return
-
- # Mapping from a colors.webpage.darkmode.algorithm setting value to
- # Chromium's DarkModeInversionAlgorithm enum values.
- algorithms = {
- # 0: kOff (not exposed)
- # 1: kSimpleInvertForTesting (not exposed)
- 'brightness-rgb': 2, # kInvertBrightness
- 'lightness-hsl': 3, # kInvertLightness
- 'lightness-cielab': 4, # kInvertLightnessLAB
- }
-
- # Mapping from a colors.webpage.darkmode.policy.images setting value to
- # Chromium's DarkModeImagePolicy enum values.
- image_policies = {
- 'always': 0, # kFilterAll
- 'never': 1, # kFilterNone
- 'smart': 2, # kFilterSmart
- }
-
- # Mapping from a colors.webpage.darkmode.policy.page setting value to
- # Chromium's DarkModePagePolicy enum values.
- page_policies = {
- 'always': 0, # kFilterAll
- 'smart': 1, # kFilterByBackground
- }
-
- bools = {
- True: 'true',
- False: 'false',
- }
-
- _setting_description_type = typing.Tuple[
- str, # qutebrowser option name
- str, # darkmode setting name
- # Mapping from the config value to a string (or something convertable
- # to a string) which gets passed to Chromium.
- typing.Optional[typing.Mapping[typing.Any, typing.Union[str, int]]],
- ]
- if qtutils.version_check('5.15', compiled=False):
- settings = [
- ('enabled', 'Enabled', bools),
- ('algorithm', 'InversionAlgorithm', algorithms),
- ] # type: typing.List[_setting_description_type]
- mandatory_setting = 'enabled'
- else:
- settings = [
- ('algorithm', '', algorithms),
- ]
- mandatory_setting = 'algorithm'
-
- settings += [
- ('contrast', 'Contrast', None),
- ('policy.images', 'ImagePolicy', image_policies),
- ('policy.page', 'PagePolicy', page_policies),
- ('threshold.text', 'TextBrightnessThreshold', None),
- ('threshold.background', 'BackgroundBrightnessThreshold', None),
- ('grayscale.all', 'Grayscale', bools),
- ('grayscale.images', 'ImageGrayscale', None),
- ]
-
- for setting, key, mapping in settings:
- # To avoid blowing up the commandline length, we only pass modified
- # settings to Chromium, as our defaults line up with Chromium's.
- # However, we always pass enabled/algorithm to make sure dark mode gets
- # actually turned on.
- value = config.instance.get(
- 'colors.webpage.darkmode.' + setting,
- fallback=setting == mandatory_setting)
- if isinstance(value, usertypes.Unset):
- continue
-
- if mapping is not None:
- value = mapping[value]
-
- # FIXME: This is "forceDarkMode" starting with Chromium 83
- prefix = 'darkMode'
-
- yield prefix + key, str(value)
-
-
def _qtwebengine_enabled_features(
feature_flags: typing.Sequence[str],
) -> typing.Iterator[str]:
@@ -230,7 +146,11 @@ def _qtwebengine_args(
yield '--enable-logging'
yield '--v=1'
- blink_settings = list(_darkmode_settings())
+ if 'wait-renderer-process' in namespace.debug_flags:
+ yield '--renderer-startup-dialog'
+
+ from qutebrowser.browser.webengine import darkmode
+ blink_settings = list(darkmode.settings())
if blink_settings:
yield '--blink-settings=' + ','.join('{}={}'.format(k, v)
for k, v in blink_settings)
@@ -239,6 +159,10 @@ def _qtwebengine_args(
if enabled_features:
yield '--enable-features=' + ','.join(enabled_features)
+ yield from _qtwebengine_settings_args()
+
+
+def _qtwebengine_settings_args() -> typing.Iterator[str]:
settings = {
'qt.force_software_rendering': {
'software-opengl': None,
diff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py
index 6c5756016..fddeaabc9 100644
--- a/qutebrowser/extensions/interceptors.py
+++ b/qutebrowser/extensions/interceptors.py
@@ -19,8 +19,8 @@
"""Infrastructure for intercepting requests."""
-import typing
import enum
+from typing import Callable, List, Optional
import attr
@@ -76,15 +76,15 @@ class Request:
"""A request which can be intercepted/blocked."""
#: The URL of the page being shown.
- first_party_url = attr.ib() # type: typing.Optional[QUrl]
+ first_party_url: Optional[QUrl] = attr.ib()
#: The URL of the file being requested.
- request_url = attr.ib() # type: QUrl
+ request_url: QUrl = attr.ib()
- is_blocked = attr.ib(False) # type: bool
+ is_blocked: bool = attr.ib(False)
#: The resource type of the request. None if not supported on this backend.
- resource_type = attr.ib(None) # type: typing.Optional[ResourceType]
+ resource_type: Optional[ResourceType] = attr.ib(None)
def block(self) -> None:
"""Block this request."""
@@ -107,10 +107,10 @@ class Request:
#: Type annotation for an interceptor function.
-InterceptorType = typing.Callable[[Request], None]
+InterceptorType = Callable[[Request], None]
-_interceptors = [] # type: typing.List[InterceptorType]
+_interceptors: List[InterceptorType] = []
def register(interceptor: InterceptorType) -> None:
diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py
index 41b9c63fd..b6d86f517 100644
--- a/qutebrowser/extensions/loader.py
+++ b/qutebrowser/extensions/loader.py
@@ -19,12 +19,13 @@
"""Loader for qutebrowser extensions."""
-import importlib.abc
import pkgutil
import types
-import typing
import sys
import pathlib
+import importlib
+import argparse
+from typing import Callable, Iterator, List, Optional, Set, Tuple
import attr
@@ -35,9 +36,6 @@ from qutebrowser.config import config
from qutebrowser.utils import log, standarddir
from qutebrowser.misc import objects
-if typing.TYPE_CHECKING:
- import argparse
-
# ModuleInfo objects for all loaded plugins
_module_infos = []
@@ -48,9 +46,9 @@ class InitContext:
"""Context an extension gets in its init hook."""
- data_dir = attr.ib() # type: pathlib.Path
- config_dir = attr.ib() # type: pathlib.Path
- args = attr.ib() # type: argparse.Namespace
+ data_dir: pathlib.Path = attr.ib()
+ config_dir: pathlib.Path = attr.ib()
+ args: argparse.Namespace = attr.ib()
@attr.s
@@ -61,13 +59,11 @@ class ModuleInfo:
This gets used by qutebrowser.api.hook.
"""
- _ConfigChangedHooksType = typing.List[typing.Tuple[typing.Optional[str],
- typing.Callable]]
+ _ConfigChangedHooksType = List[Tuple[Optional[str], Callable]]
- skip_hooks = attr.ib(False) # type: bool
- init_hook = attr.ib(None) # type: typing.Optional[typing.Callable]
- config_changed_hooks = attr.ib(
- attr.Factory(list)) # type: _ConfigChangedHooksType
+ skip_hooks: bool = attr.ib(False)
+ init_hook: Optional[Callable] = attr.ib(None)
+ config_changed_hooks: _ConfigChangedHooksType = attr.ib(attr.Factory(list))
@attr.s
@@ -75,7 +71,7 @@ class ExtensionInfo:
"""Information about a qutebrowser extension."""
- name = attr.ib() # type: str
+ name: str = attr.ib()
def add_module_info(module: types.ModuleType) -> ModuleInfo:
@@ -92,7 +88,7 @@ def load_components(*, skip_hooks: bool = False) -> None:
_load_component(info, skip_hooks=skip_hooks)
-def walk_components() -> typing.Iterator[ExtensionInfo]:
+def walk_components() -> Iterator[ExtensionInfo]:
"""Yield ExtensionInfo objects for all modules."""
if hasattr(sys, 'frozen'):
yield from _walk_pyinstaller()
@@ -104,7 +100,7 @@ def _on_walk_error(name: str) -> None:
raise ImportError("Failed to import {}".format(name))
-def _walk_normal() -> typing.Iterator[ExtensionInfo]:
+def _walk_normal() -> Iterator[ExtensionInfo]:
"""Walk extensions when not using PyInstaller."""
for _finder, name, ispkg in pkgutil.walk_packages(
# Only packages have a __path__ attribute,
@@ -117,7 +113,7 @@ def _walk_normal() -> typing.Iterator[ExtensionInfo]:
yield ExtensionInfo(name=name)
-def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]:
+def _walk_pyinstaller() -> Iterator[ExtensionInfo]:
"""Walk extensions when using PyInstaller.
See https://github.com/pyinstaller/pyinstaller/issues/1905
@@ -125,7 +121,7 @@ def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]:
Inspired by:
https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py
"""
- toc = set() # type: typing.Set[str]
+ toc: Set[str] = set()
for importer in pkgutil.iter_importers('qutebrowser'):
if hasattr(importer, 'toc'):
toc |= importer.toc
diff --git a/qutebrowser/html/warning-sessions.html b/qutebrowser/html/warning-sessions.html
index 6f447483f..82bc02aab 100644
--- a/qutebrowser/html/warning-sessions.html
+++ b/qutebrowser/html/warning-sessions.html
@@ -5,11 +5,11 @@
<span class="note">Note this warning will only appear once. Use <span class="mono">:open
qute://warning/sessions</span> to show it again at a later time.</span>
-<p>You're using qutebrowser with Qt 5.15.</p>
+<p>You're using qutebrowser with Qt 5.15. While this is the recommended Qt version to use (due to QtWebEngine security updates), qutebrowser only provides partial support for session files.</p>
<p>Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.</p>
-<p>At the time of writing (September 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v1.15.0.</p>
+<p>At the time of writing (October 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v2.0.0 (planned to be released at the end of the year or early 2021).</p>
<p>As a stop-gap measure:</p>
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
index dea85aede..23b77cba1 100644
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -21,7 +21,7 @@
import string
import types
-import typing
+from typing import Mapping, MutableMapping, Optional, Sequence
import attr
from PyQt5.QtCore import pyqtSignal, QObject, Qt
@@ -37,9 +37,9 @@ class MatchResult:
"""The result of matching a keybinding."""
- match_type = attr.ib() # type: QKeySequence.SequenceMatch
- command = attr.ib() # type: typing.Optional[str]
- sequence = attr.ib() # type: keyutils.KeySequence
+ match_type: QKeySequence.SequenceMatch = attr.ib()
+ command: Optional[str] = attr.ib()
+ sequence: keyutils.KeySequence = attr.ib()
def __attrs_post_init__(self) -> None:
if self.match_type == QKeySequence.ExactMatch:
@@ -75,9 +75,8 @@ class BindingTrie:
__slots__ = 'children', 'command'
def __init__(self) -> None:
- self.children = {
- } # type: typing.MutableMapping[keyutils.KeyInfo, BindingTrie]
- self.command = None # type: typing.Optional[str]
+ self.children: MutableMapping[keyutils.KeyInfo, BindingTrie] = {}
+ self.command: Optional[str] = None
def __setitem__(self, sequence: keyutils.KeySequence,
command: str) -> None:
@@ -99,8 +98,7 @@ class BindingTrie:
def __str__(self) -> str:
return '\n'.join(self.string_lines(blank=True))
- def string_lines(self, indent: int = 0,
- blank: bool = False) -> typing.Sequence[str]:
+ def string_lines(self, indent: int = 0, blank: bool = False) -> Sequence[str]:
"""Get a list of strings for a pretty-printed version of this trie."""
lines = []
if self.command is not None:
@@ -114,7 +112,7 @@ class BindingTrie:
return lines
- def update(self, mapping: typing.Mapping) -> None:
+ def update(self, mapping: Mapping) -> None:
"""Add data from the given mapping to the trie."""
for key in mapping:
self[key] = mapping[key]
diff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py
index 6ef0dd201..d77c8702d 100644
--- a/qutebrowser/keyinput/eventfilter.py
+++ b/qutebrowser/keyinput/eventfilter.py
@@ -19,7 +19,7 @@
"""Global Qt event filter which dispatches key events."""
-import typing
+from typing import cast
from PyQt5.QtCore import pyqtSlot, QObject, QEvent
from PyQt5.QtGui import QKeyEvent, QWindow
@@ -102,7 +102,7 @@ class EventFilter(QObject):
handler = self._handlers[typ]
try:
- return handler(typing.cast(QKeyEvent, event))
+ return handler(cast(QKeyEvent, event))
except:
# If there is an exception in here and we leave the eventfilter
# activated, we'll get an infinite loop and a stack overflow.
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
index b95f4a55d..aa5457c6d 100644
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -32,7 +32,7 @@ handle what we actually think we do.
"""
import itertools
-import typing
+from typing import cast, overload, Iterable, Iterator, List, Mapping, Optional, Union
import attr
from PyQt5.QtCore import Qt, QEvent
@@ -53,10 +53,10 @@ _MODIFIER_MAP = {
_NIL_KEY = Qt.Key(0)
-_ModifierType = typing.Union[Qt.KeyboardModifier, Qt.KeyboardModifiers]
+_ModifierType = Union[Qt.KeyboardModifier, Qt.KeyboardModifiers]
-def _build_special_names() -> typing.Mapping[Qt.Key, str]:
+def _build_special_names() -> Mapping[Qt.Key, str]:
"""Build _SPECIAL_NAMES dict from the special_names_str mapping below.
The reason we don't do this directly is that certain Qt versions don't have
@@ -231,8 +231,7 @@ def _remap_unicode(key: Qt.Key, text: str) -> Qt.Key:
return key
-def _check_valid_utf8(s: str,
- data: typing.Union[Qt.Key, _ModifierType]) -> None:
+def _check_valid_utf8(s: str, data: Union[Qt.Key, _ModifierType]) -> None:
"""Make sure the given string is valid UTF-8.
Makes sure there are no chars where Qt did fall back to weird UTF-16
@@ -288,7 +287,7 @@ class KeyParseError(Exception):
"""Raised by _parse_single_key/parse_keystring on parse errors."""
- def __init__(self, keystr: typing.Optional[str], error: str) -> None:
+ def __init__(self, keystr: Optional[str], error: str) -> None:
if keystr is None:
msg = "Could not parse keystring: {}".format(error)
else:
@@ -296,7 +295,7 @@ class KeyParseError(Exception):
super().__init__(msg)
-def _parse_keystring(keystr: str) -> typing.Iterator[str]:
+def _parse_keystring(keystr: str) -> Iterator[str]:
key = ''
special = False
for c in keystr:
@@ -363,8 +362,8 @@ class KeyInfo:
modifiers: A Qt::KeyboardModifiers enum value.
"""
- key = attr.ib() # type: Qt.Key
- modifiers = attr.ib() # type: _ModifierType
+ key: Qt.Key = attr.ib()
+ modifiers: _ModifierType = attr.ib()
@classmethod
def from_event(cls, e: QKeyEvent) -> 'KeyInfo':
@@ -377,7 +376,7 @@ class KeyInfo:
modifiers = e.modifiers()
_assert_plain_key(key)
_assert_plain_modifier(modifiers)
- return cls(key, typing.cast(Qt.KeyboardModifier, modifiers))
+ return cls(key, cast(Qt.KeyboardModifier, modifiers))
def __hash__(self) -> int:
"""Convert KeyInfo to int before hashing.
@@ -473,7 +472,7 @@ class KeySequence:
_MAX_LEN = 4
def __init__(self, *keys: int) -> None:
- self._sequences = [] # type: typing.List[QKeySequence]
+ self._sequences: List[QKeySequence] = []
for sub in utils.chunk(keys, self._MAX_LEN):
args = [self._convert_key(key) for key in sub]
sequence = QKeySequence(*args)
@@ -493,7 +492,7 @@ class KeySequence:
parts.append(str(info))
return ''.join(parts)
- def __iter__(self) -> typing.Iterator[KeyInfo]:
+ def __iter__(self) -> Iterator[KeyInfo]:
"""Iterate over KeyInfo objects."""
for key_and_modifiers in self._iter_keys():
key = Qt.Key(int(key_and_modifiers) & ~Qt.KeyboardModifierMask)
@@ -535,17 +534,15 @@ class KeySequence:
def __bool__(self) -> bool:
return bool(self._sequences)
- @typing.overload
+ @overload
def __getitem__(self, item: int) -> KeyInfo:
...
- @typing.overload
+ @overload
def __getitem__(self, item: slice) -> 'KeySequence':
...
- def __getitem__(
- self, item: typing.Union[int, slice]
- ) -> typing.Union[KeyInfo, 'KeySequence']:
+ def __getitem__(self, item: Union[int, slice]) -> Union[KeyInfo, 'KeySequence']:
if isinstance(item, slice):
keys = list(self._iter_keys())
return self.__class__(*keys[item])
@@ -553,9 +550,8 @@ class KeySequence:
infos = list(self)
return infos[item]
- def _iter_keys(self) -> typing.Iterator[int]:
- sequences = typing.cast(typing.Iterable[typing.Iterable[int]],
- self._sequences)
+ def _iter_keys(self) -> Iterator[int]:
+ sequences = cast(Iterable[Iterable[int]], self._sequences)
return itertools.chain.from_iterable(sequences)
def _validate(self, keystr: str = None) -> None:
@@ -664,7 +660,7 @@ class KeySequence:
def with_mappings(
self,
- mappings: typing.Mapping['KeySequence', 'KeySequence']
+ mappings: Mapping['KeySequence', 'KeySequence']
) -> 'KeySequence':
"""Get a new KeySequence with the given mappings applied."""
keys = []
diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py
index 6e48e5a3f..ee8883070 100644
--- a/qutebrowser/keyinput/macros.py
+++ b/qutebrowser/keyinput/macros.py
@@ -20,7 +20,7 @@
"""Keyboard macro system."""
-import typing
+from typing import cast, Dict, List, Optional, Tuple
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
@@ -28,9 +28,9 @@ from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, objreg, usertypes
-_CommandType = typing.Tuple[str, int] # command, type
+_CommandType = Tuple[str, int] # command, type
-macro_recorder = typing.cast('MacroRecorder', None)
+macro_recorder = cast('MacroRecorder', None)
class MacroRecorder:
@@ -47,10 +47,10 @@ class MacroRecorder:
"""
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]
+ self._macros: Dict[str, List[_CommandType]] = {}
+ self._recording_macro: Optional[str] = None
+ self._macro_count: Dict[int, int] = {}
+ self._last_register: Optional[str] = None
@cmdutils.register(instance='macro-recorder', name='record-macro')
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index 4febf98a8..27e4be34e 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
-"""Mode manager singleton which handles the current keyboard mode."""
+"""Mode manager (per window) which handles the current keyboard mode."""
import functools
from typing import Mapping, Callable, MutableMapping, Union, Set, cast
@@ -78,15 +78,17 @@ class UnavailableError(Exception):
def init(win_id: int, parent: QObject) -> 'ModeManager':
"""Initialize the mode manager and the keyparsers for the given win_id."""
+ commandrunner = runners.CommandRunner(win_id)
+
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)
+ modeman.hintmanager = hintmanager
+
keyparsers = {
usertypes.KeyMode.normal:
modeparsers.NormalKeyParser(
@@ -227,6 +229,7 @@ class ModeManager(QObject):
Attributes:
mode: The mode we're currently in.
+ hintmanager: The HintManager associated with this window.
_win_id: The window ID of this ModeManager
_prev_mode: Mode before a prompt popped up
parsers: A dictionary of modes and their keyparsers.
@@ -260,6 +263,8 @@ class ModeManager(QObject):
self._prev_mode = usertypes.KeyMode.normal
self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set() # type: Set[KeyEvent]
+ # Set after __init__
+ self.hintmanager = cast(hints.HintManager, None)
def __repr__(self) -> str:
return utils.get_repr(self, mode=self.mode)
diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py
index a55639898..48f3594a5 100644
--- a/qutebrowser/keyinput/modeparsers.py
+++ b/qutebrowser/keyinput/modeparsers.py
@@ -23,9 +23,9 @@ Module attributes:
STARTCHARS: Possible chars for starting a commandline input.
"""
-import typing
import traceback
import enum
+from typing import TYPE_CHECKING, Sequence
from PyQt5.QtCore import pyqtSlot, Qt, QObject
from PyQt5.QtGui import QKeySequence, QKeyEvent
@@ -35,12 +35,20 @@ from qutebrowser.commands import cmdexc
from qutebrowser.config import config
from qutebrowser.keyinput import basekeyparser, keyutils, macros
from qutebrowser.utils import usertypes, log, message, objreg, utils
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.commands import runners
STARTCHARS = ":/?"
-LastPress = enum.Enum('LastPress', ['none', 'filtertext', 'keystring'])
+
+
+class LastPress(enum.Enum):
+
+ """Whether the last keypress filtered a text or was part of a keystring."""
+
+ none = enum.auto()
+ filtertext = enum.auto()
+ keystring = enum.auto()
class CommandKeyParser(basekeyparser.BaseKeyParser):
@@ -224,7 +232,7 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
return match
- def update_bindings(self, strings: typing.Sequence[str],
+ def update_bindings(self, strings: Sequence[str],
preserve_filter: bool = False) -> None:
"""Update bindings when the hint strings changed.
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index faccdc73c..b8228545a 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -23,9 +23,9 @@ import binascii
import base64
import itertools
import functools
-import typing
+from typing import List, MutableSequence, Optional, Tuple, cast
-from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QRect, QPoint, QTimer, Qt,
+from PyQt5.QtCore import (pyqtBoundSignal, pyqtSlot, QRect, QPoint, QTimer, Qt,
QCoreApplication, QEventLoop, QByteArray)
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
from PyQt5.QtGui import QPalette
@@ -105,23 +105,20 @@ def raise_window(window, alert=True):
def get_target_window():
"""Get the target window for new tabs, or None if none exist."""
+ getters = {
+ 'last-focused': objreg.last_focused_window,
+ 'first-opened': objreg.first_opened_window,
+ 'last-opened': objreg.last_opened_window,
+ 'last-visible': objreg.last_visible_window,
+ }
+ getter = getters[config.val.new_instance_open_target_window]
try:
- win_mode = config.val.new_instance_open_target_window
- if win_mode == 'last-focused':
- return objreg.last_focused_window()
- elif win_mode == 'first-opened':
- return objreg.window_by_index(0)
- elif win_mode == 'last-opened':
- return objreg.window_by_index(-1)
- elif win_mode == 'last-visible':
- return objreg.last_visible_window()
- else:
- raise ValueError("Invalid win_mode {}".format(win_mode))
+ return getter()
except objreg.NoWindow:
return None
-_OverlayInfoType = typing.Tuple[QWidget, pyqtSignal, bool, str]
+_OverlayInfoType = Tuple[QWidget, pyqtBoundSignal, bool, str]
class MainWindow(QWidget):
@@ -190,8 +187,8 @@ class MainWindow(QWidget):
def __init__(self, *,
private: bool,
- geometry: typing.Optional[QByteArray] = None,
- parent: typing.Optional[QWidget] = None) -> None:
+ geometry: Optional[QByteArray] = None,
+ parent: Optional[QWidget] = None) -> None:
"""Create a new main window.
Args:
@@ -208,7 +205,7 @@ class MainWindow(QWidget):
self.setAttribute(Qt.WA_DeleteOnClose)
self.setAttribute(Qt.WA_TranslucentBackground)
self.palette().setColor(QPalette.Window, Qt.transparent)
- self._overlays = [] # type: typing.MutableSequence[_OverlayInfoType]
+ self._overlays: MutableSequence[_OverlayInfoType] = []
self.win_id = next(win_id_gen)
self.registry = objreg.ObjectRegistry()
objreg.window_registry[self.win_id] = self
@@ -218,10 +215,6 @@ class MainWindow(QWidget):
objreg.register('tab-registry', tab_registry, scope='window',
window=self.win_id)
- message_bridge = message.MessageBridge(self)
- objreg.register('message-bridge', message_bridge, scope='window',
- window=self.win_id)
-
self.setWindowTitle('qutebrowser')
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
@@ -233,9 +226,8 @@ class MainWindow(QWidget):
self.is_private = config.val.content.private_browsing or private
- self.tabbed_browser = tabbedbrowser.TabbedBrowser(
- win_id=self.win_id, private=self.is_private, parent=self
- ) # type: tabbedbrowser.TabbedBrowser
+ self.tabbed_browser: tabbedbrowser.TabbedBrowser = tabbedbrowser.TabbedBrowser(
+ win_id=self.win_id, private=self.is_private, parent=self)
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
window=self.win_id)
self._init_command_dispatcher()
@@ -420,7 +412,7 @@ class MainWindow(QWidget):
self._vbox.removeWidget(self.tabbed_browser.widget)
self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status)
- widgets = [self.tabbed_browser.widget] # type: typing.List[QWidget]
+ widgets: List[QWidget] = [self.tabbed_browser.widget]
downloads_position = config.val.downloads.position
if downloads_position == 'top':
@@ -484,13 +476,8 @@ class MainWindow(QWidget):
"""Set some sensible default geometry."""
self.setGeometry(QRect(50, 50, 800, 600))
- def _get_object(self, name):
- """Get an object for this window in the object registry."""
- return objreg.get(name, scope='window', window=self.win_id)
-
def _connect_signals(self):
"""Connect all mainwindow signals."""
- message_bridge = self._get_object('message-bridge')
mode_manager = modeman.instance(self.win_id)
# misc
@@ -498,23 +485,19 @@ class MainWindow(QWidget):
mode_manager.entered.connect(hints.on_mode_entered)
# status bar
+ mode_manager.hintmanager.set_text.connect(self.status.set_text)
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) # type: ignore[arg-type]
+ mode_manager.left.connect(message.global_bridge.mode_left)
# commands
mode_manager.keystring_updated.connect(
self.status.keystring.on_keystring_updated)
- self.status.cmd.got_cmd[str].connect( # type: ignore[index]
- self._commandrunner.run_safely)
- self.status.cmd.got_cmd[str, int].connect( # type: ignore[index]
- 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)
+ 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
mode_manager.keystring_updated.connect(self._keyhint.update_keyhint)
@@ -526,10 +509,6 @@ class MainWindow(QWidget):
message.global_bridge.clear_messages.connect(
self._messageview.clear_messages)
- 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(
self.status.on_tab_changed)
@@ -578,11 +557,11 @@ class MainWindow(QWidget):
def _set_decoration(self, hidden):
"""Set the visibility of the window decoration via Qt."""
- window_flags = Qt.Window # type: int
+ window_flags: int = Qt.Window
refresh_window = self.isVisible()
if hidden:
window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint
- self.setWindowFlags(typing.cast(Qt.WindowFlags, window_flags))
+ self.setWindowFlags(cast(Qt.WindowFlags, window_flags))
if refresh_window:
self.show()
diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py
index 1f6295d89..9c4b63084 100644
--- a/qutebrowser/mainwindow/messageview.py
+++ b/qutebrowser/mainwindow/messageview.py
@@ -19,7 +19,7 @@
"""Showing messages above the statusbar."""
-import typing
+from typing import MutableSequence
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
@@ -76,7 +76,7 @@ class MessageView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
- self._messages = [] # type: typing.MutableSequence[Message]
+ self._messages: MutableSequence[Message] = []
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index a929d6428..32fd9709e 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -23,7 +23,7 @@ import os.path
import html
import collections
import functools
-import typing
+from typing import Deque, MutableSequence, Optional, cast
import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
@@ -40,7 +40,7 @@ from qutebrowser.api import cmdutils
from qutebrowser.utils import urlmatch
-prompt_queue = typing.cast('PromptQueue', None)
+prompt_queue = cast('PromptQueue', None)
@attr.s
@@ -102,9 +102,8 @@ class PromptQueue(QObject):
super().__init__(parent)
self._question = None
self._shutting_down = False
- self._loops = [] # type: typing.MutableSequence[qtutils.EventLoop]
- self._queue = collections.deque(
- ) # type: typing.Deque[usertypes.Question]
+ self._loops: MutableSequence[qtutils.EventLoop] = []
+ self._queue: Deque[usertypes.Question] = collections.deque()
message.global_bridge.mode_left.connect(self._on_mode_left)
def __repr__(self):
@@ -196,8 +195,8 @@ class PromptQueue(QObject):
question.completed.connect(loop.quit)
question.completed.connect(loop.deleteLater)
log.prompt.debug("Starting loop.exec_() for {}".format(question))
- flags = typing.cast(QEventLoop.ProcessEventsFlags,
- QEventLoop.ExcludeSocketNotifiers)
+ flags = cast(QEventLoop.ProcessEventsFlags,
+ QEventLoop.ExcludeSocketNotifiers)
loop.exec_(flags)
log.prompt.debug("Ending loop.exec_() for {}".format(question))
@@ -289,7 +288,7 @@ class PromptContainer(QWidget):
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(10, 10, 10, 10)
self._win_id = win_id
- self._prompt = None # type: typing.Optional[_BasePrompt]
+ self._prompt: Optional[_BasePrompt] = None
self.setObjectName('PromptContainer')
self.setAttribute(Qt.WA_StyledBackground, True)
@@ -794,8 +793,7 @@ class DownloadFilenamePrompt(FilenamePrompt):
def download_open(self, cmdline, pdfjs):
if pdfjs:
- target = downloads.PDFJSDownloadTarget(
- ) # type: downloads._DownloadTarget
+ target: 'downloads._DownloadTarget' = downloads.PDFJSDownloadTarget()
else:
target = downloads.OpenFileDownloadTarget(cmdline)
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index f83c77db9..821ea030b 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -31,8 +31,7 @@ from qutebrowser.keyinput import modeman
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
keystring, percentage, url,
- tabindex)
-from qutebrowser.mainwindow.statusbar import text as textwidget
+ tabindex, textbase)
@attr.s
@@ -49,7 +48,14 @@ class ColorFlags:
passthrough: If we're currently in passthrough-mode.
"""
- CaretMode = enum.Enum('CaretMode', ['off', 'on', 'selection'])
+ class CaretMode(enum.Enum):
+
+ """The current caret "sub-mode" we're in."""
+
+ off = enum.auto()
+ on = enum.auto()
+ selection = enum.auto()
+
prompt = attr.ib(False)
insert = attr.ib(False)
command = attr.ib(False)
@@ -180,7 +186,7 @@ class StatusBar(QWidget):
objreg.register('status-command', self.cmd, scope='window',
window=win_id)
- self.txt = textwidget.Text()
+ self.txt = textbase.TextBase()
self._stack.addWidget(self.txt)
self.cmd.show_cmd.connect(self._show_cmd_widget)
@@ -328,7 +334,7 @@ class StatusBar(QWidget):
else:
suffix = ''
text = "-- {} MODE --{}".format(mode.upper(), suffix)
- self.txt.set_text(self.txt.Text.normal, text)
+ self.txt.setText(text)
def _show_cmd_widget(self):
"""Show command widget instead of temporary text."""
@@ -342,9 +348,10 @@ class StatusBar(QWidget):
self.maybe_hide()
@pyqtSlot(str)
- def set_text(self, val):
+ def set_text(self, text):
"""Set a normal (persistent) text in the status bar."""
- self.txt.set_text(self.txt.Text.normal, val)
+ log.message.debug(text)
+ self.txt.setText(text)
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
@@ -372,7 +379,7 @@ class StatusBar(QWidget):
if mode_manager.parsers[new_mode].passthrough:
self._set_mode_text(new_mode.name)
else:
- self.txt.set_text(self.txt.Text.normal, '')
+ self.txt.setText('')
if old_mode in [usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret,
diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py
index ebd9d3921..da48d1fbd 100644
--- a/qutebrowser/mainwindow/statusbar/command.py
+++ b/qutebrowser/mainwindow/statusbar/command.py
@@ -71,10 +71,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.history.changed.connect(command_history.changed)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
- self.cursorPositionChanged.connect(
- self.update_completion) # type: ignore[arg-type]
- self.textChanged.connect(
- self.update_completion) # type: ignore[arg-type]
+ self.cursorPositionChanged.connect(self.update_completion)
+ self.textChanged.connect(self.update_completion)
self.textChanged.connect(self.updateGeometry)
self.textChanged.connect(self._incremental_search)
@@ -149,7 +147,7 @@ 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) # type: ignore[index]
+ self.got_cmd[str, int].emit(text, count)
else:
self.set_cmd_text(text)
@@ -199,7 +197,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
'cmd accept')
if not was_search:
- self.got_cmd[str].emit(text[1:]) # type: ignore[index]
+ self.got_cmd[str].emit(text[1:])
@cmdutils.register(instance='status-command', scope='window')
def edit_command(self, run: bool = False) -> None:
diff --git a/qutebrowser/mainwindow/statusbar/text.py b/qutebrowser/mainwindow/statusbar/text.py
deleted file mode 100644
index 449836740..000000000
--- a/qutebrowser/mainwindow/statusbar/text.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2014-2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
-#
-# This file is part of qutebrowser.
-#
-# qutebrowser is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# qutebrowser is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
-
-"""Text displayed in the statusbar."""
-
-import enum
-
-from PyQt5.QtCore import pyqtSlot
-
-from qutebrowser.mainwindow.statusbar import textbase
-from qutebrowser.utils import log
-
-
-class Text(textbase.TextBase):
-
- """Text displayed in the statusbar.
-
- Attributes:
- _normaltext: The "permanent" text. Never automatically cleared.
- _temptext: The temporary text to display.
-
- The temptext is shown from StatusBar when a temporary text or error is
- available. If not, the permanent text is shown.
- """
-
- Text = enum.Enum('Text', ['normal', 'temp'])
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self._normaltext = ''
- self._temptext = ''
-
- def set_text(self, which, text):
- """Set a text.
-
- Args:
- which: Which text to set, a self.Text instance.
- text: The text to set.
- """
- log.statusbar.debug("Setting {} text to '{}'.".format(
- which.name, text))
- if which is self.Text.normal:
- self._normaltext = text
- elif which is self.Text.temp:
- self._temptext = text
- else:
- raise ValueError("Invalid value {} for which!".format(which))
- self.update_text()
-
- @pyqtSlot(str)
- def maybe_reset_text(self, text):
- """Clear a normal text if it still matches an expected text."""
- if self._normaltext == text:
- log.statusbar.debug("Resetting: '{}'".format(text))
- self.set_text(self.Text.normal, '')
- else:
- log.statusbar.debug("Ignoring reset: '{}'".format(text))
-
- def update_text(self):
- """Update QLabel text when needed."""
- if self._temptext:
- self.setText(self._temptext)
- elif self._normaltext:
- self.setText(self._normaltext)
- else:
- self.setText('')
diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py
index c8300dc97..db8905345 100644
--- a/qutebrowser/mainwindow/statusbar/url.py
+++ b/qutebrowser/mainwindow/statusbar/url.py
@@ -29,9 +29,19 @@ from qutebrowser.config import stylesheet
from qutebrowser.utils import usertypes, urlutils
-# Note this has entries for success/error/warn from widgets.webview:LoadStatus
-UrlType = enum.Enum('UrlType', ['success', 'success_https', 'error', 'warn',
- 'hover', 'normal'])
+class UrlType(enum.Enum):
+
+ """The type/color of the URL being shown.
+
+ Note this has entries for success/error/warn from widgets.webview:LoadStatus.
+ """
+
+ success = enum.auto()
+ success_https = enum.auto()
+ error = enum.auto()
+ warn = enum.auto()
+ hover = enum.auto()
+ normal = enum.auto()
class UrlText(textbase.TextBase):
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 57a9ae018..2523cdaeb 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -22,8 +22,9 @@
import collections
import functools
import weakref
-import typing
import datetime
+from typing import (
+ Any, Deque, List, Mapping, MutableMapping, MutableSequence, Optional, Tuple)
import attr
from PyQt5.QtWidgets import QSizePolicy, QWidget, QApplication
@@ -68,12 +69,10 @@ class TabDeque:
size = config.val.tabs.focus_stack_size
if size < 0:
size = None
- self._stack = collections.deque(
- maxlen=size
- ) # type: typing.Deque[weakref.ReferenceType[QWidget]]
+ self._stack: Deque[weakref.ReferenceType[QWidget]] = collections.deque(
+ maxlen=size)
# Items that have been removed from the primary stack.
- self._stack_deleted = [
- ] # type: typing.List[weakref.ReferenceType[QWidget]]
+ self._stack_deleted: List[weakref.ReferenceType[QWidget]] = []
self._ignore_next = False
self._keep_deleted_next = False
@@ -94,7 +93,7 @@ class TabDeque:
Throws IndexError on failure.
"""
- tab = None # type: typing.Optional[QWidget]
+ tab: Optional[QWidget] = None
while tab is None or tab.pending_removal or tab is cur_tab:
tab = self._stack.pop()()
self._stack_deleted.append(weakref.ref(cur_tab))
@@ -106,7 +105,7 @@ class TabDeque:
Throws IndexError on failure.
"""
- tab = None # type: typing.Optional[QWidget]
+ tab: Optional[QWidget] = None
while tab is None or tab.pending_removal or tab is cur_tab:
tab = self._stack_deleted.pop()()
# On next tab-switch, current tab will be added to stack as normal.
@@ -224,18 +223,15 @@ class TabbedBrowser(QWidget):
# This init is never used, it is immediately thrown away in the next
# line.
- self.undo_stack = (
- collections.deque()
- ) # type: typing.MutableSequence[typing.MutableSequence[_UndoEntry]]
+ self.undo_stack: MutableSequence[MutableSequence[_UndoEntry]] = (
+ collections.deque())
self._update_stack_size()
self._filter = signalfilter.SignalFilter(win_id, self)
self._now_focused = None
self.search_text = None
- self.search_options = {} # type: typing.Mapping[str, typing.Any]
- self._local_marks = {
- } # type: typing.MutableMapping[QUrl, typing.MutableMapping[str, int]]
- self._global_marks = {
- } # type: typing.MutableMapping[str, typing.Tuple[int, QUrl]]
+ self.search_options: Mapping[str, Any] = {}
+ self._local_marks: MutableMapping[QUrl, MutableMapping[str, int]] = {}
+ self._global_marks: MutableMapping[str, Tuple[int, QUrl]] = {}
self.default_window_icon = self.widget.window().windowIcon()
self.is_private = private
self.tab_deque = TabDeque()
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index fdefa075e..b40c59bd5 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -19,9 +19,9 @@
"""The tab widget used for TabbedBrowser from browser.py."""
-import typing
import functools
import contextlib
+from typing import Optional, cast
import attr
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,
@@ -60,8 +60,7 @@ class TabWidget(QTabWidget):
bar = TabBar(win_id, self)
self.setStyle(TabBarStyle())
self.setTabBar(bar)
- bar.tabCloseRequested.connect(
- self.tabCloseRequested) # type: ignore[arg-type]
+ bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self.update_tab_titles))
bar.currentChanged.connect(self._on_current_changed)
@@ -340,8 +339,7 @@ class TabWidget(QTabWidget):
def setTabIcon(self, idx: int, icon: QIcon) -> None:
"""Always show tab icons for pinned tabs in some circumstances."""
- tab = typing.cast(typing.Optional[browsertab.AbstractTab],
- self.widget(idx))
+ tab = cast(Optional[browsertab.AbstractTab], self.widget(idx))
if (icon.isNull() and
config.cache['tabs.favicons.show'] != 'never' and
config.cache['tabs.pinned.shrink'] and
diff --git a/qutebrowser/mainwindow/windowundo.py b/qutebrowser/mainwindow/windowundo.py
index 4a4ea5d66..af7b2766a 100644
--- a/qutebrowser/mainwindow/windowundo.py
+++ b/qutebrowser/mainwindow/windowundo.py
@@ -20,7 +20,7 @@
"""Code for :undo --window."""
import collections
-import typing
+from typing import MutableSequence, cast
import attr
from PyQt5.QtCore import QObject
@@ -30,7 +30,7 @@ from qutebrowser.config import config
from qutebrowser.mainwindow import mainwindow
-instance = typing.cast('WindowUndoManager', None)
+instance = cast('WindowUndoManager', None)
@attr.s
@@ -48,9 +48,7 @@ class WindowUndoManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
- self._undos = (
- collections.deque()
- ) # type: typing.MutableSequence[_WindowUndoEntry]
+ self._undos: MutableSequence[_WindowUndoEntry] = collections.deque()
QApplication.instance().window_closing.connect(self._on_window_closing)
config.instance.changed.connect(self._on_config_changed)
diff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py
index 4838d55ed..49d648f58 100644
--- a/qutebrowser/misc/autoupdate.py
+++ b/qutebrowser/misc/autoupdate.py
@@ -55,7 +55,7 @@ class PyPIVersionClient(QObject):
self._client = httpclient.HTTPClient(self)
else:
self._client = client
- self._client.error.connect(self.error) # type: ignore[arg-type]
+ self._client.error.connect(self.error)
self._client.success.connect(self.on_client_success)
def get_version(self, package='qutebrowser'):
diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py
index 089e3191f..f9c210f15 100644
--- a/qutebrowser/misc/backendproblem.py
+++ b/qutebrowser/misc/backendproblem.py
@@ -25,8 +25,8 @@ import functools
import html
import enum
import shutil
-import typing
import argparse
+from typing import Any, List, Sequence, Tuple
import attr
from PyQt5.QtCore import Qt
@@ -55,15 +55,13 @@ class _Button:
"""A button passed to BackendProblemDialog."""
- text = attr.ib() # type: str
- setting = attr.ib() # type: str
- value = attr.ib() # type: typing.Any
- default = attr.ib(default=False) # type: bool
+ text: str = attr.ib()
+ setting: str = attr.ib()
+ value: Any = attr.ib()
+ default: bool = attr.ib(default=False)
-def _other_backend(
- backend: usertypes.Backend
-) -> typing.Tuple[usertypes.Backend, str]:
+def _other_backend(backend: usertypes.Backend) -> Tuple[usertypes.Backend, str]:
"""Get the other backend enum/setting for a given backend."""
other_backend = {
usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine,
@@ -103,7 +101,7 @@ class _Dialog(QDialog):
def __init__(self, *, because: str,
text: str,
backend: usertypes.Backend,
- buttons: typing.Sequence[_Button] = None,
+ buttons: Sequence[_Button] = None,
parent: QWidget = None) -> None:
super().__init__(parent)
vbox = QVBoxLayout(self)
@@ -157,10 +155,10 @@ class _BackendImports:
"""Whether backend modules could be imported."""
- 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
+ webkit_available: bool = attr.ib(default=None)
+ webengine_available: bool = attr.ib(default=None)
+ webkit_error: str = attr.ib(default=None)
+ webengine_error: str = attr.ib(default=None)
class _BackendProblemChecker:
@@ -181,7 +179,7 @@ class _BackendProblemChecker:
self._save_manager = save_manager
self._no_err_windows = no_err_windows
- def _show_dialog(self, *args: typing.Any, **kwargs: typing.Any) -> None:
+ def _show_dialog(self, *args: Any, **kwargs: Any) -> None:
"""Show a dialog for a backend problem."""
if self._no_err_windows:
text = _error_text(*args, **kwargs)
@@ -255,7 +253,7 @@ class _BackendProblemChecker:
raise utils.Unreachable
- def _xwayland_options(self) -> typing.Tuple[str, typing.List[_Button]]:
+ def _xwayland_options(self) -> Tuple[str, List[_Button]]:
"""Get buttons/text for a possible XWayland solution."""
buttons = []
text = "<p>You can work around this in one of the following ways:</p>"
diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py
index 8283dd13e..116514b8d 100644
--- a/qutebrowser/misc/checkpyver.py
+++ b/qutebrowser/misc/checkpyver.py
@@ -43,11 +43,11 @@ except ImportError: # pragma: no cover
# to stderr.
def check_python_version():
"""Check if correct python version is run."""
- if sys.hexversion < 0x03050200:
+ if sys.hexversion < 0x03060000:
# We don't use .format() and print_function here just in case someone
# still has < 2.6 installed.
version_str = '.'.join(map(str, sys.version_info[:3]))
- text = ("At least Python 3.5.2 is required to run qutebrowser, but " +
+ text = ("At least Python 3.6 is required to run qutebrowser, but " +
"it's running with " + version_str + ".\n")
if (Tk and # type: ignore[unreachable]
'--no-err-windows' not in sys.argv): # pragma: no cover
diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py
index 810bbd1f9..1403ee56d 100644
--- a/qutebrowser/misc/cmdhistory.py
+++ b/qutebrowser/misc/cmdhistory.py
@@ -19,7 +19,7 @@
"""Command history for the status bar."""
-import typing
+from typing import MutableSequence
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
@@ -60,7 +60,7 @@ class History(QObject):
super().__init__(parent)
self._tmphist = None
if history is None:
- self.history = [] # type: typing.MutableSequence[str]
+ self.history: MutableSequence[str] = []
else:
self.history = history
@@ -82,9 +82,9 @@ class History(QObject):
"""
log.misc.debug("Preset text: '{}'".format(text))
if text:
- items = [
+ items: MutableSequence[str] = [
e for e in self.history
- if e.startswith(text)] # type: typing.MutableSequence[str]
+ if e.startswith(text)]
else:
items = self.history
if not items:
diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py
index 3f80db769..65e584bc9 100644
--- a/qutebrowser/misc/crashsignal.py
+++ b/qutebrowser/misc/crashsignal.py
@@ -29,7 +29,7 @@ import argparse
import functools
import threading
import faulthandler
-import typing
+from typing import TYPE_CHECKING, Optional, MutableMapping, cast
try:
# WORKAROUND for segfaults when using pdb in pytest for some reason...
import readline # pylint: disable=unused-import
@@ -45,7 +45,7 @@ from qutebrowser.api import cmdutils
from qutebrowser.misc import earlyinit, crashdialog, ipc, objects
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils
from qutebrowser.qt import sip
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.misc import quitter
@@ -59,7 +59,7 @@ class ExceptionInfo:
objects = attr.ib()
-crash_handler = typing.cast('CrashHandler', None)
+crash_handler = cast('CrashHandler', None)
class CrashHandler(QObject):
@@ -337,10 +337,9 @@ class SignalHandler(QObject):
self._quitter = quitter
self._notifier = None
self._timer = usertypes.Timer(self, 'python_hacks')
- self._orig_handlers = {
- } # type: typing.MutableMapping[int, signal._HANDLER]
+ self._orig_handlers: MutableMapping[int, 'signal._HANDLER'] = {}
self._activated = False
- self._orig_wakeup_fd = None # type: typing.Optional[int]
+ self._orig_wakeup_fd: Optional[int] = None
def activate(self):
"""Set up signal handlers.
@@ -363,7 +362,7 @@ class SignalHandler(QObject):
for fd in [read_fd, write_fd]:
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
- self._notifier = QSocketNotifier(typing.cast(sip.voidptr, read_fd),
+ self._notifier = QSocketNotifier(cast(sip.voidptr, read_fd),
QSocketNotifier.Read,
self)
self._notifier.activated.connect( # type: ignore[attr-defined]
diff --git a/qutebrowser/misc/debugcachestats.py b/qutebrowser/misc/debugcachestats.py
index 417e4505a..227f9d668 100644
--- a/qutebrowser/misc/debugcachestats.py
+++ b/qutebrowser/misc/debugcachestats.py
@@ -23,17 +23,17 @@ Because many modules depend on this command, this needs to have as few
dependencies as possible to avoid cyclic dependencies.
"""
-import typing
+from typing import Any, Callable, List, Optional, Tuple, TypeVar
# The second element of each tuple should be a lru_cache wrapped function
-_CACHE_FUNCTIONS = [] # type: typing.List[typing.Tuple[str, typing.Any]]
+_CACHE_FUNCTIONS: List[Tuple[str, Any]] = []
-_T = typing.TypeVar('_T', bound=typing.Callable)
+_T = TypeVar('_T', bound=Callable)
-def register(name: typing.Optional[str] = None) -> typing.Callable[[_T], _T]:
+def register(name: Optional[str] = None) -> Callable[[_T], _T]:
"""Register a lru_cache wrapped function for debug_cache_stats."""
def wrapper(fn: _T) -> _T:
_CACHE_FUNCTIONS.append((fn.__name__ if name is None else name, fn))
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index c02b2f03c..704db1777 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -19,7 +19,7 @@
"""Things which need to be done really early (e.g. before importing Qt).
-At this point we can be sure we have all python 3.5 features available.
+At this point we can be sure we have all python 3.6 features available.
"""
try:
@@ -210,13 +210,13 @@ def _check_modules(modules):
try:
# https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e
# pylint: disable=bad-continuation
- with log.ignore_py_warnings(
+ with log.py_warning_filter(
category=DeprecationWarning,
message=r'invalid escape sequence'
- ), log.ignore_py_warnings(
+ ), log.py_warning_filter(
category=ImportWarning,
message=r'Not importing directory .*: missing __init__'
- ), log.ignore_py_warnings(
+ ), log.py_warning_filter(
category=DeprecationWarning,
message=r'the imp module is deprecated',
):
@@ -260,7 +260,7 @@ def configure_pyqt():
from qutebrowser.qt import sip
try:
# Added in sip 4.19.4
- sip.enableoverflowchecking(True) # type: ignore[attr-defined]
+ sip.enableoverflowchecking(True)
except AttributeError:
pass
diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py
index 3702715c4..872a594f3 100644
--- a/qutebrowser/misc/guiprocess.py
+++ b/qutebrowser/misc/guiprocess.py
@@ -63,11 +63,11 @@ class GUIProcess(QObject):
self._proc = QProcess(self)
self._proc.errorOccurred.connect(self._on_error)
- self._proc.errorOccurred.connect(self.error) # type: ignore[arg-type]
+ self._proc.errorOccurred.connect(self.error)
self._proc.finished.connect(self._on_finished)
- self._proc.finished.connect(self.finished) # type: ignore[arg-type]
+ self._proc.finished.connect(self.finished)
self._proc.started.connect(self._on_started)
- self._proc.started.connect(self.started) # type: ignore[arg-type]
+ self._proc.started.connect(self.started)
if additional_env is not None:
procenv = QProcessEnvironment.systemEnvironment()
diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py
index 822ba8805..b9502ca43 100644
--- a/qutebrowser/misc/httpclient.py
+++ b/qutebrowser/misc/httpclient.py
@@ -19,10 +19,9 @@
"""An HTTP client based on QNetworkAccessManager."""
-import typing
import functools
-import urllib.request
import urllib.parse
+from typing import MutableMapping
from PyQt5.QtCore import pyqtSignal, QObject, QTimer
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
@@ -67,7 +66,7 @@ class HTTPClient(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._nam = QNetworkAccessManager(self)
- self._timers = {} # type: typing.MutableMapping[QNetworkReply, QTimer]
+ self._timers: MutableMapping[QNetworkReply, QTimer] = {}
def post(self, url, data=None):
"""Create a new POST request.
diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py
index 749c5dff1..d3e5a2db0 100644
--- a/qutebrowser/misc/lineparser.py
+++ b/qutebrowser/misc/lineparser.py
@@ -22,7 +22,7 @@
import os
import os.path
import contextlib
-import typing
+from typing import Sequence
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
@@ -150,7 +150,7 @@ class LineParser(BaseLineParser):
"""
super().__init__(configdir, fname, binary=binary, parent=parent)
if not os.path.isfile(self._configfile):
- self.data = [] # type: typing.Sequence[str]
+ self.data: Sequence[str] = []
else:
log.init.debug("Reading {}".format(self._configfile))
self._read()
diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py
index 2310a1926..a38607390 100644
--- a/qutebrowser/misc/miscwidgets.py
+++ b/qutebrowser/misc/miscwidgets.py
@@ -19,7 +19,7 @@
"""Misc. widgets used at different places."""
-import typing
+from typing import Optional
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QTimer
from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
@@ -239,8 +239,8 @@ class WrapperLayout(QLayout):
def __init__(self, parent=None):
super().__init__(parent)
- self._widget = None # type: typing.Optional[QWidget]
- self._container = None # type: typing.Optional[QWidget]
+ self._widget: Optional[QWidget] = None
+ self._container: Optional[QWidget] = None
def addItem(self, _widget):
raise utils.Unreachable
@@ -390,10 +390,10 @@ class InspectorSplitter(QSplitter):
self.addWidget(main_webview)
self.setFocusProxy(main_webview)
self.splitterMoved.connect(self._on_splitter_moved)
- self._main_idx = None # type: typing.Optional[int]
- self._inspector_idx = None # type: typing.Optional[int]
- self._position = None # type: typing.Optional[inspector.Position]
- self._preferred_size = None # type: typing.Optional[int]
+ self._main_idx: Optional[int] = None
+ self._inspector_idx: Optional[int] = None
+ self._position: Optional[inspector.Position] = None
+ self._preferred_size: Optional[int] = None
def cycle_focus(self):
"""Cycle keyboard focus between the main/inspector widget."""
diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py
index 28a1830d5..c2e20e9ad 100644
--- a/qutebrowser/misc/objects.py
+++ b/qutebrowser/misc/objects.py
@@ -22,10 +22,10 @@
# NOTE: We need to be careful with imports here, as this is imported from
# earlyinit.
-import typing
import argparse
+from typing import TYPE_CHECKING, Any, Dict, Set, Union, cast
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.utils import usertypes
from qutebrowser.commands import command
@@ -38,11 +38,11 @@ class NoBackend:
def name(self) -> str:
raise AssertionError("No backend set!")
- def __eq__(self, other: typing.Any) -> bool:
+ def __eq__(self, other: Any) -> bool:
raise AssertionError("No backend set!")
-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)
+backend: Union['usertypes.Backend', NoBackend] = NoBackend()
+commands: Dict[str, 'command.Command'] = {}
+debug_flags: Set[str] = set()
+args = cast(argparse.Namespace, None)
diff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py
index c7f0c8072..86342d57b 100644
--- a/qutebrowser/misc/quitter.py
+++ b/qutebrowser/misc/quitter.py
@@ -25,11 +25,11 @@ import sys
import json
import atexit
import shutil
-import typing
import argparse
import tokenize
import functools
import subprocess
+from typing import Iterable, Mapping, MutableSequence, Sequence, cast
from PyQt5.QtCore import QObject, pyqtSignal, QTimer
from PyQt5.QtWidgets import QApplication
@@ -46,7 +46,7 @@ from qutebrowser.mainwindow import prompt
from qutebrowser.completion.models import miscmodels
-instance = typing.cast('Quitter', None)
+instance = cast('Quitter', None)
class Quitter(QObject):
@@ -97,10 +97,10 @@ class Quitter(QObject):
compile(f.read(), fn, 'exec')
def _get_restart_args(
- self, pages: typing.Iterable[str] = (),
+ self, pages: Iterable[str] = (),
session: str = None,
- override_args: typing.Mapping[str, str] = None
- ) -> typing.Sequence[str]:
+ override_args: Mapping[str, str] = None
+ ) -> Sequence[str]:
"""Get args to relaunch qutebrowser.
Args:
@@ -120,7 +120,7 @@ class Quitter(QObject):
args = [sys.executable, '-m', 'qutebrowser']
# Add all open pages so they get reopened.
- page_args = [] # type: typing.MutableSequence[str]
+ page_args: MutableSequence[str] = []
for win in pages:
page_args.extend(win)
page_args.append('')
@@ -157,9 +157,9 @@ class Quitter(QObject):
return args
- def restart(self, pages: typing.Sequence[str] = (),
+ def restart(self, pages: Sequence[str] = (),
session: str = None,
- override_args: typing.Mapping[str, str] = None) -> bool:
+ override_args: Mapping[str, str] = None) -> bool:
"""Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as
diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py
index daa484934..c7a20adbb 100644
--- a/qutebrowser/misc/savemanager.py
+++ b/qutebrowser/misc/savemanager.py
@@ -21,7 +21,7 @@
import os.path
import collections
-import typing
+from typing import MutableMapping
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
@@ -112,8 +112,7 @@ class SaveManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
- self.saveables = collections.OrderedDict(
- ) # type: typing.MutableMapping[str, Saveable]
+ self.saveables: MutableMapping[str, Saveable] = collections.OrderedDict()
self._save_timer = usertypes.Timer(self, name='save-timer')
self._save_timer.timeout.connect(self.autosave)
self._set_autosave_interval()
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index 0ebb415ac..eecc1964e 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -23,9 +23,9 @@ import os
import os.path
import itertools
import urllib
-import typing
import glob
import shutil
+from typing import Any, Iterable, MutableMapping, MutableSequence, Optional, Union, cast
from PyQt5.QtCore import Qt, QUrl, QObject, QPoint, QTimer, QDateTime
from PyQt5.QtWidgets import QApplication
@@ -40,7 +40,7 @@ from qutebrowser.mainwindow import mainwindow
from qutebrowser.qt import sip
-_JsonType = typing.MutableMapping[str, typing.Any]
+_JsonType = MutableMapping[str, Any]
class Sentinel:
@@ -49,9 +49,9 @@ class Sentinel:
default = Sentinel()
-session_manager = typing.cast('SessionManager', None)
+session_manager = cast('SessionManager', None)
-ArgType = typing.Union[str, Sentinel]
+ArgType = Union[str, Sentinel]
def init(parent=None):
@@ -80,7 +80,7 @@ def init(parent=None):
session_manager = SessionManager(base_path, parent)
-def shutdown(session: typing.Optional[ArgType], last_window: bool) -> None:
+def shutdown(session: Optional[ArgType], last_window: bool) -> None:
"""Handle a shutdown by saving sessions and removing the autosave file."""
if session_manager is None:
return # type: ignore
@@ -153,7 +153,7 @@ class SessionManager(QObject):
def __init__(self, base_path, parent=None):
super().__init__(parent)
- self.current = None # type: typing.Optional[str]
+ self.current: Optional[str] = None
self._base_path = base_path
self._last_window_session = None
self.did_load = False
@@ -196,9 +196,9 @@ class SessionManager(QObject):
Return:
A dict with the saved data for this item.
"""
- data = {
+ data: _JsonType = {
'url': bytes(item.url().toEncoded()).decode('ascii'),
- } # type: _JsonType
+ }
if item.title():
data['title'] = item.title()
@@ -246,7 +246,7 @@ class SessionManager(QObject):
tab: The WebView to save.
active: Whether the tab is currently active.
"""
- data = {'history': []} # type: _JsonType
+ data: _JsonType = {'history': []}
if active:
data['active'] = True
for idx, item in enumerate(tab.history):
@@ -263,9 +263,9 @@ class SessionManager(QObject):
def _save_all(self, *, only_window=None, with_private=False):
"""Get a dict with data for all windows/tabs."""
- data = {'windows': []} # type: _JsonType
+ data: _JsonType = {'windows': []}
if only_window is not None:
- winlist = [only_window] # type: typing.Iterable[int]
+ winlist: Iterable[int] = [only_window]
else:
winlist = objreg.window_registry
@@ -282,7 +282,7 @@ class SessionManager(QObject):
if tabbed_browser.is_private and not with_private:
continue
- win_data = {} # type: _JsonType
+ win_data: _JsonType = {}
active_window = QApplication.instance().activeWindow()
if getattr(active_window, 'win_id', None) == win_id:
win_data['active'] = True
@@ -377,7 +377,7 @@ class SessionManager(QObject):
def _load_tab(self, new_tab, data): # noqa: C901
"""Load yaml data into a newly opened tab."""
entries = []
- lazy_load = [] # type: typing.MutableSequence[_JsonType]
+ lazy_load: MutableSequence[_JsonType] = []
# use len(data['history'])
# -> dropwhile empty if not session.lazy_session
lazy_index = len(data['history'])
@@ -436,10 +436,10 @@ class SessionManager(QObject):
orig_url = url
if histentry.get("last_visited"):
- last_visited = QDateTime.fromString(
+ last_visited: Optional[QDateTime] = QDateTime.fromString(
histentry.get("last_visited"),
Qt.ISODate,
- ) # type: typing.Optional[QDateTime]
+ )
else:
last_visited = None
diff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py
index a8d24bd12..3540d8824 100644
--- a/qutebrowser/misc/throttle.py
+++ b/qutebrowser/misc/throttle.py
@@ -19,8 +19,8 @@
"""A throttle for throttling function calls."""
-import typing
import time
+from typing import Any, Callable, Mapping, Optional, Sequence
import attr
from PyQt5.QtCore import QObject
@@ -31,8 +31,8 @@ from qutebrowser.utils import usertypes
@attr.s
class _CallArgs:
- args = attr.ib() # type: typing.Sequence[typing.Any]
- kwargs = attr.ib() # type: typing.Mapping[str, typing.Any]
+ args: Sequence[Any] = attr.ib()
+ kwargs: Mapping[str, Any] = attr.ib()
class Throttle(QObject):
@@ -45,7 +45,7 @@ class Throttle(QObject):
"""
def __init__(self,
- func: typing.Callable,
+ func: Callable,
delay_ms: int,
parent: QObject = None) -> None:
"""Constructor.
@@ -59,8 +59,8 @@ class Throttle(QObject):
super().__init__(parent)
self._delay_ms = delay_ms
self._func = func
- self._pending_call = None # type: typing.Optional[_CallArgs]
- self._last_call_ms = None # type: typing.Optional[int]
+ self._pending_call: Optional[_CallArgs] = None
+ self._last_call_ms: Optional[int] = None
self._timer = usertypes.Timer(self, 'throttle-timer')
self._timer.setSingleShot(True)
@@ -71,7 +71,7 @@ class Throttle(QObject):
self._pending_call = None
self._last_call_ms = int(time.monotonic() * 1000)
- def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
cur_time_ms = int(time.monotonic() * 1000)
if self._pending_call is None:
if (self._last_call_ms is None or
diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py
index 8c2462b2b..56138c798 100644
--- a/qutebrowser/misc/utilcmds.py
+++ b/qutebrowser/misc/utilcmds.py
@@ -24,7 +24,7 @@
import functools
import os
import traceback
-import typing
+from typing import Optional
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
@@ -274,7 +274,7 @@ def version(win_id: int, paste: bool = False) -> None:
pastebin_version()
-_keytester_widget = None # type: typing.Optional[miscwidgets.KeyTesterWidget]
+_keytester_widget: Optional[miscwidgets.KeyTesterWidget] = None
@cmdutils.register(debug=True)
diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py
index 93c38d841..fb2776376 100644
--- a/qutebrowser/qutebrowser.py
+++ b/qutebrowser/qutebrowser.py
@@ -170,12 +170,14 @@ def debug_flag_error(flag):
log-scroll-pos: Log all scrolling changes.
stack: Enable Chromium stack logging.
chromium: Enable Chromium logging.
+ wait-renderer-process: Wait for debugger in renderer process.
+ avoid-chromium-init: Enable `--version` without initializing Chromium.
werror: Turn Python warnings into errors.
"""
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',
'no-scroll-filtering', 'log-requests', 'log-cookies',
'lost-focusproxy', 'log-scroll-pos', 'stack', 'chromium',
- 'werror']
+ 'wait-renderer-process', 'avoid-chromium-init', 'werror']
if flag in valid_flags:
return flag
diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py
index 66cfeed9e..cd2edc39a 100644
--- a/qutebrowser/utils/debug.py
+++ b/qutebrowser/utils/debug.py
@@ -24,22 +24,23 @@ import inspect
import logging
import functools
import datetime
-import typing
import types
+from typing import (
+ Any, Callable, List, Mapping, MutableSequence, Optional, Sequence, Type, Union)
-from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtSignal
+from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSignal
from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import log, utils, qtutils, objreg
from qutebrowser.qt import sip
-def log_events(klass: typing.Type) -> typing.Type:
+def log_events(klass: Type) -> Type:
"""Class decorator to log Qt events."""
old_event = klass.event
@functools.wraps(old_event)
- def new_event(self: typing.Any, e: QEvent) -> bool:
+ def new_event(self: Any, e: QEvent) -> bool:
"""Wrapper for event() which logs events."""
log.misc.debug("Event in {}: {}".format(utils.qualname(klass),
qenum_key(QEvent, e.type())))
@@ -54,7 +55,7 @@ def log_signals(obj: QObject) -> QObject:
Can be used as class decorator.
"""
- def log_slot(obj: QObject, signal: pyqtSignal, *args: typing.Any) -> None:
+ def log_slot(obj: QObject, signal: pyqtBoundSignal, *args: Any) -> None:
"""Slot connected to a signal to log it."""
dbg = dbg_signal(signal, args)
try:
@@ -83,9 +84,7 @@ def log_signals(obj: QObject) -> QObject:
old_init = obj.__init__ # type: ignore[misc]
@functools.wraps(old_init)
- def new_init(self: typing.Any,
- *args: typing.Any,
- **kwargs: typing.Any) -> None:
+ def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
"""Wrapper for __init__() which logs signals."""
old_init(self, *args, **kwargs)
connect_log_slot(self)
@@ -97,10 +96,10 @@ def log_signals(obj: QObject) -> QObject:
return obj
-def qenum_key(base: typing.Type,
- value: typing.Union[int, sip.simplewrapper],
+def qenum_key(base: Type,
+ value: Union[int, sip.simplewrapper],
add_base: bool = False,
- klass: typing.Type = None) -> str:
+ klass: Type = None) -> str:
"""Convert a Qt Enum value to its key as a string.
Args:
@@ -140,10 +139,10 @@ def qenum_key(base: typing.Type,
return ret
-def qflags_key(base: typing.Type,
- value: typing.Union[int, sip.simplewrapper],
+def qflags_key(base: Type,
+ value: Union[int, sip.simplewrapper],
add_base: bool = False,
- klass: typing.Type = None) -> str:
+ klass: 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
@@ -188,7 +187,7 @@ def qflags_key(base: typing.Type,
return '|'.join(names)
-def signal_name(sig: pyqtSignal) -> str:
+def signal_name(sig: pyqtBoundSignal) -> str:
"""Get a cleaned up name of a signal.
Unfortunately, the way to get the name of a signal differs based on:
@@ -199,7 +198,7 @@ def signal_name(sig: pyqtSignal) -> str:
fails, extract it from the repr().
Args:
- sig: The pyqtSignal
+ sig: A bound signal.
Return:
The cleaned up signal name.
@@ -209,8 +208,7 @@ def signal_name(sig: pyqtSignal) -> str:
# Examples:
# sig.signal == '2signal1'
# sig.signal == '2signal2(QString,QString)'
- m = re.fullmatch(r'[0-9]+(?P<name>.*)\(.*\)',
- sig.signal) # type: ignore[attr-defined]
+ m = re.fullmatch(r'[0-9]+(?P<name>.*)\(.*\)', sig.signal)
elif hasattr(sig, 'signatures'):
# Unbound signal, PyQt >= 5.11
# Examples:
@@ -238,8 +236,7 @@ def signal_name(sig: pyqtSignal) -> str:
return m.group('name')
-def format_args(args: typing.Sequence = None,
- kwargs: typing.Mapping = None) -> str:
+def format_args(args: Sequence = None, kwargs: 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]
@@ -251,11 +248,11 @@ def format_args(args: typing.Sequence = None,
return ', '.join(arglist)
-def dbg_signal(sig: pyqtSignal, args: typing.Any) -> str:
+def dbg_signal(sig: pyqtBoundSignal, args: Any) -> str:
"""Get a string representation of a signal for debugging.
Args:
- sig: A pyqtSignal.
+ sig: A bound signal.
args: The arguments as list of strings.
Return:
@@ -264,9 +261,9 @@ def dbg_signal(sig: pyqtSignal, args: typing.Any) -> str:
return '{}({})'.format(signal_name(sig), format_args(args))
-def format_call(func: typing.Callable,
- args: typing.Sequence = None,
- kwargs: typing.Mapping = None,
+def format_call(func: Callable,
+ args: Sequence = None,
+ kwargs: Mapping = None,
full: bool = True) -> str:
"""Get a string representation of a function calls with the given args.
@@ -293,7 +290,7 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
Usable as context manager or as decorator.
"""
- def __init__(self, logger: typing.Union[logging.Logger, str],
+ def __init__(self, logger: Union[logging.Logger, str],
action: str = 'operation') -> None:
"""Constructor.
@@ -305,28 +302,25 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
self._logger = logging.getLogger(logger)
else:
self._logger = logger
- self._started = None # type: typing.Optional[datetime.datetime]
+ self._started: Optional[datetime.datetime] = None
self._action = action
def __enter__(self) -> None:
self._started = datetime.datetime.now()
- # The string annotation is a WORKAROUND for a Python 3.5.2 bug:
- # https://github.com/python/typing/issues/266
-
def __exit__(self,
- _exc_type: 'typing.Optional[typing.Type[BaseException]]',
- _exc_val: typing.Optional[BaseException],
- _exc_tb: typing.Optional[types.TracebackType]) -> None:
+ _exc_type: Optional[Type[BaseException]],
+ _exc_val: Optional[BaseException],
+ _exc_tb: Optional[types.TracebackType]) -> None:
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))
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
@functools.wraps(func)
- def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
"""Call the original function."""
with self:
return func(*args, **kwargs)
@@ -334,14 +328,14 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
return wrapped
-def _get_widgets() -> typing.Sequence[str]:
+def _get_widgets() -> 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: typing.MutableSequence[str],
+def _get_pyqt_objects(lines: MutableSequence[str],
obj: QObject,
depth: int = 0) -> None:
"""Recursive method for get_all_objects to get Qt objects."""
@@ -362,7 +356,7 @@ def get_all_objects(start_obj: QObject = None) -> str:
if start_obj is None:
start_obj = QApplication.instance()
- pyqt_lines = [] # type: typing.List[str]
+ pyqt_lines: 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 0ef971dfc..5a3a0d263 100644
--- a/qutebrowser/utils/docutils.py
+++ b/qutebrowser/utils/docutils.py
@@ -25,7 +25,7 @@ import inspect
import os.path
import collections
import enum
-import typing
+from typing import Callable, Dict, Optional, List, Union
import qutebrowser
from qutebrowser.utils import log, utils
@@ -77,21 +77,28 @@ class DocstringParser:
arg_descs: A dict of argument names to their descriptions
"""
- State = enum.Enum('State', ['short', 'desc', 'desc_hidden',
- 'arg_start', 'arg_inside', 'misc'])
+ class State(enum.Enum):
- def __init__(self, func: typing.Callable) -> None:
+ """The current state of the parser."""
+
+ short = enum.auto()
+ desc = enum.auto()
+ desc_hidden = enum.auto()
+ arg_start = enum.auto()
+ arg_inside = enum.auto()
+ misc = enum.auto()
+
+ def __init__(self, func: Callable) -> None:
"""Constructor.
Args:
func: The function to parse the docstring for.
"""
self._state = self.State.short
- 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]]]
+ self._cur_arg_name: Optional[str] = None
+ self._short_desc_parts: List[str] = []
+ self._long_desc_parts: List[str] = []
+ self.arg_descs: Dict[str, Union[str, List[str]]] = collections.OrderedDict()
doc = inspect.getdoc(func)
handlers = {
self.State.short: self._parse_short,
diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py
index 1b499a377..94068c640 100644
--- a/qutebrowser/utils/javascript.py
+++ b/qutebrowser/utils/javascript.py
@@ -19,10 +19,10 @@
"""Utilities related to javascript interaction."""
-import typing
+from typing import Sequence, Union
-_InnerJsArgType = typing.Union[None, str, bool, int, float]
-_JsArgType = typing.Union[_InnerJsArgType, typing.Sequence[_InnerJsArgType]]
+_InnerJsArgType = Union[None, str, bool, int, float]
+_JsArgType = Union[_InnerJsArgType, Sequence[_InnerJsArgType]]
def string_escape(text: str) -> str:
diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py
index 78663645d..f60c46bbc 100644
--- a/qutebrowser/utils/jinja.py
+++ b/qutebrowser/utils/jinja.py
@@ -21,10 +21,10 @@
import os
import os.path
-import typing
import functools
import contextlib
import html
+from typing import Any, Callable, FrozenSet, Iterator, List, Set, Tuple
import jinja2
import jinja2.nodes
@@ -68,7 +68,7 @@ class Loader(jinja2.BaseLoader):
self,
_env: jinja2.Environment,
template: str
- ) -> typing.Tuple[str, str, typing.Callable[[], bool]]:
+ ) -> Tuple[str, str, Callable[[], bool]]:
path = os.path.join(self._subdir, template)
try:
source = utils.read_file(path)
@@ -98,7 +98,7 @@ class Environment(jinja2.Environment):
self._autoescape = True
@contextlib.contextmanager
- def no_autoescape(self) -> typing.Iterator[None]:
+ def no_autoescape(self) -> Iterator[None]:
"""Context manager to temporarily turn off autoescaping."""
self._autoescape = False
yield
@@ -122,7 +122,7 @@ class Environment(jinja2.Environment):
mimetype = utils.guess_mimetype(filename)
return urlutils.data_url(mimetype, data).toString()
- def getattr(self, obj: typing.Any, attribute: str) -> typing.Any:
+ def getattr(self, obj: Any, attribute: str) -> Any:
"""Override jinja's getattr() to be less clever.
This means it doesn't fall back to __getitem__, and it doesn't hide
@@ -131,7 +131,7 @@ class Environment(jinja2.Environment):
return getattr(obj, attribute)
-def render(template: str, **kwargs: typing.Any) -> str:
+def render(template: str, **kwargs: Any) -> str:
"""Render the given template and pass the given arguments to it."""
return environment.get_template(template).render(**kwargs)
@@ -142,10 +142,10 @@ js_environment = jinja2.Environment(loader=Loader('javascript'))
@debugcachestats.register()
@functools.lru_cache()
-def template_config_variables(template: str) -> typing.FrozenSet[str]:
+def template_config_variables(template: str) -> FrozenSet[str]:
"""Return the config variables used in the template."""
unvisted_nodes = [environment.parse(template)]
- result = set() # type: typing.Set[str]
+ result: Set[str] = set()
while unvisted_nodes:
node = unvisted_nodes.pop()
if not isinstance(node, jinja2.nodes.Getattr):
@@ -154,7 +154,7 @@ def template_config_variables(template: str) -> typing.FrozenSet[str]:
# List of attribute names in reverse order.
# For example it's ['ab', 'c', 'd'] for 'conf.d.c.ab'.
- attrlist = [] # type: typing.List[str]
+ attrlist: List[str] = []
while isinstance(node, jinja2.nodes.Getattr):
attrlist.append(node.attr) # type: ignore[attr-defined]
node = node.node # type: ignore[attr-defined]
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 165e5143f..d8378788c 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -31,8 +31,9 @@ import traceback
import warnings
import json
import inspect
-import typing
import argparse
+from typing import (TYPE_CHECKING, Any, Iterator, Mapping, MutableSequence,
+ Optional, Set, Tuple, Union, cast)
from PyQt5 import QtCore
# Optional imports
@@ -41,7 +42,7 @@ try:
except ImportError:
colorama = None
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.config import config as configmodule
_log_inited = False
@@ -98,8 +99,8 @@ LOG_LEVELS = {
def vdebug(self: logging.Logger,
msg: str,
- *args: typing.Any,
- **kwargs: typing.Any) -> None:
+ *args: Any,
+ **kwargs: Any) -> None:
"""Log with a VDEBUG level.
VDEBUG is used when a debug message is rather verbose, and probably of
@@ -159,8 +160,8 @@ LOGGER_NAMES = [
]
-ram_handler = None # type: typing.Optional[RAMHandler]
-console_handler = None # type: typing.Optional[logging.Handler]
+ram_handler: Optional['RAMHandler'] = None
+console_handler: Optional[logging.Handler] = None
console_filter = None
@@ -233,7 +234,7 @@ def _init_py_warnings() -> None:
@contextlib.contextmanager
-def disable_qt_msghandler() -> typing.Iterator[None]:
+def disable_qt_msghandler() -> Iterator[None]:
"""Contextmanager which temporarily disables the Qt message handler."""
old_handler = QtCore.qInstallMessageHandler(None)
try:
@@ -243,9 +244,9 @@ def disable_qt_msghandler() -> typing.Iterator[None]:
@contextlib.contextmanager
-def ignore_py_warnings(**kwargs: typing.Any) -> typing.Iterator[None]:
+def py_warning_filter(action: str = 'ignore', **kwargs: Any) -> Iterator[None]:
"""Contextmanager to temporarily disable certain Python warnings."""
- warnings.filterwarnings('ignore', **kwargs)
+ warnings.filterwarnings(action, **kwargs)
yield
if _log_inited:
_init_py_warnings()
@@ -257,7 +258,7 @@ def _init_handlers(
force_color: bool,
json_logging: bool,
ram_capacity: int
-) -> typing.Tuple[logging.StreamHandler, typing.Optional['RAMHandler']]:
+) -> Tuple[logging.StreamHandler, Optional['RAMHandler']]:
"""Init log handlers.
Args:
@@ -311,8 +312,8 @@ def _init_formatters(
color: bool,
force_color: bool,
json_logging: bool
-) -> typing.Tuple[typing.Union['JSONFormatter', 'ColoredFormatter'],
- 'ColoredFormatter', 'HTMLFormatter', bool]:
+) -> Tuple[Union['JSONFormatter', 'ColoredFormatter'],
+ 'ColoredFormatter', 'HTMLFormatter', bool]:
"""Init log formatters.
Args:
@@ -364,7 +365,7 @@ def change_console_formatter(level: int) -> None:
"""
assert console_handler is not None
- old_formatter = typing.cast(ColoredFormatter, console_handler.formatter)
+ old_formatter = cast(ColoredFormatter, console_handler.formatter)
console_fmt = get_console_format(level)
console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{',
use_colors=old_formatter.use_colors)
@@ -504,7 +505,7 @@ def qt_message_handler(msg_type: QtCore.QtMsgType,
assert _args is not None
if _args.debug:
- stack = ''.join(traceback.format_stack()) # type: typing.Optional[str]
+ stack: Optional[str] = ''.join(traceback.format_stack())
else:
stack = None
@@ -515,7 +516,7 @@ def qt_message_handler(msg_type: QtCore.QtMsgType,
@contextlib.contextmanager
-def hide_qt_warning(pattern: str, logger: str = 'qt') -> typing.Iterator[None]:
+def hide_qt_warning(pattern: str, logger: str = 'qt') -> Iterator[None]:
"""Hide Qt warnings matching the given regex."""
log_filter = QtWarningFilter(pattern)
logger_obj = logging.getLogger(logger)
@@ -578,7 +579,7 @@ class InvalidLogFilterError(Exception):
"""Raised when an invalid filter string is passed to LogFilter.parse()."""
- def __init__(self, names: typing.Set[str]):
+ def __init__(self, names: Set[str]):
invalid = names - set(LOGGER_NAMES)
super().__init__("Invalid log category {} - valid categories: {}"
.format(', '.join(sorted(invalid)),
@@ -599,7 +600,7 @@ class LogFilter(logging.Filter):
than debug.
"""
- def __init__(self, names: typing.Set[str], *, negated: bool = False,
+ def __init__(self, names: Set[str], *, negated: bool = False,
only_debug: bool = True) -> None:
super().__init__()
self.names = names
@@ -607,7 +608,7 @@ class LogFilter(logging.Filter):
self.only_debug = only_debug
@classmethod
- def parse(cls, filter_str: typing.Optional[str], *,
+ def parse(cls, filter_str: Optional[str], *,
only_debug: bool = True) -> 'LogFilter':
"""Parse a log filter from a string."""
if filter_str is None or filter_str == 'none':
@@ -661,11 +662,11 @@ class RAMHandler(logging.Handler):
def __init__(self, capacity: int) -> None:
super().__init__()
- self.html_formatter = None # type: typing.Optional[HTMLFormatter]
+ self.html_formatter: Optional[HTMLFormatter] = None
if capacity != -1:
- self._data = collections.deque(
+ self._data: MutableSequence[logging.LogRecord] = collections.deque(
maxlen=capacity
- ) # type: typing.MutableSequence[logging.LogRecord]
+ )
else:
self._data = collections.deque()
@@ -748,9 +749,7 @@ class HTMLFormatter(logging.Formatter):
_colordict: The colordict passed to the logger.
"""
- def __init__(self, fmt: str,
- datefmt: str,
- log_colors: typing.Mapping[str, str]) -> None:
+ def __init__(self, fmt: str, datefmt: str, log_colors: Mapping[str, str]) -> None:
"""Constructor.
Args:
@@ -759,8 +758,8 @@ class HTMLFormatter(logging.Formatter):
log_colors: The colors to use for logging levels.
"""
super().__init__(fmt, datefmt)
- self._log_colors = log_colors # type: typing.Mapping[str, str]
- self._colordict = {} # type: typing.Mapping[str, str]
+ self._log_colors: Mapping[str, str] = log_colors
+ self._colordict: Mapping[str, str] = {}
# We could solve this nicer by using CSS, but for this simple case this
# works.
for color in COLORS:
diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py
index 2754d87e7..1009f1e98 100644
--- a/qutebrowser/utils/message.py
+++ b/qutebrowser/utils/message.py
@@ -23,11 +23,11 @@
"""Message singleton so we don't have to define unneeded signals."""
import traceback
-import typing
+from typing import Any, Callable, Iterable, List, Tuple, Union
-from PyQt5.QtCore import pyqtSignal, QObject
+from PyQt5.QtCore import pyqtSignal, pyqtBoundSignal, QObject
-from qutebrowser.utils import usertypes, log, utils
+from qutebrowser.utils import usertypes, log
def _log_stack(typ: str, stack: str) -> None:
@@ -86,8 +86,8 @@ def info(message: str, *, replace: bool = False) -> None:
def _build_question(title: str,
text: str = None, *,
mode: usertypes.PromptMode,
- default: typing.Union[None, bool, str] = None,
- abort_on: typing.Iterable[pyqtSignal] = (),
+ default: Union[None, bool, str] = None,
+ abort_on: Iterable[pyqtBoundSignal] = (),
url: str = None,
option: bool = None) -> usertypes.Question:
"""Common function for ask/ask_async."""
@@ -110,7 +110,7 @@ def _build_question(title: str,
return question
-def ask(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+def ask(*args: Any, **kwargs: Any) -> Any:
"""Ask a modular question in the statusbar (blocking).
Args:
@@ -134,8 +134,8 @@ def ask(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
def ask_async(title: str,
mode: usertypes.PromptMode,
- handler: typing.Callable[[typing.Any], None],
- **kwargs: typing.Any) -> None:
+ handler: Callable[[Any], None],
+ **kwargs: Any) -> None:
"""Ask an async question in the statusbar.
Args:
@@ -151,13 +151,13 @@ def ask_async(title: str,
global_bridge.ask(question, blocking=False)
-_ActionType = typing.Callable[[], typing.Any]
+_ActionType = Callable[[], Any]
def confirm_async(*, yes_action: _ActionType,
no_action: _ActionType = None,
cancel_action: _ActionType = None,
- **kwargs: typing.Any) -> usertypes.Question:
+ **kwargs: Any) -> usertypes.Question:
"""Ask a yes/no question to the user and execute the given actions.
Args:
@@ -219,8 +219,7 @@ class GlobalMessageBridge(QObject):
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
self._connected = False
- self._cache = [
- ] # type: typing.List[typing.Tuple[usertypes.MessageLevel, str, bool]]
+ self._cache: List[Tuple[usertypes.MessageLevel, str, bool]] = []
def ask(self, question: usertypes.Question,
blocking: bool, *,
@@ -259,42 +258,4 @@ class GlobalMessageBridge(QObject):
self._cache = []
-class MessageBridge(QObject):
-
- """Bridge for messages to be shown in the statusbar.
-
- Signals:
- s_set_text: Set a persistent text in the statusbar.
- arg: The text to set.
- s_maybe_reset_text: Reset the text if it hasn't been changed yet.
- arg: The expected text.
- """
-
- s_set_text = pyqtSignal(str)
- s_maybe_reset_text = pyqtSignal(str)
-
- def __repr__(self) -> str:
- return utils.get_repr(self)
-
- def set_text(self, text: str, *, log_stack: bool = False) -> None:
- """Set the normal text of the statusbar.
-
- Args:
- text: The text to set.
- log_stack: ignored
- """
- text = str(text)
- log.message.debug(text)
- self.s_set_text.emit(text)
-
- 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:
- text: The expected text.
- log_stack: ignored
- """
- self.s_maybe_reset_text.emit(str(text))
-
-
global_bridge = GlobalMessageBridge()
diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py
index 015334990..90b70be17 100644
--- a/qutebrowser/utils/objreg.py
+++ b/qutebrowser/utils/objreg.py
@@ -22,18 +22,19 @@
import collections
import functools
-import typing
+from typing import (TYPE_CHECKING, Any, Callable, MutableMapping, MutableSequence,
+ Optional, Sequence, Union)
from PyQt5.QtCore import QObject, QTimer
from PyQt5.QtWidgets import QApplication
-from PyQt5.QtWidgets import QWidget # pylint: disable=unused-import
+from PyQt5.QtWidgets import QWidget
-from qutebrowser.utils import log, usertypes
-if typing.TYPE_CHECKING:
+from qutebrowser.utils import log, usertypes, utils
+if TYPE_CHECKING:
from qutebrowser.mainwindow import mainwindow
-_WindowTab = typing.Union[str, int, None]
+_WindowTab = Union[str, int, None]
class RegistryUnavailableError(Exception):
@@ -51,7 +52,7 @@ class CommandOnlyError(Exception):
"""Raised when an object is requested which is used for commands only."""
-_IndexType = typing.Union[str, int]
+_IndexType = Union[str, int]
class ObjectRegistry(collections.UserDict):
@@ -67,11 +68,10 @@ class ObjectRegistry(collections.UserDict):
def __init__(self) -> None:
super().__init__()
- self._partial_objs = {
- } # type: typing.MutableMapping[_IndexType, typing.Callable[[], None]]
- self.command_only = [] # type: typing.MutableSequence[str]
+ self._partial_objs: MutableMapping[_IndexType, Callable[[], None]] = {}
+ self.command_only: MutableSequence[str] = []
- def __setitem__(self, name: _IndexType, obj: typing.Any) -> None:
+ def __setitem__(self, name: _IndexType, obj: Any) -> None:
"""Register an object in the object registry.
Sets a slot to remove QObjects when they are destroyed.
@@ -139,7 +139,7 @@ class ObjectRegistry(collections.UserDict):
except KeyError:
pass
- def dump_objects(self) -> typing.Sequence[str]:
+ def dump_objects(self) -> Sequence[str]:
"""Dump all objects as a list of strings."""
lines = []
for name, obj in self.data.items():
@@ -166,7 +166,7 @@ def _get_tab_registry(win_id: _WindowTab,
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:
- window = QApplication.activeWindow() # type: typing.Optional[QWidget]
+ window: Optional[QWidget] = QApplication.activeWindow()
if window is None or not hasattr(window, 'win_id'):
raise RegistryUnavailableError('tab')
win_id = window.win_id
@@ -192,7 +192,7 @@ def _get_window_registry(window: _WindowTab) -> ObjectRegistry:
raise TypeError("window is None with scope window!")
try:
if window == 'current':
- win = QApplication.activeWindow() # type: typing.Optional[QWidget]
+ win: Optional[QWidget] = QApplication.activeWindow()
elif window == 'last-focused':
win = last_focused_window()
else:
@@ -228,11 +228,11 @@ def _get_registry(scope: str,
def get(name: str,
- default: typing.Any = usertypes.UNSET,
+ default: Any = usertypes.UNSET,
scope: str = 'global',
window: _WindowTab = None,
tab: _WindowTab = None,
- from_command: bool = False) -> typing.Any:
+ from_command: bool = False) -> Any:
"""Helper function to get an object.
Args:
@@ -253,7 +253,7 @@ def get(name: str,
def register(name: str,
- obj: typing.Any,
+ obj: Any,
update: bool = False,
scope: str = None,
registry: ObjectRegistry = None,
@@ -296,7 +296,7 @@ def delete(name: str,
del reg[name]
-def dump_objects() -> typing.Sequence[str]:
+def dump_objects() -> Sequence[str]:
"""Get all registered objects in all registries as a string."""
blocks = []
lines = []
@@ -321,22 +321,46 @@ def dump_objects() -> typing.Sequence[str]:
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')
+ window = get('last-visible-main-window')
except KeyError:
return last_focused_window()
+ if window.tabbed_browser.is_shutting_down:
+ return last_focused_window()
+ return 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')
+ window = get('last-focused-main-window')
except KeyError:
- return window_by_index(-1)
+ return last_opened_window()
+ if window.tabbed_browser.is_shutting_down:
+ return last_opened_window()
+ return window
-def window_by_index(idx: int) -> 'mainwindow.MainWindow':
+def _window_by_index(idx: int) -> 'mainwindow.MainWindow':
"""Get the Nth opened window object."""
if not window_registry:
raise NoWindow()
key = sorted(window_registry)[idx]
return window_registry[key]
+
+
+def last_opened_window() -> 'mainwindow.MainWindow':
+ """Get the last opened window object."""
+ for idx in range(-1, -(len(window_registry)+1), -1):
+ window = _window_by_index(idx)
+ if not window.tabbed_browser.is_shutting_down:
+ return window
+ raise utils.Unreachable()
+
+
+def first_opened_window() -> 'mainwindow.MainWindow':
+ """Get the first opened window object."""
+ for idx in range(0, len(window_registry)+1):
+ window = _window_by_index(idx)
+ if not window.tabbed_browser.is_shutting_down:
+ return window
+ raise utils.Unreachable()
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index e853b38f8..c92827f99 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -31,7 +31,7 @@ Module attributes:
import io
import operator
import contextlib
-import typing
+from typing import TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, cast
import pkg_resources
from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
@@ -43,6 +43,9 @@ try:
from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover
qWebKitVersion = None # type: ignore[assignment] # noqa: N816
+if TYPE_CHECKING:
+ from PyQt5.QtWebKit import QWebHistory
+ from PyQt5.QtWebEngineWidgets import QWebEngineHistory
from qutebrowser.misc import objects
from qutebrowser.utils import usertypes
@@ -71,7 +74,7 @@ class QtOSError(OSError):
if msg is None:
msg = dev.errorString()
- self.qt_errno = None # type: typing.Optional[QFileDevice.FileError]
+ self.qt_errno: Optional[QFileDevice.FileError] = None
if isinstance(dev, QFileDevice):
msg = self._init_filedev(dev, msg)
@@ -155,8 +158,11 @@ def check_overflow(arg: int, ctype: str, fatal: bool = True) -> int:
return arg
-if typing.TYPE_CHECKING:
- class Validatable(typing.Protocol):
+if TYPE_CHECKING:
+ # Protocol was added in Python 3.8
+ from typing import Protocol
+
+ class Validatable(Protocol):
"""An object with an isValid() method (e.g. QUrl)."""
@@ -184,7 +190,13 @@ def check_qdatastream(stream: QDataStream) -> None:
raise OSError(status_to_str[stream.status()])
-_QtSerializableType = typing.Union[QObject, QByteArray, QUrl]
+_QtSerializableType = Union[
+ QObject,
+ QByteArray,
+ QUrl,
+ 'QWebEngineHistory',
+ 'QWebHistory'
+]
def serialize(obj: _QtSerializableType) -> QByteArray:
@@ -222,7 +234,7 @@ def savefile_open(
filename: str,
binary: bool = False,
encoding: str = 'utf-8'
-) -> typing.Iterator[typing.IO]:
+) -> Iterator[IO]:
"""Context manager to easily use a QSaveFile."""
f = QSaveFile(filename)
cancelled = False
@@ -231,10 +243,10 @@ def savefile_open(
if not open_ok:
raise QtOSError(f)
- dev = typing.cast(typing.BinaryIO, PyQIODevice(f))
+ dev = cast(BinaryIO, PyQIODevice(f))
if binary:
- new_f = dev # type: typing.IO
+ new_f: IO = dev
else:
new_f = io.TextIOWrapper(dev, encoding=encoding)
@@ -352,21 +364,21 @@ class PyQIODevice(io.BufferedIOBase):
def readable(self) -> bool:
return self.dev.isReadable()
- def readline(self, size: int = -1) -> bytes:
+ def readline(self, size: Optional[int] = -1) -> bytes:
self._check_open()
self._check_readable()
- if size < 0:
+ if size is None or size < 0:
qt_size = 0 # no maximum size
elif size == 0:
return b''
else:
qt_size = size + 1 # Qt also counts the NUL byte
- buf = None # type: typing.Union[QByteArray, bytes, None]
+ buf: Union[QByteArray, bytes, None] = None
if self.dev.canReadLine():
buf = self.dev.readLine(qt_size)
- elif size < 0:
+ elif size is None or size < 0:
buf = self.dev.readAll()
else:
buf = self.dev.read(size)
@@ -392,7 +404,10 @@ class PyQIODevice(io.BufferedIOBase):
def writable(self) -> bool:
return self.dev.isWritable()
- def write(self, data: typing.Union[bytes, bytearray]) -> int:
+ def write( # type: ignore[override]
+ self,
+ data: Union[bytes, bytearray]
+ ) -> int:
self._check_open()
self._check_writable()
num = self.dev.write(data)
@@ -400,11 +415,11 @@ class PyQIODevice(io.BufferedIOBase):
raise QtOSError(self.dev)
return num
- def read(self, size: typing.Optional[int] = None) -> bytes:
+ def read(self, size: Optional[int] = None) -> bytes:
self._check_open()
self._check_readable()
- buf = None # type: typing.Union[QByteArray, bytes, None]
+ buf: Union[QByteArray, bytes, None] = None
if size in [None, -1]:
buf = self.dev.readAll()
else:
@@ -451,7 +466,7 @@ class EventLoop(QEventLoop):
def exec_(
self,
flags: QEventLoop.ProcessEventsFlags =
- typing.cast(QEventLoop.ProcessEventsFlags, QEventLoop.AllEvents)
+ cast(QEventLoop.ProcessEventsFlags, QEventLoop.AllEvents)
) -> int:
"""Override exec_ to raise an exception when re-running."""
if self._executing:
diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py
index 8e5a91c30..a86fd9bdc 100644
--- a/qutebrowser/utils/standarddir.py
+++ b/qutebrowser/utils/standarddir.py
@@ -26,7 +26,7 @@ import shutil
import contextlib
import enum
import argparse
-import typing
+from typing import Iterator, Optional
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
@@ -41,14 +41,14 @@ class _Location(enum.Enum):
"""A key for _locations."""
- config = 1
- auto_config = 2
- data = 3
- system_data = 4
- cache = 5
- download = 6
- runtime = 7
- config_py = 8
+ config = enum.auto()
+ auto_config = enum.auto()
+ data = enum.auto()
+ system_data = enum.auto()
+ cache = enum.auto()
+ download = enum.auto()
+ runtime = enum.auto()
+ config_py = enum.auto()
APPNAME = 'qutebrowser'
@@ -60,7 +60,7 @@ class EmptyValueError(Exception):
@contextlib.contextmanager
-def _unset_organization() -> typing.Iterator[None]:
+def _unset_organization() -> Iterator[None]:
"""Temporarily unset QApplication.organizationName().
This is primarily needed in config.py.
@@ -76,7 +76,7 @@ def _unset_organization() -> typing.Iterator[None]:
qapp.setOrganizationName(orgname)
-def _init_config(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_config(args: Optional[argparse.Namespace]) -> None:
"""Initialize the location for configs."""
typ = QStandardPaths.ConfigLocation
path = _from_args(typ, args)
@@ -127,7 +127,7 @@ def config_py() -> str:
return _locations[_Location.config_py]
-def _init_data(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_data(args: Optional[argparse.Namespace]) -> None:
"""Initialize the location for data."""
typ = QStandardPaths.DataLocation
path = _from_args(typ, args)
@@ -167,7 +167,7 @@ def data(system: bool = False) -> str:
return _locations[_Location.data]
-def _init_cache(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_cache(args: Optional[argparse.Namespace]) -> None:
"""Initialize the location for the cache."""
typ = QStandardPaths.CacheLocation
path = _from_args(typ, args)
@@ -187,7 +187,7 @@ def cache() -> str:
return _locations[_Location.cache]
-def _init_download(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_download(args: Optional[argparse.Namespace]) -> None:
"""Initialize the location for downloads.
Note this is only the default directory as found by Qt.
@@ -204,7 +204,7 @@ def download() -> str:
return _locations[_Location.download]
-def _init_runtime(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_runtime(args: 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.
@@ -279,8 +279,8 @@ def _writable_location(typ: QStandardPaths.StandardLocation) -> str:
def _from_args(
typ: QStandardPaths.StandardLocation,
- args: typing.Optional[argparse.Namespace]
-) -> typing.Optional[str]:
+ args: Optional[argparse.Namespace]
+) -> Optional[str]:
"""Get the standard directory from an argparse namespace.
Return:
@@ -332,7 +332,7 @@ def _init_dirs(args: argparse.Namespace = None) -> None:
_init_runtime(args)
-def init(args: typing.Optional[argparse.Namespace]) -> None:
+def init(args: Optional[argparse.Namespace]) -> None:
"""Initialize all standard dirs."""
if args is not None:
# args can be None during tests
diff --git a/qutebrowser/utils/urlmatch.py b/qutebrowser/utils/urlmatch.py
index 503436ef8..e4e4984db 100644
--- a/qutebrowser/utils/urlmatch.py
+++ b/qutebrowser/utils/urlmatch.py
@@ -25,14 +25,14 @@ https://cs.chromium.org/chromium/src/extensions/common/url_pattern.cc
https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h
Based on the following commit in Chromium:
-https://chromium.googlesource.com/chromium/src/+/757854e199e159523e7789de5cb2f6ba49b79b63
-(February 4 2020, newest commit as per July 1st 2020)
+https://chromium.googlesource.com/chromium/src/+/6f4a6681eae01c2036336c18b06303e16a304a7c
+(October 10 2020, newest commit as per October 28th 2020)
"""
import ipaddress
import fnmatch
-import typing
import urllib.parse
+from typing import Any, Optional, Tuple
from PyQt5.QtCore import QUrl
@@ -73,11 +73,11 @@ class UrlPattern:
# Make sure all attributes are initialized if we exit early.
self._pattern = pattern
self._match_all = False
- self._match_subdomains = False # type: bool
- 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]
+ self._match_subdomains: bool = False
+ self._scheme: Optional[str] = None
+ self.host: Optional[str] = None
+ self._path: Optional[str] = None
+ self._port: Optional[int] = None
# > The special pattern <all_urls> matches any URL that starts with a
# > permitted scheme.
@@ -104,7 +104,7 @@ class UrlPattern:
self._init_path(parsed)
self._init_port(parsed)
- def _to_tuple(self) -> typing.Tuple:
+ def _to_tuple(self) -> 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)
@@ -112,7 +112,7 @@ class UrlPattern:
def __hash__(self) -> int:
return hash(self._to_tuple())
- def __eq__(self, other: typing.Any) -> bool:
+ def __eq__(self, other: Any) -> bool:
if not isinstance(other, UrlPattern):
return NotImplemented
return self._to_tuple() == other._to_tuple()
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index a14be78a8..598210010 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -25,7 +25,7 @@ import os.path
import ipaddress
import posixpath
import urllib.parse
-import typing
+from typing import Optional, Tuple, Union
from PyQt5.QtCore import QUrl, QUrlQuery
from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy
@@ -72,8 +72,7 @@ class InvalidUrlError(Error):
super().__init__(self.msg)
-def _parse_search_term(s: str) -> typing.Tuple[typing.Optional[str],
- typing.Optional[str]]:
+def _parse_search_term(s: str) -> Tuple[Optional[str], Optional[str]]:
"""Get a search engine name and search term from a string.
Args:
@@ -89,8 +88,8 @@ def _parse_search_term(s: str) -> typing.Tuple[typing.Optional[str],
if len(split) == 2:
if split[0] in config.val.url.searchengines:
- engine = split[0] # type: typing.Optional[str]
- term = split[1] # type: typing.Optional[str]
+ engine: Optional[str] = split[0]
+ term: Optional[str] = split[1]
else:
engine = None
term = s
@@ -385,7 +384,7 @@ def raise_cmdexc_if_invalid(url: QUrl) -> None:
def get_path_if_valid(pathstr: str,
cwd: str = None,
relative: bool = False,
- check_exists: bool = False) -> typing.Optional[str]:
+ check_exists: bool = False) -> Optional[str]:
"""Check if path is a valid path.
Args:
@@ -403,7 +402,7 @@ def get_path_if_valid(pathstr: str,
expanded = os.path.expanduser(pathstr)
if os.path.isabs(expanded):
- path = expanded # type: typing.Optional[str]
+ path: Optional[str] = expanded
elif relative and cwd:
path = os.path.join(cwd, expanded)
elif relative:
@@ -430,7 +429,7 @@ def get_path_if_valid(pathstr: str,
return path
-def filename_from_url(url: QUrl) -> typing.Optional[str]:
+def filename_from_url(url: QUrl) -> Optional[str]:
"""Get a suitable filename from a URL.
Args:
@@ -450,7 +449,7 @@ def filename_from_url(url: QUrl) -> typing.Optional[str]:
return None
-HostTupleType = typing.Tuple[str, str, int]
+HostTupleType = Tuple[str, str, int]
def host_tuple(url: QUrl) -> HostTupleType:
@@ -594,7 +593,7 @@ class InvalidProxyTypeError(Exception):
super().__init__("Invalid proxy type {}!".format(typ))
-def proxy_from_url(url: QUrl) -> typing.Union[QNetworkProxy, pac.PACFetcher]:
+def proxy_from_url(url: QUrl) -> Union[QNetworkProxy, pac.PACFetcher]:
"""Create a QNetworkProxy from QUrl and a proxy type.
Args:
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index 0b6f9c219..9ecae9e92 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -21,7 +21,7 @@
import operator
import enum
-import typing
+from typing import TYPE_CHECKING, Any, Optional, Sequence, TypeVar, Union
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
@@ -30,7 +30,19 @@ from PyQt5.QtCore import QUrl
from qutebrowser.utils import log, qtutils, utils
-_T = typing.TypeVar('_T')
+if TYPE_CHECKING:
+ # Protocol was added in Python 3.8
+ from typing import Protocol
+
+ class SupportsLessThan(Protocol):
+
+ """Protocol for the _T TypeVar below."""
+
+ def __lt__(self, other: Any) -> bool:
+ ...
+
+
+_T = TypeVar('_T', bound='SupportsLessThan')
class Unset:
@@ -46,7 +58,7 @@ class Unset:
UNSET = Unset()
-class NeighborList(typing.Sequence[_T]):
+class NeighborList(Sequence[_T]):
"""A list of items which saves its current position.
@@ -60,10 +72,15 @@ class NeighborList(typing.Sequence[_T]):
_mode: The current mode.
"""
- Modes = enum.Enum('Modes', ['edge', 'exception'])
+ class Modes(enum.Enum):
+
+ """Behavior for the 'mode' argument."""
+
+ edge = enum.auto()
+ exception = enum.auto()
- def __init__(self, items: typing.Sequence[_T] = None,
- default: typing.Union[_T, Unset] = UNSET,
+ def __init__(self, items: Sequence[_T] = None,
+ default: Union[_T, Unset] = UNSET,
mode: Modes = Modes.exception) -> None:
"""Constructor.
@@ -77,19 +94,19 @@ class NeighborList(typing.Sequence[_T]):
if not isinstance(mode, self.Modes):
raise TypeError("Mode {} is not a Modes member!".format(mode))
if items is None:
- self._items = [] # type: typing.Sequence[_T]
+ self._items: Sequence[_T] = []
else:
self._items = list(items)
self._default = default
if not isinstance(default, Unset):
idx = self._items.index(default)
- self._idx = idx # type: typing.Optional[int]
+ self._idx: Optional[int] = idx
else:
self._idx = None
self._mode = mode
- self.fuzzyval = None # type: typing.Optional[int]
+ self.fuzzyval: Optional[int] = None
def __getitem__(self, key: int) -> _T: # type: ignore[override]
return self._items[key]
@@ -158,7 +175,7 @@ class NeighborList(typing.Sequence[_T]):
return new
@property
- def items(self) -> typing.Sequence[_T]:
+ def items(self) -> Sequence[_T]:
"""Getter for items, which should not be set."""
return self._items
@@ -224,42 +241,48 @@ class NeighborList(typing.Sequence[_T]):
return self.curitem()
-# The mode of a Question.
-PromptMode = enum.Enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
- 'download'])
+class PromptMode(enum.Enum):
+
+ """The mode of a Question."""
+
+ yesno = enum.auto()
+ text = enum.auto()
+ user_pwd = enum.auto()
+ alert = enum.auto()
+ download = enum.auto()
class ClickTarget(enum.Enum):
"""How to open a clicked link."""
- normal = 0 #: Open the link in the current tab
- tab = 1 #: Open the link in a new foreground tab
- tab_bg = 2 #: Open the link in a new background tab
- window = 3 #: Open the link in a new window
- hover = 4 #: Only hover over the link
+ normal = enum.auto() #: Open the link in the current tab
+ tab = enum.auto() #: Open the link in a new foreground tab
+ tab_bg = enum.auto() #: Open the link in a new background tab
+ window = enum.auto() #: Open the link in a new window
+ hover = enum.auto() #: Only hover over the link
class KeyMode(enum.Enum):
"""Key input modes."""
- normal = 1 #: Normal mode (no mode was entered)
- hint = 2 #: Hint mode (showing labels for links)
- command = 3 #: Command mode (after pressing the colon key)
- yesno = 4 #: Yes/No prompts
- prompt = 5 #: Text prompts
- insert = 6 #: Insert mode (passing through most keys)
- passthrough = 7 #: Passthrough mode (passing through all keys)
- caret = 8 #: Caret mode (moving cursor with keys)
- set_mark = 9
- jump_mark = 10
- record_macro = 11
- run_macro = 12
+ normal = enum.auto() #: Normal mode (no mode was entered)
+ hint = enum.auto() #: Hint mode (showing labels for links)
+ command = enum.auto() #: Command mode (after pressing the colon key)
+ yesno = enum.auto() #: Yes/No prompts
+ prompt = enum.auto() #: Text prompts
+ insert = enum.auto() #: Insert mode (passing through most keys)
+ passthrough = enum.auto() #: Passthrough mode (passing through all keys)
+ caret = enum.auto() #: Caret mode (moving cursor with keys)
+ set_mark = enum.auto()
+ jump_mark = enum.auto()
+ record_macro = enum.auto()
+ run_macro = enum.auto()
# 'register' is a bit of an oddball here: It's not really a "real" mode,
# but it's used in the config for common bindings for
# set_mark/jump_mark/record_macro/run_macro.
- register = 13
+ register = enum.auto()
class Exit(enum.IntEnum):
@@ -273,44 +296,76 @@ class Exit(enum.IntEnum):
err_init = 4
-# Load status of a tab
-LoadStatus = enum.Enum('LoadStatus', ['none', 'success', 'success_https',
- 'error', 'warn', 'loading'])
+class LoadStatus(enum.Enum):
+
+ """Load status of a tab."""
+
+ none = enum.auto()
+ success = enum.auto()
+ success_https = enum.auto()
+ error = enum.auto()
+ warn = enum.auto()
+ loading = enum.auto()
+
+
+class Backend(enum.Enum):
+ """The backend being used (usertypes.backend)."""
-# Backend of a tab
-Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine'])
+ QtWebKit = enum.auto()
+ QtWebEngine = enum.auto()
class JsWorld(enum.Enum):
"""World/context to run JavaScript code in."""
- main = 1 #: Same world as the web page's JavaScript.
- application = 2 #: Application world, used by qutebrowser internally.
- user = 3 #: User world, currently not used.
- jseval = 4 #: World used for the jseval-command.
+ main = enum.auto() #: Same world as the web page's JavaScript.
+ application = enum.auto() #: Application world, used by qutebrowser internally.
+ user = enum.auto() #: User world, currently not used.
+ jseval = enum.auto() #: World used for the jseval-command.
-# Log level of a JS message. This needs to match up with the keys allowed for
-# the content.javascript.log setting.
-JsLogLevel = enum.Enum('JsLogLevel', ['unknown', 'info', 'warning', 'error'])
+class JsLogLevel(enum.Enum):
+ """Log level of a JS message.
-MessageLevel = enum.Enum('MessageLevel', ['error', 'warning', 'info'])
+ This needs to match up with the keys allowed for the
+ content.javascript.log setting.
+ """
+
+ unknown = enum.auto()
+ info = enum.auto()
+ warning = enum.auto()
+ error = enum.auto()
+
+
+class MessageLevel(enum.Enum):
+
+ """The level of a message being shown."""
+
+ error = enum.auto()
+ warning = enum.auto()
+ info = enum.auto()
-IgnoreCase = enum.Enum('IgnoreCase', ['smart', 'never', 'always'])
+class IgnoreCase(enum.Enum):
+
+ """Possible values for the 'search.ignore_case' setting."""
+
+ smart = enum.auto()
+ never = enum.auto()
+ always = enum.auto()
class CommandValue(enum.Enum):
"""Special values which are injected when running a command handler."""
- count = 1
- win_id = 2
- cur_tab = 3
- count_tab = 4
+ count = enum.auto()
+ win_id = enum.auto()
+ cur_tab = enum.auto()
+ count_tab = enum.auto()
class Question(QObject):
@@ -360,13 +415,13 @@ class Question(QObject):
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
- 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[str]
- self.option = None # type: typing.Optional[bool]
- self.answer = None # type: typing.Union[str, bool, None]
+ self.mode: Optional[PromptMode] = None
+ self.default: Union[bool, str, None] = None
+ self.title: Optional[str] = None
+ self.text: Optional[str] = None
+ self.url: Optional[str] = None
+ self.option: Optional[bool] = None
+ self.answer: Union[str, bool, None] = None
self.is_aborted = False
self.interrupted = False
@@ -440,7 +495,7 @@ class AbstractCertificateErrorWrapper:
"""A wrapper over an SSL/certificate error."""
- def __init__(self, error: typing.Any) -> None:
+ def __init__(self, error: Any) -> None:
self._error = error
def __str__(self) -> str:
@@ -458,18 +513,32 @@ class NavigationRequest:
"""A request to navigate to the given URL."""
- Type = enum.Enum('Type', [
- 'link_clicked',
- 'typed', # QtWebEngine only
- 'form_submitted',
- 'form_resubmitted', # QtWebKit only
- 'back_forward',
- 'reloaded',
- 'redirect', # QtWebEngine >= 5.14 only
- 'other'
- ])
-
- 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
+ class Type(enum.Enum):
+
+ """The type of a request.
+
+ Based on QWebEngineUrlRequestInfo::NavigationType and QWebPage::NavigationType.
+ """
+
+ #: Navigation initiated by clicking a link.
+ link_clicked = 1
+ #: Navigation explicitly initiated by typing a URL (QtWebEngine only).
+ typed = 2
+ #: Navigation submits a form.
+ form_submitted = 3
+ #: An HTML form was submitted a second time (QtWebKit only).
+ form_resubmitted = 4
+ #: Navigation initiated by a history action.
+ back_forward = 5
+ #: Navigation initiated by refreshing the page.
+ reloaded = 6
+ #: Navigation triggered automatically by page content or remote server
+ #: (QtWebEngine >= 5.14 only)
+ redirect = 7
+ #: None of the above.
+ other = 8
+
+ url: QUrl = attr.ib()
+ navigation_type: Type = attr.ib()
+ is_main_frame: bool = attr.ib()
+ accepted: bool = attr.ib(default=True)
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 0bbba9a4f..6952acf97 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -35,9 +35,9 @@ import socket
import shlex
import glob
import mimetypes
-import typing
import ctypes
import ctypes.util
+from typing import Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
@@ -159,7 +159,7 @@ def preload_resources() -> None:
# FIXME:typing Return value should be bytes/str
-def read_file(filename: str, binary: bool = False) -> typing.Any:
+def read_file(filename: str, binary: bool = False) -> Any:
"""Get the contents of a file contained with qutebrowser.
Args:
@@ -181,7 +181,7 @@ def read_file(filename: str, binary: bool = False) -> typing.Any:
# 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: # type: typing.IO
+ with open(fn, 'rb') as f: # type: IO
return f.read()
else:
with open(fn, 'r', encoding='utf-8') as f:
@@ -212,7 +212,7 @@ def resource_filename(filename: str) -> str:
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]:
+ percent: int) -> Tuple[int, int, int]:
"""Get a color which is percent% interpolated between start and end.
Args:
@@ -237,7 +237,7 @@ def interpolate_color(
start: QColor,
end: QColor,
percent: int,
- colorspace: typing.Optional[QColor.Spec] = QColor.Rgb
+ colorspace: Optional[QColor.Spec] = QColor.Rgb
) -> QColor:
"""Get an interpolated color value.
@@ -303,9 +303,7 @@ def format_seconds(total_seconds: int) -> str:
return prefix + ':'.join(chunks)
-def format_size(size: typing.Optional[float],
- base: int = 1024,
- suffix: str = '') -> str:
+def format_size(size: Optional[float], base: int = 1024, suffix: str = '') -> str:
"""Format a byte size so it's human readable.
Inspired by http://stackoverflow.com/q/1094841
@@ -324,13 +322,13 @@ class FakeIOStream(io.TextIOBase):
"""A fake file-like stream which calls a function for write-calls."""
- def __init__(self, write_func: typing.Callable[[str], int]) -> None:
+ def __init__(self, write_func: Callable[[str], int]) -> None:
super().__init__()
self.write = write_func # type: ignore[assignment]
@contextlib.contextmanager
-def fake_io(write_func: typing.Callable[[str], int]) -> typing.Iterator[None]:
+def fake_io(write_func: Callable[[str], int]) -> Iterator[None]:
"""Run code with stdout and stderr replaced by FakeIOStreams.
Args:
@@ -354,7 +352,7 @@ def fake_io(write_func: typing.Callable[[str], int]) -> typing.Iterator[None]:
@contextlib.contextmanager
-def disabled_excepthook() -> typing.Iterator[None]:
+def disabled_excepthook() -> Iterator[None]:
"""Run code with the exception hook temporarily disabled."""
old_excepthook = sys.excepthook
sys.excepthook = sys.__excepthook__
@@ -387,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: typing.Any, predicate: bool = True) -> None:
+ def __init__(self, retval: Any, predicate: bool = True) -> None:
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
@@ -398,7 +396,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
self._retval = retval
self._predicate = predicate
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
"""Called when a function should be decorated.
Args:
@@ -413,7 +411,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
retval = self._retval
@functools.wraps(func)
- def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
"""Call the original function."""
try:
return func(*args, **kwargs)
@@ -424,7 +422,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
return wrapper
-def is_enum(obj: typing.Any) -> bool:
+def is_enum(obj: Any) -> bool:
"""Check if a given object is an enum."""
try:
return issubclass(obj, enum.Enum)
@@ -432,9 +430,7 @@ def is_enum(obj: typing.Any) -> bool:
return False
-def get_repr(obj: typing.Any,
- constructor: bool = False,
- **attrs: typing.Any) -> str:
+def get_repr(obj: Any, constructor: bool = False, **attrs: Any) -> str:
"""Get a suitable __repr__ string for an object.
Args:
@@ -457,7 +453,7 @@ def get_repr(obj: typing.Any,
return '<{}>'.format(cls)
-def qualname(obj: typing.Any) -> str:
+def qualname(obj: Any) -> str:
"""Get the fully qualified name of an object.
Based on twisted.python.reflect.fullyQualifiedName.
@@ -485,14 +481,10 @@ def qualname(obj: typing.Any) -> str:
return repr(obj)
-# The string annotation is a WORKAROUND for a Python 3.5.2 bug:
-# https://github.com/python/typing/issues/266
+_ExceptionType = Union[Type[BaseException], Tuple[Type[BaseException]]]
-def raises(exc: ('typing.Union[' # pylint: disable=bad-docstring-quotes
- ' typing.Type[BaseException], '
- ' typing.Tuple[typing.Type[BaseException]]]'),
- func: typing.Callable,
- *args: typing.Any) -> bool:
+
+def raises(exc: _ExceptionType, func: Callable, *args: Any) -> bool:
"""Check if a function raises a given exception.
Args:
@@ -520,7 +512,7 @@ def force_encoding(text: str, encoding: str) -> str:
def sanitize_filename(name: str,
- replacement: typing.Optional[str] = '_',
+ replacement: Optional[str] = '_',
shorten: bool = False) -> str:
"""Replace invalid filename characters.
@@ -708,7 +700,7 @@ def open_file(filename: str, cmdline: str = None) -> None:
proc.start_detached(cmd, args)
-def unused(_arg: typing.Any) -> None:
+def unused(_arg: Any) -> None:
"""Function which does nothing to avoid pylint complaining."""
@@ -730,16 +722,22 @@ def expand_windows_drive(path: str) -> str:
return path
-def yaml_load(f: typing.Union[str, typing.IO[str]]) -> typing.Any:
+def yaml_load(f: Union[str, IO[str]]) -> Any:
"""Wrapper over yaml.load using the C loader if possible."""
start = datetime.datetime.now()
# WORKAROUND for https://github.com/yaml/pyyaml/pull/181
- with log.ignore_py_warnings(
+ with log.py_warning_filter(
category=DeprecationWarning,
message=r"Using or importing the ABCs from 'collections' instead "
r"of from 'collections\.abc' is deprecated.*"):
- data = yaml.load(f, Loader=YamlLoader)
+ try:
+ data = yaml.load(f, Loader=YamlLoader)
+ except ValueError as e:
+ if str(e).startswith('could not convert string to float'):
+ # WORKAROUND for https://github.com/yaml/pyyaml/issues/168
+ raise yaml.YAMLError(e)
+ raise # pragma: no cover
end = datetime.datetime.now()
@@ -760,8 +758,7 @@ def yaml_load(f: typing.Union[str, typing.IO[str]]) -> typing.Any:
return data
-def yaml_dump(data: typing.Any,
- f: typing.IO[str] = None) -> typing.Optional[str]:
+def yaml_dump(data: Any, f: IO[str] = None) -> Optional[str]:
"""Wrapper over yaml.dump using the C dumper if possible.
Also returns a str instead of bytes.
@@ -774,7 +771,7 @@ def yaml_dump(data: typing.Any,
return yaml_data.decode('utf-8')
-def chunk(elems: typing.Sequence, n: int) -> typing.Iterator[typing.Sequence]:
+def chunk(elems: Sequence, n: int) -> Iterator[Sequence]:
"""Yield successive n-sized chunks from elems.
If elems % n != 0, the last chunk will be smaller.
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 476926d34..031a8410c 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -30,8 +30,8 @@ import collections
import enum
import datetime
import getpass
-import typing
import functools
+from typing import Mapping, Optional, Sequence, Tuple, cast
import attr
import pkg_resources
@@ -82,20 +82,36 @@ class DistributionInfo:
"""Information about the running distribution."""
- 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
+ id: Optional[str] = attr.ib()
+ parsed: 'Distribution' = attr.ib()
+ version: Optional[Tuple[str, ...]] = attr.ib()
+ pretty: str = attr.ib()
pastebin_url = None
-Distribution = enum.Enum(
- 'Distribution', ['unknown', 'ubuntu', 'debian', 'void', 'arch',
- 'gentoo', 'fedora', 'opensuse', 'linuxmint', 'manjaro',
- 'kde_flatpak'])
-def distribution() -> typing.Optional[DistributionInfo]:
+class Distribution(enum.Enum):
+
+ """A known Linux distribution.
+
+ Usually lines up with ID=... in /etc/os-release.
+ """
+
+ unknown = enum.auto()
+ ubuntu = enum.auto()
+ debian = enum.auto()
+ void = enum.auto()
+ arch = enum.auto()
+ gentoo = enum.auto() # includes funtoo
+ fedora = enum.auto()
+ opensuse = enum.auto()
+ linuxmint = enum.auto()
+ manjaro = enum.auto()
+ kde_flatpak = enum.auto() # org.kde.Platform
+
+
+def distribution() -> Optional[DistributionInfo]:
"""Get some information about the running Linux distribution.
Returns:
@@ -123,9 +139,8 @@ def distribution() -> typing.Optional[DistributionInfo]:
assert pretty is not None
if 'VERSION_ID' in info:
- dist_version = pkg_resources.parse_version(
- info['VERSION_ID']
- ) # type: typing.Optional[typing.Tuple[str, ...]]
+ dist_version: Optional[Tuple[str, ...]] = pkg_resources.parse_version(
+ info['VERSION_ID'])
else:
dist_version = None
@@ -154,7 +169,7 @@ def is_sandboxed() -> bool:
return current_distro.parsed == Distribution.kde_flatpak
-def _git_str() -> typing.Optional[str]:
+def _git_str() -> Optional[str]:
"""Try to find out git version.
Return:
@@ -188,7 +203,7 @@ def _call_git(gitpath: str, *args: str) -> str:
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
-def _git_str_subprocess(gitpath: str) -> typing.Optional[str]:
+def _git_str_subprocess(gitpath: str) -> Optional[str]:
"""Try to get the git commit ID and timestamp by calling git.
Args:
@@ -210,7 +225,7 @@ def _git_str_subprocess(gitpath: str) -> typing.Optional[str]:
return None
-def _release_info() -> typing.Sequence[typing.Tuple[str, str]]:
+def _release_info() -> Sequence[Tuple[str, str]]:
"""Try to gather distribution release information.
Return:
@@ -234,14 +249,14 @@ def _release_info() -> typing.Sequence[typing.Tuple[str, str]]:
return data
-def _module_versions() -> typing.Sequence[str]:
+def _module_versions() -> Sequence[str]:
"""Get versions of optional modules.
Return:
A list of lines with version info.
"""
lines = []
- modules = collections.OrderedDict([
+ modules: Mapping[str, Sequence[str]] = collections.OrderedDict([
('sip', ['SIP_VERSION_STR']),
('colorama', ['VERSION', '__version__']),
('pypeg2', ['__version__']),
@@ -254,7 +269,7 @@ def _module_versions() -> typing.Sequence[str]:
('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)
@@ -274,7 +289,7 @@ def _module_versions() -> typing.Sequence[str]:
return lines
-def _path_info() -> typing.Mapping[str, str]:
+def _path_info() -> Mapping[str, str]:
"""Get info about important path names.
Return:
@@ -293,7 +308,7 @@ def _path_info() -> typing.Mapping[str, str]:
return info
-def _os_info() -> typing.Sequence[str]:
+def _os_info() -> Sequence[str]:
"""Get operating system info.
Return:
@@ -403,6 +418,7 @@ def _chromium_version() -> str:
5.12.7: Security fixes up to 79.0.3945.130 (2020-01-16)
5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18)
5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03)
+ 5.12.10: Security fixes up to 86.0.4240.75 (2020-10-06)
Qt 5.13: Chromium 73
73.0.3683.105 (~2019-02-28)
@@ -419,6 +435,10 @@ def _chromium_version() -> str:
Qt 5.15: Chromium 80
80.0.3987.163 (2020-04-02)
5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05)
+ 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25)
+
+ 5.15.2: Updated to 83.0.4103.122 (~2020-06-24)
+ Security fixes up to 86.0.4240.111 (2020-10-20)
Also see:
@@ -430,6 +450,8 @@ def _chromium_version() -> str:
return 'unavailable' # type: ignore[unreachable]
if webenginesettings.parsed_user_agent is None:
+ if 'avoid-chromium-init' in objects.debug_flags:
+ return 'avoided'
webenginesettings.init_user_agent()
assert webenginesettings.parsed_user_agent is not None
@@ -550,21 +572,21 @@ class OpenGLInfo:
"""Information about the OpenGL setup in use."""
# If we're using OpenGL ES. If so, no further information is available.
- gles = attr.ib(False) # type: bool
+ gles: bool = attr.ib(False)
# The name of the vendor. Examples:
# - nouveau
# - "Intel Open Source Technology Center", "Intel", "Intel Inc."
- vendor = attr.ib(None) # type: typing.Optional[str]
+ vendor: Optional[str] = attr.ib(None)
# The OpenGL version as a string. See tests for examples.
- version_str = attr.ib(None) # type: typing.Optional[str]
+ version_str: Optional[str] = attr.ib(None)
# The parsed version as a (major, minor) tuple of ints
- version = attr.ib(None) # type: typing.Optional[typing.Tuple[int, ...]]
+ version: Optional[Tuple[int, ...]] = attr.ib(None)
# The vendor specific information following the version number
- vendor_specific = attr.ib(None) # type: typing.Optional[str]
+ vendor_specific: Optional[str] = attr.ib(None)
def __str__(self) -> str:
if self.gles:
@@ -602,7 +624,7 @@ class OpenGLInfo:
@functools.lru_cache(maxsize=1)
-def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover
+def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
"""Get the OpenGL vendor used.
This returns a string such as 'nouveau' or
@@ -620,8 +642,7 @@ def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover
vendor, version = override.split(', ', maxsplit=1)
return OpenGLInfo.parse(vendor=vendor, version=version)
- old_context = typing.cast(typing.Optional[QOpenGLContext],
- QOpenGLContext.currentContext())
+ old_context = cast(Optional[QOpenGLContext], QOpenGLContext.currentContext())
old_surface = None if old_context is None else old_context.surface()
surface = QOffscreenSurface()
diff --git a/requirements.txt b/requirements.txt
index 0223e896a..dad7a8f50 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,10 +2,10 @@
adblock==0.3.2
attrs==20.2.0
-colorama==0.4.3
+colorama==0.4.4
cssutils==1.0.2
Jinja2==2.11.2
MarkupSafe==1.1.1
-Pygments==2.7.1
+Pygments==2.7.2
pyPEG2==2.15.2
PyYAML==5.3.1
diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py
index 313aa13e3..f4e4ac07f 100644
--- a/scripts/dev/check_coverage.py
+++ b/scripts/dev/check_coverage.py
@@ -53,7 +53,12 @@ class Message:
print(self.text)
-MsgType = enum.Enum('MsgType', 'insufficient_coverage, perfect_file')
+class MsgType(enum.Enum):
+
+ """The type of a message to be output."""
+
+ insufficient_coverage = enum.auto()
+ perfect_file = enum.auto()
# A list of (test_file, tested_file) tuples. test_file can be None.
@@ -213,6 +218,8 @@ PERFECT_FILES = [
'qutebrowser/browser/webengine/spell.py'),
('tests/unit/browser/webengine/test_webengine_cookies.py',
'qutebrowser/browser/webengine/cookies.py'),
+ ('tests/unit/browser/webengine/test_darkmode.py',
+ 'qutebrowser/browser/webengine/darkmode.py'),
]
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index 439976c34..5b79b801d 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -84,6 +84,7 @@ CHANGELOG_URLS = {
'jaraco.functools': 'https://github.com/jaraco/jaraco.functools/blob/master/CHANGES.rst',
'parse': 'https://github.com/r1chardj0n3s/parse#potential-gotchas',
'py': 'https://py.readthedocs.io/en/latest/changelog.html#changelog',
+ 'Pympler': 'https://github.com/pympler/pympler/blob/master/CHANGELOG.md',
'pytest-mock': 'https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst',
'pytest-qt': 'https://github.com/pytest-dev/pytest-qt/blob/master/CHANGELOG.rst',
'wcwidth': 'https://github.com/jquast/wcwidth#history',
@@ -99,17 +100,18 @@ CHANGELOG_URLS = {
'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst',
'wheel': 'https://github.com/pypa/wheel/blob/master/docs/news.rst',
'mako': 'https://docs.makotemplates.org/en/latest/changelog.html',
- 'lxml': 'https://lxml.de/4.5/changes-4.5.0.html',
+ 'lxml': 'https://lxml.de/4.6/changes-4.6.0.html',
'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master',
'tox-pip-version': 'https://github.com/pglass/tox-pip-version/commits/master',
'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst',
- 'pep517': 'https://github.com/pypa/pep517/commits/master',
- 'cryptography': 'https://cryptography.io/en/latest/changelog/',
+ 'pep517': 'https://github.com/pypa/pep517/blob/master/doc/changelog.rst',
+ 'cryptography': 'https://cryptography.io/en/latest/changelog.html',
'toml': 'https://github.com/uiri/toml/releases',
'PyQt5': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine': 'https://www.riverbankcomputing.com/news',
'PyQt-builder': 'https://www.riverbankcomputing.com/news',
'PyQt5-sip': 'https://www.riverbankcomputing.com/news',
+ 'PyQt5_stubs': 'https://github.com/stlehmann/PyQt5-stubs/blob/master/CHANGELOG.md',
'sip': 'https://www.riverbankcomputing.com/news',
'Pygments': 'https://pygments.org/docs/changelog/',
'vulture': 'https://github.com/jendrikseipp/vulture/blob/master/CHANGELOG.md',
@@ -122,6 +124,15 @@ CHANGELOG_URLS = {
'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md',
'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
'diff_cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG',
+ 'pytest-clarity': 'https://github.com/darrenburns/pytest-clarity/commits/master',
+ 'pytest-icdiff': 'https://github.com/hjwp/pytest-icdiff/blob/master/HISTORY.rst',
+ 'icdiff': 'https://github.com/jeffkaufman/icdiff/blob/master/ChangeLog',
+ 'termcolor': 'https://pypi.org/project/termcolor/',
+ 'pprintpp': 'https://github.com/wolever/pprintpp/blob/master/CHANGELOG.txt',
+ 'beautifulsoup4': 'https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG',
+ 'check-manifest': 'https://github.com/mgedmin/check-manifest/blob/master/CHANGES.rst',
+ 'yamllint': 'https://github.com/adrienverge/yamllint/blob/master/CHANGELOG.rst',
+ 'filelock': 'https://github.com/benediktschmitt/py-filelock/commits/master',
}
# PyQt versions which need SIP v4
@@ -303,8 +314,8 @@ class Change:
return '| {} | {} | {} |'.format(self.link, self.old, self.new)
-def print_changed_files():
- """Output all changed files from this run."""
+def _get_changed_files():
+ """Get a list of changed files via git."""
changed_files = set()
filenames = git_diff('--name-only')
for filename in filenames:
@@ -312,8 +323,12 @@ def print_changed_files():
filename = filename.replace('misc/requirements/requirements-', '')
filename = filename.replace('.txt', '')
changed_files.add(filename)
- files_text = '\n'.join('- ' + line for line in sorted(changed_files))
+ return sorted(changed_files)
+
+
+def _get_changes():
+ """Get a list of changed versions from git."""
changes_dict = {}
diff = git_diff()
for line in diff:
@@ -326,10 +341,16 @@ def print_changed_files():
name, version = line[1:].split('==')
if ';' in version: # pip environment markers
version = version.split(';')[0].strip()
+ elif line[1:].startswith('-e'):
+ rest, name = line.split('#egg=')
+ version = rest.split('@')[1][:7]
else:
name = line[1:]
version = '?'
+ if name.startswith('#'): # duplicate requirements
+ name = name[1:].strip()
+
if name not in changes_dict:
changes_dict[name] = Change(name)
@@ -338,7 +359,15 @@ def print_changed_files():
elif line.startswith('+'):
changes_dict[name].new = version
- changes = [change for _name, change in sorted(changes_dict.items())]
+ return [change for _name, change in sorted(changes_dict.items())]
+
+
+def print_changed_files():
+ """Output all changed files from this run."""
+ changed_files = _get_changed_files()
+ files_text = '\n'.join('- ' + line for line in changed_files)
+
+ changes = _get_changes()
diff_text = '\n'.join(str(change) for change in changes)
utils.print_title('Changed')
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index 3e7b21898..1d024c6e5 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -32,7 +32,7 @@ import vulture
import qutebrowser.app # pylint: disable=unused-import
from qutebrowser.extensions import loader
from qutebrowser.misc import objects
-from qutebrowser.utils import utils
+from qutebrowser.utils import utils, version
from qutebrowser.browser.webkit import rfc6266
# To run the decorators from there
# pylint: disable=unused-import
@@ -124,6 +124,9 @@ def whitelist_generator(): # noqa
'_get_default_metavar_for_positional', '_metavar_formatter']:
yield 'scripts.dev.src2asciidoc.UsageFormatter.' + attr
+ for dist in version.Distribution:
+ yield 'qutebrowser.utils.version.Distribution.{}'.format(dist.name)
+
# attrs
yield 'qutebrowser.browser.webkit.network.networkmanager.ProxyId.hostname'
yield 'qutebrowser.command.command.ArgInfo._validate_exclusive'
diff --git a/scripts/dev/ua_fetch.py b/scripts/dev/ua_fetch.py
new file mode 100644
index 000000000..a4ef889a0
--- /dev/null
+++ b/scripts/dev/ua_fetch.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+"""Fetch and print the most common user agents.
+
+This script fetches the most common user agents according to
+https://github.com/Kikobeats/top-user-agents, and prints the most recent
+Chrome user agent for Windows, macOS and Linux.
+"""
+
+import math
+import sys
+import textwrap
+
+import requests
+import qutebrowser.config.websettings
+
+
+def version(ua):
+ """Comparable version of a user agent."""
+ return tuple(int(v) for v in ua.upstream_browser_version.split('.')[:2])
+
+
+def wrap(ini, sub, string):
+ return textwrap.wrap(string, width=80, initial_indent=ini, subsequent_indent=sub)
+
+
+response = requests.get('https://raw.githubusercontent.com/Kikobeats/top-user-agents/master/index.json')
+
+if response.status_code != 200:
+ print('Unable to fetch the user agent index', file=sys.stderr)
+ sys.exit(1)
+
+ua_checks = {
+ 'Win10': lambda ua: ua.os_info.startswith('Windows NT'),
+ 'macOS': lambda ua: ua.os_info.startswith('Macintosh'),
+ 'Linux': lambda ua: ua.os_info.startswith('X11'),
+}
+
+ua_strings = {}
+ua_versions = {}
+ua_names = {}
+
+for ua_string in reversed(response.json()):
+ # reversed to prefer more common versions
+
+ # Filter out browsers that are not Chrome-based
+ parts = ua_string.split()
+ if not any(part.startswith("Chrome/") for part in parts):
+ continue
+ if any(part.startswith("OPR/") or part.startswith("Edg/") for part in parts):
+ continue
+
+ user_agent = qutebrowser.config.websettings.UserAgent.parse(ua_string)
+
+ # check which os_string conditions are met and select the most recent version
+ for key, check in ua_checks.items():
+ if check(user_agent):
+ v = version(user_agent)
+ if v >= ua_versions.get(key, (-math.inf,)):
+ ua_versions[key] = v
+ ua_strings[key] = ua_string
+ ua_names[key] = f'Chrome {v[0]} {key}'
+
+for key, ua_string in ua_strings.items():
+ quoted_ua_string = f'"{ua_string}"'
+ for line in wrap(" - - ", " ", quoted_ua_string):
+ print(line)
+ for line in wrap(" - ", " ", ua_names[key]):
+ print(line)
diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py
index 7c12dab15..5f7df5cae 100644
--- a/scripts/dev/update_version.py
+++ b/scripts/dev/update_version.py
@@ -82,7 +82,7 @@ if __name__ == "__main__":
.format(v=version))
print("* Windows: git fetch; git checkout v{v}; "
"py -3.7 -m tox -e build-release -- --asciidoc "
- "$env:userprofile\\bin\\asciidoc-9.9.2\\asciidoc.py --upload"
+ "$env:userprofile\\bin\\asciidoc-9.0.2\\asciidoc.py --upload"
.format(v=version))
print("* macOS: git fetch && git checkout v{v} && "
"tox -e build-release -- --upload"
diff --git a/scripts/open_url_in_instance.sh b/scripts/open_url_in_instance.sh
index ec2a5a26d..0d6edef51 100755
--- a/scripts/open_url_in_instance.sh
+++ b/scripts/open_url_in_instance.sh
@@ -12,4 +12,4 @@ printf '{"args": ["%s"], "target_arg": null, "version": "%s", "protocol_version"
"${_url}" \
"${_qb_version}" \
"${_proto_version}" \
- "${PWD}" | socat - UNIX-CONNECT:"${_ipc_socket}" 2>/dev/null || "$_qute_bin" "$@" &
+ "${PWD}" | socat -lf /dev/null - UNIX-CONNECT:"${_ipc_socket}" || "$_qute_bin" "$@" &
diff --git a/setup.py b/setup.py
index 0c0bf73b4..1169eae81 100755
--- a/setup.py
+++ b/setup.py
@@ -72,7 +72,7 @@ try:
['qutebrowser = qutebrowser.qutebrowser:main']},
zip_safe=True,
install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
- python_requires='>=3.5',
+ python_requires='>=3.6',
name='qutebrowser',
version=_get_constant('version'),
description=_get_constant('description'),
@@ -94,7 +94,6 @@ try:
'Operating System :: MacOS',
'Operating System :: POSIX :: BSD',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
diff --git a/tests/conftest.py b/tests/conftest.py
index d4d06c6bc..ef169be4f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -34,7 +34,7 @@ pytest.register_assert_rewrite('helpers')
from helpers import logfail
from helpers.logfail import fail_on_logging
-from helpers.messagemock import message_mock, message_bridge
+from helpers.messagemock import message_mock
from helpers.fixtures import * # noqa: F403
from helpers import utils as testutils
from qutebrowser.utils import qtutils, standarddir, usertypes, utils, version
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py
index 0208cce05..87748a43a 100644
--- a/tests/end2end/features/conftest.py
+++ b/tests/end2end/features/conftest.py
@@ -378,6 +378,7 @@ def hint(quteproc, args):
@bdd.when(bdd.parsers.parse('I hint with args "{args}" and follow {letter}'))
def hint_and_follow(quteproc, args, letter):
args = args.replace('(testdata)', testutils.abs_datapath())
+ args = args.replace('(python-executable)', sys.executable)
quteproc.send_cmd(':hint {}'.format(args))
quteproc.wait_for(message='hints: *')
quteproc.send_cmd(':follow-hint {}'.format(letter))
diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature
index caf1200e2..35e48c483 100644
--- a/tests/end2end/features/hints.feature
+++ b/tests/end2end/features/hints.feature
@@ -61,17 +61,17 @@ Feature: Using hints
Scenario: Using :hint spawn with flags and -- (issue 797)
When I open data/hints/html/simple.html
- And I hint with args "-- all spawn -v python -c ''" and follow a
+ And I hint with args "-- all spawn -v (python-executable) -c ''" and follow a
Then the message "Command exited successfully." should be shown
Scenario: Using :hint spawn with flags (issue 797)
When I open data/hints/html/simple.html
- And I hint with args "all spawn -v python -c ''" and follow a
+ And I hint with args "all spawn -v (python-executable) -c ''" and follow a
Then the message "Command exited successfully." should be shown
Scenario: Using :hint spawn with flags and --rapid (issue 797)
When I open data/hints/html/simple.html
- And I hint with args "--rapid all spawn -v python -c ''" and follow a
+ And I hint with args "--rapid all spawn -v (python-executable) -c ''" and follow a
Then the message "Command exited successfully." should be shown
@posix
diff --git a/tests/end2end/features/marks.feature b/tests/end2end/features/marks.feature
index 7ac60edeb..921e0e76c 100644
--- a/tests/end2end/features/marks.feature
+++ b/tests/end2end/features/marks.feature
@@ -112,11 +112,11 @@ Feature: Setting positional marks
Then the page should be scrolled to 0 0
Scenario: Hovering a hint does not set the ' mark
- When I run :scroll-px 30 20
- And I wait until the scroll position changed to 30/20
- And I run :scroll-to-perc 0
+ When I run :scroll-px 10 20
+ And I wait until the scroll position changed to 10/20
+ And I run :scroll-to-perc 0
And I wait until the scroll position changed
And I hint with args "links hover" and follow s
And I run :jump-mark "'"
- And I wait until the scroll position changed to 30/20
- Then the page should be scrolled to 30 20
+ And I wait until the scroll position changed to 10/20
+ Then the page should be scrolled to 10 20
diff --git a/tests/end2end/features/private.feature b/tests/end2end/features/private.feature
index 07ff225a3..fe870dded 100644
--- a/tests/end2end/features/private.feature
+++ b/tests/end2end/features/private.feature
@@ -172,7 +172,7 @@ Feature: Using private browsing
- url: http://localhost:*/data/numbers/1.txt
- url: http://localhost:*/data/numbers/2.txt
- @flaky
+ @skip # Too flaky
Scenario: Saving a private session with only-active-window
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
@@ -181,7 +181,12 @@ Feature: Using private browsing
And I open data/numbers/5.txt in a new tab
And I run :session-save --only-active-window window_session_name
And I run :window-only
+ And I wait for "removed: tab" in the log
+ And I wait for "removed: tab" in the log
And I run :tab-only
+ And I wait for "removed: tab" in the log
+ And I wait for "removed: tab" in the log
+ And I wait for "removed: tab" in the log
And I run :session-load -c window_session_name
And I wait until data/numbers/5.txt is loaded
Then the session should look like:
diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature
index 55e366b4f..84e1b04e8 100644
--- a/tests/end2end/features/qutescheme.feature
+++ b/tests/end2end/features/qutescheme.feature
@@ -89,6 +89,7 @@ Feature: Special qute:// pages
And I open qute://help/img/ without waiting
Then "*Error while * qute://*" should be logged
And "* url='qute://help/img'* LoadStatus.error" should be logged
+ And "Load error: ERR_FILE_NOT_FOUND" should be logged
# :history
diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature
index ec38116c3..e55b5839d 100644
--- a/tests/end2end/features/urlmarks.feature
+++ b/tests/end2end/features/urlmarks.feature
@@ -231,7 +231,7 @@ Feature: quickmarks and bookmarks
And the page should contain the plaintext "twentyone"
Scenario: Listing bookmarks
- When I open data/title.html in a new tab
+ When I open data/title.html#unique-url in a new tab
And I run :bookmark-add
And I open qute://bookmarks
Then the page should contain the plaintext "Test title"
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index 78fd0e48a..f5cc2bb19 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -308,6 +308,27 @@ def is_ignored_chromium_message(line):
'Could not open platform files for entry.',
'Unable to terminate process *: No such process (3)',
'Failed to read /proc/*/stat',
+
+ # Qt 5.15.1 debug build (Chromium 83)
+ # '[314297:7:0929/214605.491958:ERROR:context_provider_command_buffer.cc(145)]
+ # GpuChannelHost failed to create command buffer.'
+ 'GpuChannelHost failed to create command buffer.',
+ # [338691:4:0929/220114.488847:WARNING:ipc_message_attachment_set.cc(49)]
+ # MessageAttachmentSet destroyed with unconsumed attachments: 0/1
+ 'MessageAttachmentSet destroyed with unconsumed attachments: *',
+
+ # GitHub Actions with Qt 5.15.1
+ ('SharedImageManager::ProduceGLTexture: Trying to produce a '
+ 'representation from a non-existent mailbox. *'),
+ ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : '
+ 'DoCreateAndTexStorage2DSharedImageINTERNAL: invalid mailbox name'),
+ ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : '
+ 'DoBeginSharedImageAccessCHROMIUM: bound texture is not a shared image'),
+ ('[.DisplayCompositor]RENDER WARNING: texture bound to texture unit 0 is '
+ 'not renderable. It might be non-power-of-2 or have incompatible texture '
+ 'filtering (maybe)?'),
+ ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : '
+ 'DoEndSharedImageAccessCHROMIUM: bound texture is not a shared image'),
]
return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages)
@@ -749,6 +770,7 @@ class QuteProc(testprocess.Process):
Return:
The parsed log line with "command called: ..." or None.
"""
+ __tracebackhide__ = lambda e: e.errisinstance(testprocess.WaitForTimeout)
summary = command
if count is not None:
summary += ' (count {})'.format(count)
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
index cd9aefe16..3e8731fad 100644
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -333,16 +333,17 @@ def test_command_on_start(request, quteproc_new):
quteproc_new.wait_for_quit()
-def test_launching_with_python2():
+@pytest.mark.parametrize('python', ['python2', 'python3.5'])
+def test_launching_with_old_python(python):
try:
proc = subprocess.run(
- ['python2', '-m', 'qutebrowser', '--no-err-windows'],
+ [python, '-m', 'qutebrowser', '--no-err-windows'],
stderr=subprocess.PIPE,
check=False)
except FileNotFoundError:
- pytest.skip("python2 not found")
+ pytest.skip(f"{python} not found")
assert proc.returncode == 1
- error = "At least Python 3.5.2 is required to run qutebrowser"
+ error = "At least Python 3.6 is required to run qutebrowser"
assert proc.stderr.decode('ascii').startswith(error)
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index 015238d1b..2183ac1d1 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -97,6 +97,10 @@ class WinRegistryHelper:
def windowTitle(self):
return 'window title - qutebrowser'
+ @property
+ def tabbed_browser(self):
+ return self.registry['tabbed-browser']
+
def __init__(self):
self._ids = []
@@ -705,3 +709,16 @@ def state_config(data_tmpdir, monkeypatch):
state = configfiles.StateConfig()
monkeypatch.setattr(configfiles, 'state', state)
return state
+
+
+@pytest.fixture
+def unwritable_tmp_path(tmp_path):
+ tmp_path.chmod(0)
+ if os.access(str(tmp_path), os.W_OK):
+ # Docker container or similar
+ pytest.skip("Directory was still writable")
+
+ yield tmp_path
+
+ # Make sure pytest can clean up the tmp_path
+ tmp_path.chmod(0o755)
diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py
index 4c1107029..03320a98f 100644
--- a/tests/helpers/messagemock.py
+++ b/tests/helpers/messagemock.py
@@ -24,7 +24,7 @@ import logging
import attr
import pytest
-from qutebrowser.utils import usertypes, message, objreg
+from qutebrowser.utils import usertypes, message
@attr.s
@@ -90,12 +90,3 @@ def message_mock():
mmock.patch()
yield mmock
mmock.unpatch()
-
-
-@pytest.fixture
-def message_bridge(win_registry):
- """Fixture to get a MessageBridge."""
- bridge = message.MessageBridge()
- objreg.register('message-bridge', bridge, scope='window', window=0)
- yield bridge
- objreg.delete('message-bridge', scope='window', window=0)
diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py
index 41fb4f100..77221be4c 100644
--- a/tests/helpers/utils.py
+++ b/tests/helpers/utils.py
@@ -234,7 +234,7 @@ def change_cwd(path):
@contextlib.contextmanager
def ignore_bs4_warning():
"""WORKAROUND for https://bugs.launchpad.net/beautifulsoup/+bug/1847592."""
- with log.ignore_py_warnings(
+ with log.py_warning_filter(
category=DeprecationWarning,
message="Using or importing the ABCs from 'collections' instead "
"of from 'collections.abc' is deprecated", module='bs4.element'):
diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py
index 58643640c..3ee486303 100644
--- a/tests/unit/api/test_cmdutils.py
+++ b/tests/unit/api/test_cmdutils.py
@@ -290,7 +290,11 @@ class TestRegister:
else:
assert pos_args == [('arg', 'arg')]
- Enum = enum.Enum('Test', ['x', 'y'])
+ class Enum(enum.Enum):
+
+ # pylint: disable=invalid-name
+ x = enum.auto()
+ y = enum.auto()
@pytest.mark.parametrize('typ, inp, choices, expected', [
(int, '42', None, 42),
diff --git a/tests/unit/browser/test_hints.py b/tests/unit/browser/test_hints.py
index 56e5b980c..382c322fb 100644
--- a/tests/unit/browser/test_hints.py
+++ b/tests/unit/browser/test_hints.py
@@ -46,8 +46,7 @@ def tabbed_browser(tabbed_browser_stubs, web_tab):
return tb
-def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
- mode_manager):
+def test_show_benchmark(benchmark, tabbed_browser, qtbot, mode_manager):
"""Benchmark showing/drawing of hint labels."""
tab = tabbed_browser.widget.tabs[0]
@@ -66,8 +65,8 @@ def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
benchmark(bench)
-def test_match_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
- mode_manager, qapp, config_stub):
+def test_match_benchmark(benchmark, tabbed_browser, qtbot, mode_manager, qapp,
+ config_stub):
"""Benchmark matching of hint labels."""
tab = tabbed_browser.widget.tabs[0]
diff --git a/tests/unit/browser/webengine/test_darkmode.py b/tests/unit/browser/webengine/test_darkmode.py
new file mode 100644
index 000000000..b2ca6a20a
--- /dev/null
+++ b/tests/unit/browser/webengine/test_darkmode.py
@@ -0,0 +1,228 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+
+import pytest
+
+from qutebrowser.config import configdata
+from qutebrowser.utils import usertypes, version
+from qutebrowser.browser.webengine import darkmode
+from qutebrowser.misc import objects
+from helpers import utils
+
+
+pytestmark = utils.qt510
+
+
+@pytest.fixture(autouse=True)
+def patch_backend(monkeypatch):
+ monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
+
+
+@pytest.mark.parametrize('settings, expected', [
+ # Disabled
+ ({}, []),
+
+ # Enabled without customization
+ ({'enabled': True}, [('forceDarkModeEnabled', 'true')]),
+
+ # Algorithm
+ (
+ {'enabled': True, 'algorithm': 'brightness-rgb'},
+ [
+ ('forceDarkModeEnabled', 'true'),
+ ('forceDarkModeInversionAlgorithm', '2')
+ ],
+ ),
+])
+def test_basics(config_stub, monkeypatch, settings, expected):
+ for k, v in settings.items():
+ config_stub.set_obj('colors.webpage.darkmode.' + k, v)
+ monkeypatch.setattr(darkmode, '_variant',
+ lambda: darkmode.Variant.qt_515_2)
+
+ if expected:
+ expected.append(('forceDarkModeImagePolicy', '2'))
+
+ assert list(darkmode.settings()) == expected
+
+
+QT_514_SETTINGS = [
+ ('darkMode', '2'),
+ ('darkModeImagePolicy', '2'),
+ ('darkModeGrayscale', 'true'),
+]
+
+
+QT_515_0_SETTINGS = [
+ ('darkModeEnabled', 'true'),
+ ('darkModeInversionAlgorithm', '2'),
+ ('darkModeGrayscale', 'true'),
+]
+
+
+QT_515_1_SETTINGS = [
+ ('darkModeEnabled', 'true'),
+ ('darkModeInversionAlgorithm', '2'),
+ ('darkModeImagePolicy', '2'),
+ ('darkModeGrayscale', 'true'),
+]
+
+
+QT_515_2_SETTINGS = [
+ ('forceDarkModeEnabled', 'true'),
+ ('forceDarkModeInversionAlgorithm', '2'),
+ ('forceDarkModeImagePolicy', '2'),
+ ('forceDarkModeGrayscale', 'true'),
+]
+
+
+@pytest.mark.parametrize('qversion, expected', [
+ ('5.14.0', QT_514_SETTINGS),
+ ('5.14.1', QT_514_SETTINGS),
+ ('5.14.2', QT_514_SETTINGS),
+
+ ('5.15.0', QT_515_0_SETTINGS),
+ ('5.15.1', QT_515_1_SETTINGS),
+
+ ('5.15.2', QT_515_2_SETTINGS),
+])
+def test_qt_version_differences(config_stub, monkeypatch, qversion, expected):
+ monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion)
+
+ major, minor, patch = [int(part) for part in qversion.split('.')]
+ hexversion = major << 16 | minor << 8 | patch
+ if major > 5 or minor >= 13:
+ # Added in Qt 5.13
+ monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', hexversion)
+
+ settings = {
+ 'enabled': True,
+ 'algorithm': 'brightness-rgb',
+ 'grayscale.all': True,
+ }
+ for k, v in settings.items():
+ config_stub.set_obj('colors.webpage.darkmode.' + k, v)
+
+ assert list(darkmode.settings()) == expected
+
+
+@utils.qt514
+@pytest.mark.parametrize('setting, value, exp_key, exp_val', [
+ ('contrast', -0.5,
+ 'Contrast', '-0.5'),
+ ('policy.page', 'smart',
+ 'PagePolicy', '1'),
+ ('policy.images', 'smart',
+ 'ImagePolicy', '2'),
+ ('threshold.text', 100,
+ 'TextBrightnessThreshold', '100'),
+ ('threshold.background', 100,
+ 'BackgroundBrightnessThreshold', '100'),
+ ('grayscale.all', True,
+ 'Grayscale', 'true'),
+ ('grayscale.images', 0.5,
+ 'ImageGrayscale', '0.5'),
+])
+def test_customization(config_stub, monkeypatch, setting, value, exp_key, exp_val):
+ config_stub.val.colors.webpage.darkmode.enabled = True
+ config_stub.set_obj('colors.webpage.darkmode.' + setting, value)
+ monkeypatch.setattr(darkmode, '_variant', lambda: darkmode.Variant.qt_515_2)
+
+ expected = []
+ expected.append(('forceDarkModeEnabled', 'true'))
+ if exp_key != 'ImagePolicy':
+ expected.append(('forceDarkModeImagePolicy', '2'))
+ expected.append(('forceDarkMode' + exp_key, exp_val))
+
+ assert list(darkmode.settings()) == expected
+
+
+@pytest.mark.parametrize('qversion, webengine_version, expected', [
+ # Without PYQT_WEBENGINE_VERSION
+ ('5.9.9', None, darkmode.Variant.unavailable),
+ ('5.10.1', None, darkmode.Variant.qt_510),
+ ('5.11.3', None, darkmode.Variant.qt_511_to_513),
+ ('5.12.9', None, darkmode.Variant.qt_511_to_513),
+
+ # With PYQT_WEBENGINE_VERSION
+ (None, 0x050d00, darkmode.Variant.qt_511_to_513),
+ (None, 0x050e00, darkmode.Variant.qt_514),
+ (None, 0x050f00, darkmode.Variant.qt_515_0),
+ (None, 0x050f01, darkmode.Variant.qt_515_1),
+ (None, 0x050f02, darkmode.Variant.qt_515_2),
+ (None, 0x060000, darkmode.Variant.qt_515_2), # Qt 6
+])
+def test_variant(monkeypatch, qversion, webengine_version, expected):
+ monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion)
+ monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', webengine_version)
+ assert darkmode._variant() == expected
+
+
+def test_broken_smart_images_policy(config_stub, monkeypatch, caplog):
+ config_stub.val.colors.webpage.darkmode.enabled = True
+ config_stub.val.colors.webpage.darkmode.policy.images = 'smart'
+ monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', 0x050f00)
+
+ with caplog.at_level(logging.WARNING):
+ settings = list(darkmode.settings())
+
+ assert caplog.messages[-1] == (
+ 'Ignoring colors.webpage.darkmode.policy.images = smart because of '
+ 'Qt 5.15.0 bug')
+
+ expected = [
+ [('darkModeEnabled', 'true')], # Qt 5.15
+ [('darkMode', '4')], # Qt 5.14
+ ]
+ assert settings in expected
+
+
+@utils.qt510
+def test_new_chromium():
+ """Fail if we encounter an unknown Chromium version.
+
+ Dark mode in Chromium (or rather, the underlying Blink) is being changed with
+ almost every Chromium release.
+
+ Make this test fail deliberately with newer Chromium versions, so that
+ we can test whether dark mode still works manually, and adjust if not.
+ """
+ assert version._chromium_version() in [
+ 'unavailable', # QtWebKit
+ '61.0.3163.140', # Qt 5.10
+ '65.0.3325.230', # Qt 5.11
+ '69.0.3497.128', # Qt 5.12
+ '73.0.3683.105', # Qt 5.13
+ '77.0.3865.129', # Qt 5.14
+ '80.0.3987.163', # Qt 5.15.0
+ '83.0.4103.122', # Qt 5.15.2
+ ]
+
+
+def test_options(configdata_init):
+ """Make sure all darkmode options have the right attributes set."""
+ for name, opt in configdata.DATA.items():
+ if not name.startswith('colors.webpage.darkmode.'):
+ continue
+
+ assert not opt.supports_pattern, name
+ assert opt.restart, name
+ assert not opt.raw_backends['QtWebKit'], name
+ assert opt.raw_backends['QtWebEngine'] in ['Qt 5.10', 'Qt 5.14'], name
diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py
index 8d4289f4b..58e5602b3 100644
--- a/tests/unit/browser/webkit/test_mhtml.py
+++ b/tests/unit/browser/webkit/test_mhtml.py
@@ -29,10 +29,7 @@ mhtml = pytest.importorskip('qutebrowser.browser.webkit.mhtml')
try:
import cssutils
-except (ImportError, re.error):
- # Catching re.error because cssutils in earlier releases (<= 1.0) is
- # broken on Python 3.5
- # See https://bitbucket.org/cthedot/cssutils/issues/52
+except ImportError:
cssutils = None
diff --git a/tests/unit/commands/test_argparser.py b/tests/unit/commands/test_argparser.py
index ccf81edd1..69119c4cf 100644
--- a/tests/unit/commands/test_argparser.py
+++ b/tests/unit/commands/test_argparser.py
@@ -28,7 +28,10 @@ from PyQt5.QtCore import QUrl
from qutebrowser.commands import argparser, cmdexc
-Enum = enum.Enum('Enum', ['foo', 'foo_bar'])
+class Enum(enum.Enum):
+
+ foo = enum.auto()
+ foo_bar = enum.auto()
class TestArgumentParser:
diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py
index cb2608145..331d214b1 100644
--- a/tests/unit/config/test_configcommands.py
+++ b/tests/unit/config/test_configcommands.py
@@ -499,7 +499,8 @@ class TestSource:
else:
assert False, location
- pyfile.write_text('c.content.javascript.enabled = False\n',
+ pyfile.write_text('\n'.join(['config.load_autoconfig(False)',
+ 'c.content.javascript.enabled = False']),
encoding='utf-8')
commands.config_source(arg, clear=clear)
@@ -511,14 +512,16 @@ class TestSource:
def test_config_py_arg_source(self, commands, config_py_arg, config_stub):
assert config_stub.val.content.javascript.enabled
- config_py_arg.write_text('c.content.javascript.enabled = False\n',
+ config_py_arg.write_text('\n'.join(['config.load_autoconfig(False)',
+ 'c.content.javascript.enabled = False']),
encoding='utf-8')
commands.config_source()
assert not config_stub.val.content.javascript.enabled
def test_errors(self, commands, config_tmpdir):
pyfile = config_tmpdir / 'config.py'
- pyfile.write_text('c.foo = 42', encoding='utf-8')
+ pyfile.write_text('\n'.join(['config.load_autoconfig(False)',
+ 'c.foo = 42']), encoding='utf-8')
with pytest.raises(cmdutils.CommandError) as excinfo:
commands.config_source()
@@ -529,7 +532,8 @@ class TestSource:
def test_invalid_source(self, commands, config_tmpdir):
pyfile = config_tmpdir / 'config.py'
- pyfile.write_text('1/0', encoding='utf-8')
+ pyfile.write_text('\n'.join(['config.load_autoconfig(False)',
+ '1/0']), encoding='utf-8')
with pytest.raises(cmdutils.CommandError) as excinfo:
commands.config_source()
@@ -571,7 +575,8 @@ class TestEdit:
def test_with_sourcing(self, commands, config_stub, patch_editor):
assert config_stub.val.content.javascript.enabled
- mock = patch_editor('c.content.javascript.enabled = False')
+ mock = patch_editor('\n'.join(['config.load_autoconfig(False)',
+ 'c.content.javascript.enabled = False']))
commands.config_edit()
@@ -580,16 +585,16 @@ class TestEdit:
def test_config_py_with_sourcing(self, commands, config_stub, patch_editor, config_py_arg):
assert config_stub.val.content.javascript.enabled
- conf = 'c.content.javascript.enabled = False'
- mock = patch_editor(conf)
+ conf = ['config.load_autoconfig(False)', 'c.content.javascript.enabled = False']
+ mock = patch_editor("\n".join(conf))
commands.config_edit()
mock.assert_called_once_with(unittest.mock.ANY)
assert not config_stub.val.content.javascript.enabled
- assert config_py_arg.read_text('utf-8').splitlines() == [conf]
+ assert config_py_arg.read_text('utf-8').splitlines() == conf
def test_error(self, commands, config_stub, patch_editor, message_mock,
caplog):
- patch_editor('c.foo = 42')
+ patch_editor('\n'.join(['config.load_autoconfig(False)', 'c.foo = 42']))
with caplog.at_level(logging.ERROR):
commands.config_edit()
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
index bef4ef004..11808e2c2 100644
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -659,6 +659,7 @@ class ConfPy:
def __init__(self, tmpdir, filename: str = "config.py"):
self._file = tmpdir / filename
self.filename = str(self._file)
+ config.instance.warn_autoconfig = False
def write(self, *lines):
text = '\n'.join(lines)
@@ -954,6 +955,19 @@ class TestConfigPy:
# points at the location *after* the +
assert " ^" in tblines or " ^" in tblines
+ def test_load_autoconfig_warning(self, confpy):
+ confpy.write('')
+ config.instance.warn_autoconfig = True
+ with pytest.raises(configexc.ConfigFileErrors) as excinfo:
+ configfiles.read_config_py(confpy.filename)
+ assert len(excinfo.value.errors) == 1
+ error = excinfo.value.errors[0]
+ assert error.text == "autoconfig loading not specified"
+ exception_text = ('Your config.py should call either `config.load_autoconfig()`'
+ ' (to load settings configured via the GUI) or '
+ '`config.load_autoconfig(False)` (to not do so)')
+ assert str(error.exception) == exception_text
+
def test_unhandled_exception(self, confpy):
confpy.write("1/0")
error = confpy.read(error=True)
@@ -1144,8 +1158,8 @@ class TestConfigPyWriter:
# qute://help/configuring.html
# qute://help/settings.html
- # Uncomment this to still load settings configured via autoconfig.yml
- # config.load_autoconfig()
+ # Change the argument to True to still load settings configured via autoconfig.yml
+ config.load_autoconfig(False)
# This is an option description. Nullam eu ante vel est convallis
# dignissim. Fusce suscipit, wisi nec facilisis facilisis, est dui
@@ -1197,7 +1211,7 @@ class TestConfigPyWriter:
lines = list(writer._gen_lines())
assert "## Autogenerated config.py" in lines
- assert "# config.load_autoconfig()" in lines
+ assert "# config.load_autoconfig(True)" in lines
assert "# c.opt = 'val'" in lines
assert "## Bindings for normal mode" in lines
assert "# config.bind(',x', 'message-info normal')" in lines
@@ -1246,7 +1260,7 @@ class TestConfigPyWriter:
commented=False)
lines = list(writer._gen_lines())
assert lines[0] == '# Autogenerated config.py'
- assert lines[-2] == '# config.load_autoconfig()'
+ assert lines[-2] == 'config.load_autoconfig(False)'
assert not lines[-1]
def test_pattern(self):
diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py
index 8381456e1..6b44196b6 100644
--- a/tests/unit/config/test_configinit.py
+++ b/tests/unit/config/test_configinit.py
@@ -63,7 +63,8 @@ def configdata_init(monkeypatch):
class TestEarlyInit:
def test_config_py_path(self, args, init_patch, config_py_arg):
- config_py_arg.write('c.colors.hints.bg = "red"\n')
+ config_py_arg.write('\n'.join(['config.load_autoconfig()',
+ 'c.colors.hints.bg = "red"']))
configinit.early_init(args)
expected = 'colors.hints.bg = red'
assert config.instance.dump_userconfig() == expected
@@ -75,7 +76,8 @@ class TestEarlyInit:
config_py_file = config_tmpdir / 'config.py'
if config_py:
- config_py_lines = ['c.colors.hints.bg = "red"']
+ config_py_lines = ['c.colors.hints.bg = "red"',
+ 'config.load_autoconfig(False)']
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
@@ -147,8 +149,7 @@ class TestEarlyInit:
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
- if load_autoconfig:
- config_py_lines.append('config.load_autoconfig()')
+ config_py_lines.append('config.load_autoconfig({})'.format(load_autoconfig))
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
@@ -310,6 +311,7 @@ class TestLateInit:
elif method == 'py':
config_py_file = config_tmpdir / 'config.py'
lines = ["c.{} = '{}'".format(k, v) for k, v in settings]
+ lines.append("config.load_autoconfig(False)")
config_py_file.write_text('\n'.join(lines), 'utf-8', ensure=True)
configinit.early_init(args)
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index a98584164..b637ec13c 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -19,6 +19,7 @@
"""Tests for qutebrowser.config.configtypes."""
import re
+import sys
import json
import math
import warnings
@@ -248,16 +249,11 @@ class TestAll:
configtypes.PercOrInt, # ditto
]:
return
- elif (isinstance(typ, functools.partial) and
- isinstance(typ.func, (configtypes.ListOrValue,
- configtypes.List))):
+ elif (isinstance(klass, functools.partial) and
+ klass.func in [configtypes.ListOrValue, configtypes.List]):
# ListOrValue: "- /" -> "/"
# List: "- /" -> ["/"]
return
- elif (isinstance(typ, configtypes.ListOrValue) and
- isinstance(typ.valtype, configtypes.Int)):
- # "00" -> "0"
- return
assert converted == s
@@ -1487,27 +1483,19 @@ class TestRegex:
@pytest.mark.parametrize('val', [
pytest.param(r'(foo|bar))?baz[fis]h', id='unmatched parens'),
pytest.param('(' * 500, id='too many parens'),
+ pytest.param(r'foo\Xbar', id='invalid escape X'),
+ pytest.param(r'foo\Cbar', id='invalid escape C'),
+ pytest.param(r'[[]]', id='nested set', marks=pytest.mark.skipif(
+ sys.hexversion < 0x03070000,
+ reason="Warning was added in Python 3.7")),
+ pytest.param(r'[a||b]', id='set operation', marks=pytest.mark.skipif(
+ sys.hexversion < 0x03070000,
+ reason="Warning was added in Python 3.7")),
])
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().to_py(val)
- @pytest.mark.parametrize('val', [
- r'foo\Xbar',
- r'foo\Cbar',
- ])
- def test_to_py_maybe_valid(self, klass, val):
- """Those values are valid on some Python versions (and systems?).
-
- On others, they raise a DeprecationWarning because of an invalid
- escape. This tests makes sure this gets translated to a
- ValidationError.
- """
- try:
- klass().to_py(val)
- except configexc.ValidationError:
- pass
-
@pytest.mark.parametrize('warning', [
Warning('foo'), DeprecationWarning('foo'),
])
@@ -1523,20 +1511,6 @@ class TestRegex:
with pytest.raises(type(warning)):
regex.to_py('foo')
- def test_bad_pattern_warning(self, mocker, klass):
- """Test a simulated bad pattern warning.
-
- This only seems to happen with Python 3.5, so we simulate this for
- better coverage.
- """
- regex = klass()
- m = mocker.patch('qutebrowser.config.configtypes.re')
- m.compile.side_effect = lambda *args: warnings.warn(r'bad escape \C',
- DeprecationWarning)
- m.error = re.error
- with pytest.raises(configexc.ValidationError):
- regex.to_py('foo')
-
@pytest.mark.parametrize('flags, expected', [
(None, 0),
('IGNORECASE', re.IGNORECASE),
diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py
index 0b3cc7c2b..a3a7f910d 100644
--- a/tests/unit/config/test_qtargs.py
+++ b/tests/unit/config/test_qtargs.py
@@ -22,8 +22,8 @@ import os
import pytest
from qutebrowser import qutebrowser
-from qutebrowser.config import qtargs, configdata
-from qutebrowser.utils import usertypes, version
+from qutebrowser.config import qtargs
+from qutebrowser.utils import usertypes
from helpers import utils
@@ -134,18 +134,24 @@ class TestQtArgs:
assert '--disable-in-process-stack-traces' in args
assert '--enable-in-process-stack-traces' not in args
- @pytest.mark.parametrize('flags, added', [
- ([], False),
- (['--debug-flag', 'chromium'], True),
+ @pytest.mark.parametrize('flags, args', [
+ ([], []),
+ (['--debug-flag', 'chromium'], ['--enable-logging', '--v=1']),
+ (['--debug-flag', 'wait-renderer-process'], ['--renderer-startup-dialog']),
])
- def test_chromium_debug(self, monkeypatch, parser, flags, added):
+ def test_chromium_flags(self, monkeypatch, parser, flags, args):
monkeypatch.setattr(qtargs.objects, 'backend',
usertypes.Backend.QtWebEngine)
parsed = parser.parse_args(flags)
args = qtargs.qt_args(parsed)
- for arg in ['--enable-logging', '--v=1']:
- assert (arg in args) == added
+ if args:
+ for arg in args:
+ assert arg in args
+ else:
+ assert '--enable-logging' not in args
+ assert '--v=1' not in args
+ assert '--renderer-startup-dialog' not in args
@pytest.mark.parametrize('config, added', [
('none', False),
@@ -381,125 +387,23 @@ class TestQtArgs:
assert combined_flag in args
assert overlay_flag not in args
- @utils.qt514
+ @utils.qt510
def test_blink_settings(self, config_stub, monkeypatch, parser):
+ from qutebrowser.browser.webengine import darkmode
monkeypatch.setattr(qtargs.objects, 'backend',
usertypes.Backend.QtWebEngine)
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- True)
+ monkeypatch.setattr(darkmode, '_variant',
+ lambda: darkmode.Variant.qt_515_2)
config_stub.val.colors.webpage.darkmode.enabled = True
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
- assert '--blink-settings=darkModeEnabled=true' in args
-
-
-class TestDarkMode:
+ expected = ('--blink-settings=forceDarkModeEnabled=true,'
+ 'forceDarkModeImagePolicy=2')
- pytestmark = utils.qt514
-
- @pytest.fixture(autouse=True)
- def patch_backend(self, monkeypatch):
- monkeypatch.setattr(qtargs.objects, 'backend',
- usertypes.Backend.QtWebEngine)
-
- @pytest.mark.parametrize('settings, new_qt, expected', [
- # Disabled
- ({}, True, []),
- ({}, False, []),
-
- # Enabled without customization
- (
- {'enabled': True},
- True,
- [('darkModeEnabled', 'true')]
- ),
- (
- {'enabled': True},
- False,
- [('darkMode', '4')]
- ),
-
- # Algorithm
- (
- {'enabled': True, 'algorithm': 'brightness-rgb'},
- True,
- [('darkModeEnabled', 'true'),
- ('darkModeInversionAlgorithm', '2')],
- ),
- (
- {'enabled': True, 'algorithm': 'brightness-rgb'},
- False,
- [('darkMode', '2')],
- ),
-
- ])
- def test_basics(self, config_stub, monkeypatch,
- settings, new_qt, expected):
- for k, v in settings.items():
- config_stub.set_obj('colors.webpage.darkmode.' + k, v)
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- new_qt)
-
- assert list(qtargs._darkmode_settings()) == expected
-
- @pytest.mark.parametrize('setting, value, exp_key, exp_val', [
- ('contrast', -0.5,
- 'darkModeContrast', '-0.5'),
- ('policy.page', 'smart',
- 'darkModePagePolicy', '1'),
- ('policy.images', 'smart',
- 'darkModeImagePolicy', '2'),
- ('threshold.text', 100,
- 'darkModeTextBrightnessThreshold', '100'),
- ('threshold.background', 100,
- 'darkModeBackgroundBrightnessThreshold', '100'),
- ('grayscale.all', True,
- 'darkModeGrayscale', 'true'),
- ('grayscale.images', 0.5,
- 'darkModeImageGrayscale', '0.5'),
- ])
- def test_customization(self, config_stub, monkeypatch,
- setting, value, exp_key, exp_val):
- config_stub.val.colors.webpage.darkmode.enabled = True
- config_stub.set_obj('colors.webpage.darkmode.' + setting, value)
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- True)
-
- expected = [('darkModeEnabled', 'true'), (exp_key, exp_val)]
- assert list(qtargs._darkmode_settings()) == expected
-
- def test_new_chromium(self):
- """Fail if we encounter an unknown Chromium version.
-
- Dark mode in Chromium currently is undergoing various changes (as it's
- relatively recent), and Qt 5.15 is supposed to update the underlying
- Chromium at some point.
-
- Make this test fail deliberately with newer Chromium versions, so that
- we can test whether dark mode still works manually, and adjust if not.
- """
- assert version._chromium_version() in [
- 'unavailable', # QtWebKit
- '77.0.3865.129', # Qt 5.14
- '80.0.3987.163', # Qt 5.15
- ]
-
- def test_options(self, configdata_init):
- """Make sure all darkmode options have the right attributes set."""
- for name, opt in configdata.DATA.items():
- if not name.startswith('colors.webpage.darkmode.'):
- continue
-
- backends = {'QtWebEngine': 'Qt 5.14', 'QtWebKit': False}
- assert not opt.supports_pattern, name
- assert opt.restart, name
- assert opt.raw_backends == backends, name
+ assert expected in args
class TestEnvVars:
diff --git a/tests/unit/javascript/stylesheet/test_appendchild.js b/tests/unit/javascript/stylesheet/test_appendchild.js
index d1deadba6..aa1294cb3 100644
--- a/tests/unit/javascript/stylesheet/test_appendchild.js
+++ b/tests/unit/javascript/stylesheet/test_appendchild.js
@@ -9,37 +9,37 @@ var iframe, object;
// svg iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '1' };
-iframe.src = "svg.xml";
+// iframe.src = "svg.xml";
kungFuDeathGrip.appendChild(iframe);
// object iframe
object = document.createElement('object');
object.onload = function () { kungFuDeathGrip.title += '2' };
-object.data = "svg.xml";
+// object.data = "svg.xml";
kungFuDeathGrip.appendChild(object);
// xml iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '3' };
-iframe.src = "empty.xml";
+// iframe.src = "empty.xml";
kungFuDeathGrip.appendChild(iframe);
// html iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '4' };
-iframe.src = "empty.html";
+// iframe.src = "empty.html";
kungFuDeathGrip.appendChild(iframe);
// html iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '5' };
-iframe.src = "xhtml.1";
+// iframe.src = "xhtml.1";
kungFuDeathGrip.appendChild(iframe);
// html iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '6' };
-iframe.src = "xhtml.2";
+// iframe.src = "xhtml.2";
kungFuDeathGrip.appendChild(iframe);
// html iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '7' };
-iframe.src = "xhtml.3";
+// iframe.src = "xhtml.3";
kungFuDeathGrip.appendChild(iframe);
// add the lot to the document
diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py
index 5202efd07..2d4da12e8 100644
--- a/tests/unit/misc/test_checkpyver.py
+++ b/tests/unit/misc/test_checkpyver.py
@@ -28,21 +28,22 @@ import pytest
from qutebrowser.misc import checkpyver
-TEXT = (r"At least Python 3.5.2 is required to run qutebrowser, but it's "
+TEXT = (r"At least Python 3.6 is required to run qutebrowser, but it's "
r"running with \d+\.\d+\.\d+.")
@pytest.mark.not_frozen
-def test_python2():
- """Run checkpyver with python 2."""
+@pytest.mark.parametrize('python', ['python2', 'python3.5'])
+def test_old_python(python):
+ """Run checkpyver with old python versions."""
try:
proc = subprocess.run(
- ['python2', checkpyver.__file__, '--no-err-windows'],
+ [python, checkpyver.__file__, '--no-err-windows'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False)
except FileNotFoundError:
- pytest.skip("python2 not found")
+ pytest.skip(f"{python} not found")
assert not proc.stdout
stderr = proc.stderr.decode('utf-8').rstrip()
assert re.fullmatch(TEXT, stderr), stderr
diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py
index 323ac1b21..694e6d204 100644
--- a/tests/unit/misc/test_editor.py
+++ b/tests/unit/misc/test_editor.py
@@ -137,17 +137,6 @@ class TestFileHandling:
msg = message_mock.getmsg(usertypes.MessageLevel.error)
assert msg.text.startswith("Failed to read back edited file: ")
- @pytest.fixture
- def unwritable_tmp_path(self, tmp_path):
- tmp_path.chmod(0)
- if os.access(str(tmp_path), os.W_OK):
- # Docker container or similar
- pytest.skip("File was still writable")
-
- yield tmp_path
-
- tmp_path.chmod(0o755)
-
def test_unwritable(self, monkeypatch, message_mock, editor,
unwritable_tmp_path, caplog):
"""Test file handling when the initial file is not writable."""
diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py
index 3ae4c3cfc..8e8fa47a4 100644
--- a/tests/unit/utils/test_log.py
+++ b/tests/unit/utils/test_log.py
@@ -384,9 +384,9 @@ def test_stub(caplog, suffix, expected):
assert caplog.messages == [expected]
-def test_ignore_py_warnings(caplog):
+def test_py_warning_filter(caplog):
logging.captureWarnings(True)
- with log.ignore_py_warnings(category=UserWarning):
+ with log.py_warning_filter(category=UserWarning):
warnings.warn("hidden", UserWarning)
with caplog.at_level(logging.WARNING):
warnings.warn("not hidden", UserWarning)
@@ -395,6 +395,21 @@ def test_ignore_py_warnings(caplog):
assert msg.endswith("UserWarning: not hidden")
+def test_py_warning_filter_error(caplog):
+ warnings.simplefilter('ignore')
+ warnings.warn("hidden", UserWarning)
+
+ with log.py_warning_filter('error'):
+ with pytest.raises(UserWarning):
+ warnings.warn("error", UserWarning)
+
+
+def test_warning_still_errors():
+ # Mainly a sanity check after the tests messing with warnings above.
+ with pytest.raises(UserWarning):
+ warnings.warn("error", UserWarning)
+
+
class TestQtMessageHandler:
@attr.s
diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py
index 064c51b30..ea65b7cc4 100644
--- a/tests/unit/utils/test_standarddir.py
+++ b/tests/unit/utils/test_standarddir.py
@@ -308,14 +308,12 @@ class TestInitCacheDirTag:
# http://www.brynosaurus.com/cachedir/
""").lstrip()
- def test_open_oserror(self, caplog, tmpdir, mocker, monkeypatch):
+ def test_open_oserror(self, caplog, unwritable_tmp_path, monkeypatch):
"""Test creating a new CACHEDIR.TAG."""
- monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir))
- mocker.patch('builtins.open', side_effect=OSError)
+ monkeypatch.setattr(standarddir, 'cache', lambda: str(unwritable_tmp_path))
with caplog.at_level(logging.ERROR, 'init'):
standarddir._init_cachedir_tag()
assert caplog.messages == ['Failed to create CACHEDIR.TAG']
- assert not tmpdir.listdir()
class TestCreatingDir:
diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py
index 8292a09ad..c38794c40 100644
--- a/tests/unit/utils/test_urlmatch.py
+++ b/tests/unit/utils/test_urlmatch.py
@@ -28,7 +28,6 @@ Currently not tested:
- Any other features we don't need, such as .GetAsString() or set operations.
"""
-import sys
import string
import pytest
@@ -89,11 +88,7 @@ from qutebrowser.utils import urlmatch
("http://foo:/", "Invalid port: Port is empty"),
("http://*.foo:/", "Invalid port: Port is empty"),
("http://foo:com/", "Invalid port: .* 'com'"),
- pytest.param("http://foo:123456/",
- "Invalid port: Port out of range 0-65535",
- marks=pytest.mark.skipif(
- sys.hexversion < 0x03060000,
- reason="Doesn't show an error on Python 3.5")),
+ ("http://foo:123456/", "Invalid port: Port out of range 0-65535"),
("http://foo:80:80/monkey", "Invalid port: .* '80:80'"),
("chrome://foo:1234/bar", "Ports are unsupported with chrome scheme"),
# No port specified, but port separator.
diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py
index 7fd52152c..df9d8b510 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -745,9 +745,6 @@ class TestProxyFromUrl:
def test_proxy_from_url_valid(self, url, expected):
assert urlutils.proxy_from_url(QUrl(url)) == expected
- @pytest.mark.qt_log_ignore(
- r'^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not '
- r'de-queue request, failed to report HostNotFoundError')
@pytest.mark.parametrize('scheme', ['pac+http', 'pac+https'])
def test_proxy_from_url_pac(self, scheme, qapp):
fetcher = urlutils.proxy_from_url(QUrl('{}://foo'.format(scheme)))
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
index 3eda4234f..8a07e3411 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -37,6 +37,7 @@ from PyQt5.QtGui import QColor, QClipboard
import pytest
import hypothesis
from hypothesis import strategies
+import yaml
import qutebrowser
import qutebrowser.utils # for test_qualname
@@ -560,8 +561,12 @@ class TestIsEnum:
def test_enum(self):
"""Test is_enum with an enum."""
- e = enum.Enum('Foo', 'bar, baz')
- assert utils.is_enum(e)
+ class Foo(enum.Enum):
+
+ bar = enum.auto()
+ baz = enum.auto()
+
+ assert utils.is_enum(Foo)
def test_class(self):
"""Test is_enum with a non-enum class."""
@@ -852,6 +857,10 @@ class TestYaml:
def test_load(self):
assert utils.yaml_load("[1, 2]") == [1, 2]
+ def test_load_float_bug(self):
+ with pytest.raises(yaml.YAMLError):
+ utils.yaml_load("._")
+
def test_load_file(self, tmpdir):
tmpfile = tmpdir / 'foo.yml'
tmpfile.write('[1, 2]')
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 868c4920f..c76a22e56 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -41,7 +41,7 @@ import hypothesis.strategies
import qutebrowser
from qutebrowser.config import config
from qutebrowser.utils import version, usertypes, utils, standarddir
-from qutebrowser.misc import pastebin
+from qutebrowser.misc import pastebin, objects
from qutebrowser.browser import pdfjs
@@ -871,39 +871,49 @@ _QTWE_USER_AGENT = ("Mozilla/5.0 (X11; Linux x86_64) "
"QtWebEngine/5.14.0 Chrome/{} Safari/537.36")
-def test_chromium_version(monkeypatch, caplog):
- pytest.importorskip('PyQt5.QtWebEngineWidgets')
+class TestChromiumVersion:
- ver = '77.0.3865.98'
- version.webenginesettings._init_user_agent_str(
- _QTWE_USER_AGENT.format(ver))
+ @pytest.fixture(autouse=True)
+ def clear_parsed_ua(self, monkeypatch):
+ if version.webenginesettings is not None:
+ # Not available with QtWebKit
+ monkeypatch.setattr(version.webenginesettings, 'parsed_user_agent', None)
- assert version._chromium_version() == ver
+ def test_fake_ua(self, monkeypatch, caplog):
+ pytest.importorskip('PyQt5.QtWebEngineWidgets')
+ ver = '77.0.3865.98'
+ version.webenginesettings._init_user_agent_str(
+ _QTWE_USER_AGENT.format(ver))
-def test_chromium_version_no_webengine(monkeypatch):
- monkeypatch.setattr(version, 'webenginesettings', None)
- assert version._chromium_version() == 'unavailable'
+ assert version._chromium_version() == ver
+ def test_no_webengine(self, monkeypatch):
+ monkeypatch.setattr(version, 'webenginesettings', None)
+ assert version._chromium_version() == 'unavailable'
-def test_chromium_version_prefers_saved_user_agent(monkeypatch):
- pytest.importorskip('PyQt5.QtWebEngineWidgets')
- version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT)
+ def test_prefers_saved_user_agent(self, monkeypatch):
+ pytest.importorskip('PyQt5.QtWebEngineWidgets')
+ version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT)
- class FakeProfile:
- def defaultProfile(self):
- raise AssertionError("Should not be called")
+ class FakeProfile:
+ def defaultProfile(self):
+ raise AssertionError("Should not be called")
- monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile',
- FakeProfile())
+ monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile',
+ FakeProfile())
- version._chromium_version()
+ version._chromium_version()
+ def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub):
+ pytest.importorskip('PyQt5.QtWebEngineWidgets')
+ unexpected = ['', 'unknown', 'unavailable', 'avoided']
+ assert version._chromium_version() not in unexpected
-def test_chromium_version_unpatched(qapp, cache_tmpdir, data_tmpdir,
- config_stub):
- pytest.importorskip('PyQt5.QtWebEngineWidgets')
- assert version._chromium_version() not in ['', 'unknown', 'unavailable']
+ def test_avoided(self, monkeypatch):
+ pytest.importorskip('PyQt5.QtWebEngineWidgets')
+ monkeypatch.setattr(objects, 'debug_flags', ['avoid-chromium-init'])
+ assert version._chromium_version() == 'avoided'
@attr.s
diff --git a/tox.ini b/tox.ini
index a25374672..bfc1f2b49 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,7 +18,6 @@ setenv =
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS
basepython =
py3: {env:PYTHON:python3}
- py35: {env:PYTHON:python3.5}
py36: {env:PYTHON:python3.6}
py37: {env:PYTHON:python3.7}
py38: {env:PYTHON:python3.8}