summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml17
-rw-r--r--.github/workflows/docker.yml61
-rw-r--r--.github/workflows/recompile-requirements.yml2
-rw-r--r--.pylintrc2
-rw-r--r--.yamllint1
-rw-r--r--doc/changelog.asciidoc87
-rw-r--r--doc/help/commands.asciidoc3
-rw-r--r--doc/help/settings.asciidoc17
-rw-r--r--misc/org.qutebrowser.qutebrowser.desktop2
-rw-r--r--misc/requirements/requirements-check-manifest.txt3
-rw-r--r--misc/requirements/requirements-dev.txt12
-rw-r--r--misc/requirements/requirements-flake8.txt4
-rw-r--r--misc/requirements/requirements-mypy.txt4
-rw-r--r--misc/requirements/requirements-pyinstaller.txt2
-rw-r--r--misc/requirements/requirements-pylint.txt6
-rw-r--r--misc/requirements/requirements-pyqt-5.15.0.txt5
-rw-r--r--misc/requirements/requirements-pyqt-5.15.0.txt-raw4
-rw-r--r--misc/requirements/requirements-pyqt-5.15.txt4
-rw-r--r--misc/requirements/requirements-pyqt.txt4
-rw-r--r--misc/requirements/requirements-pyqt.txt-raw2
-rw-r--r--misc/requirements/requirements-sphinx.txt11
-rw-r--r--misc/requirements/requirements-tests.txt10
-rw-r--r--misc/requirements/requirements-tests.txt-raw5
-rw-r--r--misc/requirements/requirements-tox.txt6
-rw-r--r--misc/requirements/requirements-tox.txt-raw2
-rw-r--r--misc/userscripts/README.md7
-rwxr-xr-xmisc/userscripts/dmenu_qutebrowser5
-rwxr-xr-xmisc/userscripts/format_json2
-rwxr-xr-xmisc/userscripts/kodi111
-rwxr-xr-xmisc/userscripts/qr8
-rwxr-xr-xmisc/userscripts/qutedmenu11
-rw-r--r--qutebrowser/api/downloads.py4
-rw-r--r--qutebrowser/browser/commands.py3
-rw-r--r--qutebrowser/browser/downloads.py4
-rw-r--r--qutebrowser/browser/navigate.py4
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py6
-rw-r--r--qutebrowser/browser/shared.py6
-rw-r--r--qutebrowser/browser/webengine/darkmode.py11
-rw-r--r--qutebrowser/browser/webengine/interceptor.py8
-rw-r--r--qutebrowser/completion/completiondelegate.py1
-rw-r--r--qutebrowser/completion/completionwidget.py3
-rw-r--r--qutebrowser/completion/models/listcategory.py1
-rw-r--r--qutebrowser/config/configdata.yml13
-rw-r--r--qutebrowser/config/configtypes.py185
-rw-r--r--qutebrowser/config/configutils.py58
-rw-r--r--qutebrowser/config/qtargs.py29
-rw-r--r--qutebrowser/javascript/.eslintrc.yaml2
-rw-r--r--qutebrowser/mainwindow/mainwindow.py6
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py4
-rw-r--r--qutebrowser/mainwindow/tabwidget.py2
-rw-r--r--qutebrowser/misc/crashdialog.py5
-rw-r--r--qutebrowser/misc/earlyinit.py17
-rw-r--r--qutebrowser/utils/qtutils.py108
-rw-r--r--qutebrowser/utils/urlutils.py7
-rw-r--r--qutebrowser/utils/usertypes.py16
-rw-r--r--qutebrowser/utils/utils.py114
-rw-r--r--qutebrowser/utils/version.py9
-rwxr-xr-xscripts/dev/build_release.py18
-rw-r--r--scripts/dev/ci/docker/Dockerfile.j227
-rw-r--r--scripts/dev/ci/docker/README.md9
-rw-r--r--scripts/dev/ci/docker/generate.py45
-rw-r--r--scripts/dev/misc_checks.py25
-rw-r--r--scripts/dev/recompile_requirements.py11
-rw-r--r--scripts/mkvenv.py225
-rw-r--r--tests/end2end/conftest.py2
-rw-r--r--tests/end2end/features/test_downloads_bdd.py6
-rw-r--r--tests/end2end/fixtures/testprocess.py12
-rw-r--r--tests/end2end/fixtures/webserver.py28
-rw-r--r--tests/end2end/fixtures/webserver_sub.py36
-rw-r--r--tests/end2end/templates/headers-link.html10
-rw-r--r--tests/end2end/test_invocations.py34
-rw-r--r--tests/end2end/test_mkvenv.py28
-rw-r--r--tests/unit/browser/test_history.py3
-rw-r--r--tests/unit/browser/test_navigate.py2
-rw-r--r--tests/unit/browser/webengine/test_darkmode.py16
-rw-r--r--tests/unit/browser/webkit/network/test_networkreply.py7
-rw-r--r--tests/unit/completion/test_completiondelegate.py9
-rw-r--r--tests/unit/completion/test_completionmodel.py4
-rw-r--r--tests/unit/completion/test_completionwidget.py1
-rw-r--r--tests/unit/completion/test_models.py12
-rw-r--r--tests/unit/config/test_config.py7
-rw-r--r--tests/unit/config/test_configtypes.py34
-rw-r--r--tests/unit/config/test_configutils.py43
-rw-r--r--tests/unit/config/test_qtargs.py34
-rw-r--r--tests/unit/keyinput/test_basekeyparser.py4
-rw-r--r--tests/unit/misc/test_earlyinit.py17
-rw-r--r--tests/unit/misc/test_guiprocess.py29
-rw-r--r--tests/unit/misc/test_ipc.py8
-rw-r--r--tests/unit/utils/test_qtutils.py129
-rw-r--r--tests/unit/utils/test_urlutils.py5
-rw-r--r--tests/unit/utils/test_utils.py122
-rw-r--r--tests/unit/utils/test_version.py17
-rw-r--r--tests/unit/utils/usertypes/test_question.py14
-rw-r--r--tox.ini25
94 files changed, 1463 insertions, 641 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c292d7986..5978f1f97 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,7 +13,7 @@ jobs:
linters:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
timeout-minutes: 10
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
@@ -90,7 +90,7 @@ jobs:
- uses: actions/checkout@v2
- name: Set up problem matchers
run: "python scripts/dev/ci/problemmatchers.py py38 ${{ runner.temp }}"
- - run: tox -e py38
+ - run: tox -e py
tests:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -112,6 +112,10 @@ jobs:
- testenv: py38-pyqt514
os: ubuntu-20.04
python: 3.8
+ ### PyQt 5.15.0 (Python 3.9)
+ - testenv: py39-pyqt5150
+ os: ubuntu-20.04
+ python: 3.9
### PyQt 5.15 (Python 3.9, with coverage)
- testenv: py39-pyqt515-cov
os: ubuntu-20.04
@@ -121,6 +125,11 @@ jobs:
os: macos-10.15
python: 3.7
args: "tests/unit" # Only run unit tests on macOS
+ ### macOS Big Sur
+ - testenv: py37-pyqt515
+ os: macos-11.0
+ python: 3.7
+ args: "tests/unit" # Only run unit tests on macOS
### Windows: PyQt 5.15 (Python 3.7 to match PyInstaller env)
- testenv: py37-pyqt515
os: windows-2019
@@ -164,7 +173,7 @@ jobs:
codeql:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
timeout-minutes: 30
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
steps:
- name: Checkout repository
uses: actions/checkout@v2
@@ -186,7 +195,7 @@ jobs:
irc:
timeout-minutes: 2
continue-on-error: true
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
needs: [linters, tests, tests-docker, codeql]
if: "always() && github.repository_owner == 'qutebrowser'"
steps:
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 000000000..03510ad6e
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,61 @@
+name: Rebuild Docker CI images
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: "23 5 * * *" # daily at 5:23
+
+jobs:
+ docker:
+ runs-on: ubuntu-20.04
+ strategy:
+ matrix:
+ image:
+ - archlinux-webkit
+ - archlinux-webengine
+ - archlinux-webengine-unstable
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: '3.x'
+ - run: pip install jinja2
+ - name: Generate Dockerfile
+ run: python3 generate.py ${{ matrix.image }}
+ working-directory: scripts/dev/ci/docker/
+ - uses: docker/setup-buildx-action@v1
+ - uses: docker/login-action@v1
+ with:
+ username: qutebrowser
+ password: ${{ secrets.DOCKER_TOKEN }}
+ - uses: docker/build-push-action@v2
+ with:
+ file: scripts/dev/ci/docker/Dockerfile
+ context: .
+ tags: "qutebrowser/ci:${{ matrix.image }}"
+ push: ${{ github.ref == 'refs/heads/master' }}
+
+ irc:
+ timeout-minutes: 2
+ continue-on-error: true
+ runs-on: ubuntu-20.04
+ needs: [docker]
+ if: "always() && github.repository == 'qutebrowser/qutebrowser'"
+ steps:
+ - name: Send success IRC notification
+ uses: Gottox/irc-message-action@v1.1
+ if: "needs.docker.result == 'success'"
+ with:
+ server: chat.freenode.net
+ channel: '#qutebrowser-dev'
+ 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.1
+ if: "needs.docker.result != 'success'"
+ with:
+ server: chat.freenode.net
+ channel: '#qutebrowser-dev'
+ nickname: qutebrowser-bot
+ 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 }}"
diff --git a/.github/workflows/recompile-requirements.yml b/.github/workflows/recompile-requirements.yml
index c41f67810..c939aa81d 100644
--- a/.github/workflows/recompile-requirements.yml
+++ b/.github/workflows/recompile-requirements.yml
@@ -31,7 +31,7 @@ jobs:
run: "python3 scripts/dev/recompile_requirements.py ${{ github.events.input.environments }}"
id: requirements
- name: Create pull request
- uses: peter-evans/create-pull-request@v2
+ uses: peter-evans/create-pull-request@v3
with:
committer: qutebrowser bot <bot@qutebrowser.org>
author: qutebrowser bot <bot@qutebrowser.org>
diff --git a/.pylintrc b/.pylintrc
index 2d7cbc430..eb77aa2d5 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -74,7 +74,7 @@ valid-metaclass-classmethod-first-arg=cls
[TYPECHECK]
ignored-modules=PyQt5,PyQt5.QtWebKit
-ignored-classes=DummyBox
+ignored-classes=DummyBox,__cause__
[IMPORTS]
known-third-party=sip
diff --git a/.yamllint b/.yamllint
index 638c16210..8e4d4a388 100644
--- a/.yamllint
+++ b/.yamllint
@@ -9,6 +9,7 @@ ignore: |
rules:
document-start: disable
line-length:
+ max: 88
ignore: |
/.github/*.yml
/.github/workflows/*.yml
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index f7ba4b42f..9cfa73806 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -35,6 +35,7 @@ Major changes
at the time of writing, it's recommended to
https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc#installing-qutebrowser-with-virtualenv[install qutebrowser in a virtualenv]
with a newer version of Qt/PyQt.
+- Windows 7 is not supported anymore by the Windows binaries.
- The (formerly optional) `cssutils` dependency is now removed. It was only
needed for improved behavior in corner cases when using `:download --mhtml`
with the (non-default) QtWebKit backend, and as such it's unlikely anyone is
@@ -56,12 +57,21 @@ Removed
- The `:inspector` command which was deprecated in v1.13.0 (in favor of
`:devtools`) is now removed.
+Added
+~~~~~
+
+- When QtWebEngine has been updated but PyQtWebEngine hasn't yet, the dark mode
+ settings might stop working. As a (currently undocumented) escape hatch, this
+ version adds a `QUTE_DARKMODE_VARIANT=qt_515_2` environment variable which can
+ be set to get the correct behavior in (transitive) situations like this.
+
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.
+- (TODO) Windows and macOS releases now ship Python 3.9 rather than 3.7.
- The `colors.webpage.darkmode.*` settings are now also supported with older Qt
versions (Qt 5.12 and 5.13) rather than just with Qt 5.14 and above.
- For regexes in the config (`hints.{prev,next}_regexes`), certain patterns
@@ -77,39 +87,98 @@ Changed
Fixed
~~~~~
-- The `open_url_instance.sh` userscript now complains when `socat` is not
- installed, rather than silencing the error.
- With interpolated color settings (`colors.tabs.indicator.*` and
`colors.downloads.*`), the alpha channel is now handled correctly.
v1.14.1 (unreleased)
--------------------
+Added
+~~~~~
+
+- With v1.14.0, qutebrowser configures the main window to be transparent, so
+ that it's possible to configure a translucent tab- or statusbar. However, that
+ change introduced various issues, such as performance degradation on some
+ systems or breaking dmenu window embedding with its `-w` option. To avoid those
+ issues for people who are not using transparency, the default behavior is
+ reverted to versions before v1.14.0 in this release. A new `window.transparent`
+ setting can be set to `true` to restore the behavior of v1.14.0.
+
Changed
~~~~~~~
-- (TODO) Windows and macOS releases now ship Qt 5.15.2, which is based on
- Chromium 83.0.4103.122 with security fixes up to 86.0.4240.111. This includes
+- Windows and macOS releases now ship Qt 5.15.2, which is based on
+ Chromium 83.0.4103.122 with security fixes up to 86.0.4240.183. This includes
CVE-2020-15999 in the bundled freetype library, which is known to be exploited
in the wild. It also includes various other bugfixes/features compared to
Qt 5.15.0 included in qutebrowser v1.14.0, such as:
* Correct handling of AltGr on Windows
* Fix for `content.cookies.accept` not working properly
- * Proper support for screen sharing
+ * Fixes for screen sharing (some websites are still broken until an upcoming Qt
+ 5.15.3)
* Support for FIDO U2F / WebAuth
* Fix for the unwanted creation of directories such as `databases-incognito` in
the home directory
* Proper autocompletion in the devtools console
* Proper signalisation of a tab's audible status (`[A]`)
+ * Fix for a hang when opening the context menu on macOS Big Sur (11.0)
* Hardware accelerated graphics on macOS
Fixed
~~~~~
-- Fix for a crash introduced in v1.14.0 when closing qutebrowser after opening a
- download with PDF.js.
-- New site-specific quirk to polyfill `Object.fromEntries` on Qt 5.12, thus
- fixing https://www.vr.fi/en and possibly other websites.
+- Setting the `content.headers.referer` setting to `same-domain` (the default)
+ was supposed to truncate referers to only the host with QtWebEngine.
+ Unfortunately, this functionality broke in Qt 5.14. It works properly again
+ with this release, including a test so this won't happen again.
+- With QtWebEngine 5.15, setting the `content.headers.referer` setting to
+ `never` did still send referers. This is now fixed as well.
+- In v1.14.0, a regression was introduced, causing a crash when qutebrowser was
+ closed after opening a download with PDF.js. This is now fixed.
+- With Qt 5.12, the `Object.fromEntries` JavaScript API is unavailable (it was
+ introduced in Chromium 73, while Qt 5.12 is based on 69). This caused
+ https://www.vr.fi/en and possibly other websites to break when accessed with Qt
+ 5.12. A suitable polyfill is now included with qutebrowser if
+ `content.site_specific_quirks` is enabled (which is the default).
+- While XDG startup notifications (e.g. launch feedback via the bouncy cursor
+ in KDE Plasma) were supported ever since Qt 5.1, qutebrowser's desktop file
+ accidentally declared that it wasn't supported. This is now fixed.
+- The `dmenu_qutebrowser` and `qutedmenu` userscripts now correctly read the
+ qutebrowser sqlite history which has been in use since v1.0.0.
+- With Python 3.8+ and vertical tabs, a deprecation warning for an implicit int
+ conversion was shown. This is now fixed.
+- Ever since Qt 5.11, fetching more completion data when that data is loaded
+ lazily (such as with history) and the last visible item is selected was broken.
+ The exact reason is currently unknown, but this release adds a tenative fix.
+- When PgUp/PgDown were used to go beyond the last visible item, the above issue
+ caused a crash, which is now also fixed.
+- As a workaround for an overzealous Microsoft Defender false-positive detecting
+ a "trojan" in the (unprocessed) adblock list, `:adblock-update` now doesn't
+ cache the HTTP response anymore.
+- With the QtWebKit backend and `content.headers` set to `same-domain` (the
+ default), origins with the same domain but different schemes or ports were
+ treated as the same domain. They now are correctly treated as different domains.
+- When a URL path uses percent escapes (such as
+ `https://example.com/embedded%2Fpath`), using `:navigate up` would treat the
+ `%2F` as a path separator and replace any remaining percent escapes by their
+ unescaped equivalents. Those are now handled correctly.
+- On macOS 11.0 (Big Sur), the default monospace font name caused a parsing error, thus
+ resulting in broken styling for the completion, hints, and other UI components.
+ They now look properly again.
+- Due to a Qt bug, installing Qt/PyQt from prebuilt binaries on systems with a
+ very old `libxcb-utils` version (notably, Debian Stable, but not Ubuntu since
+ 16.04 LTS) results in a setup which fails to start. This also affects the
+ `mkvenv.py` script, which now includes a workaround for this case.
+- The `open_url_instance.sh` userscript now complains when `socat` is not
+ installed, rather than silencing the error.
+- The example AppArmor profile in `misc/` was outdated and written for the
+ older QtWebKit backend. It is now updated to serve as an useful starting
+ point with QtWebEngine.
+- When running `:devtools` on Fedora without the needed (optional) dependency
+ installed, it was suggested to install `qt5-webengine-devtools`, which does
+ not, in fact, exist. It's now correctly suggested to install
+ `qt5-qtwebengine-devtools` instead.
+- Minor performance improvements.
- (TODO) Fix for various functionality breaking in private windows with v1.14.0,
after the last private window is closed. This includes:
* Ad blocking
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index 646d2f27b..0714b4fa1 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -1310,7 +1310,8 @@ Note that the command is *not* run in a shell, so things like `$VAR` or `> outpu
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
* +*-o*+, +*--output*+: Show the output in a new tab.
* +*-m*+, +*--output-messages*+: Show the output as messages.
-* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
+* +*-d*+, +*--detach*+: Detach the command from qutebrowser so that it continues running when qutebrowser quits.
+
==== count
Given to userscripts as $QUTE_COUNT.
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index ddd5df44c..309f1ab1d 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -327,6 +327,7 @@
|<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`.
|<<window.hide_decoration,window.hide_decoration>>|Hide the window decoration.
|<<window.title_format,window.title_format>>|Format to use for the window title. The same placeholders like for
+|<<window.transparent,window.transparent>>|Set the main window background to transparent.
|<<zoom.default,zoom.default>>|Default zoom level.
|<<zoom.levels,zoom.levels>>|Available zoom levels.
|<<zoom.mouse_divider,zoom.mouse_divider>>|Number of zoom increments to divide the mouse wheel movements to.
@@ -4206,6 +4207,22 @@ Type: <<types,FormatString>>
Default: +pass:[{perc}{current_title}{title_sep}qutebrowser]+
+[[window.transparent]]
+=== window.transparent
+Set the main window background to transparent.
+
+This allows having a transparent tab- or statusbar (might require a compositor such
+as picom). However, it breaks some functionality such as dmenu embedding via its
+`-w` option. On some systems, it was additionally reported that main window
+transparency negatively affects performance.
+
+Note this setting only affects windows opened after setting it.
+
+
+Type: <<types,Bool>>
+
+Default: +pass:[false]+
+
[[zoom.default]]
=== zoom.default
Default zoom level.
diff --git a/misc/org.qutebrowser.qutebrowser.desktop b/misc/org.qutebrowser.qutebrowser.desktop
index a1deb319f..cf3ee0422 100644
--- a/misc/org.qutebrowser.qutebrowser.desktop
+++ b/misc/org.qutebrowser.qutebrowser.desktop
@@ -46,7 +46,7 @@ Type=Application
Categories=Network;WebBrowser;
Exec=qutebrowser %u
Terminal=false
-StartupNotify=false
+StartupNotify=true
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute;
Keywords=Browser
Actions=new-window;preferences;
diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt
index 8b8f6ba1a..e9376f0b1 100644
--- a/misc/requirements/requirements-check-manifest.txt
+++ b/misc/requirements/requirements-check-manifest.txt
@@ -2,8 +2,7 @@
build==0.1.0
check-manifest==0.45
-packaging==20.4
+packaging==20.7
pep517==0.9.1
pyparsing==2.4.7
-six==1.15.0
toml==0.10.2
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index 13956d51f..578ab2b64 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -2,7 +2,7 @@
bump2version==1.0.1
certifi==2020.11.8
-cffi==1.14.3
+cffi==1.14.4
chardet==3.0.4
colorama==0.4.4
cryptography==3.2.1
@@ -11,15 +11,15 @@ hunter==3.3.1
idna==2.10
jwcrypto==0.8
manhole==1.6.0
-packaging==20.4
+packaging==20.7
pycparser==2.20
Pympler==0.9
pyparsing==2.4.7
-PyQt-builder==1.5.0
+PyQt-builder==1.6.0
python-dateutil==2.8.1
-requests==2.24.0
-sip==5.4.0
+requests==2.25.0
+sip==5.5.0
six==1.15.0
toml==0.10.2
uritemplate==3.0.1
-# urllib3==1.25.11
+# urllib3==1.26.2
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index 8bad8cb2d..3427c0c69 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -2,11 +2,11 @@
attrs==20.3.0
flake8==3.8.4
-flake8-bugbear==20.1.4
+flake8-bugbear==20.11.1
flake8-builtins==1.5.3
flake8-comprehensions==3.3.0
flake8-copyright==0.2.2
-flake8-debugger==3.2.1
+flake8-debugger==4.0.0
flake8-deprecated==1.3
flake8-docstrings==1.5.0
flake8-future-import==0.4.6
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index 8509c5bea..a046a0b5e 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -1,10 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
diff-cover==4.0.1
-inflect==4.1.0
+inflect==5.0.2
Jinja2==2.11.2
jinja2-pluralize==0.3.0
-lxml==4.6.1
+lxml==4.6.2
MarkupSafe==1.1.1
mypy==0.790
mypy-extensions==0.4.3
diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt
index a48e80fde..b1a3e98ee 100644
--- a/misc/requirements/requirements-pyinstaller.txt
+++ b/misc/requirements/requirements-pyinstaller.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.17
-pyinstaller==4.0
+pyinstaller==4.1
pyinstaller-hooks-contrib==2020.10
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index d82495cb9..e3856a40a 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -2,7 +2,7 @@
astroid==2.3.3 # rq.filter: < 2.4
certifi==2020.11.8
-cffi==1.14.3
+cffi==1.14.4
chardet==3.0.4
cryptography==3.2.1
github3.py==1.3.0
@@ -15,9 +15,9 @@ pycparser==2.20
pylint==2.4.4 # rq.filter: < 2.5
python-dateutil==2.8.1
./scripts/dev/pylint_checkers
-requests==2.24.0
+requests==2.25.0
six==1.15.0
typed-ast==1.4.1 ; python_version<"3.8"
uritemplate==3.0.1
-# urllib3==1.25.11
+# urllib3==1.26.2
wrapt==1.11.2
diff --git a/misc/requirements/requirements-pyqt-5.15.0.txt b/misc/requirements/requirements-pyqt-5.15.0.txt
new file mode 100644
index 000000000..53a3782ae
--- /dev/null
+++ b/misc/requirements/requirements-pyqt-5.15.0.txt
@@ -0,0 +1,5 @@
+# This file is automatically generated by scripts/dev/recompile_requirements.py
+
+PyQt5==5.15.2 # rq.filter: < 6
+PyQt5-sip==12.8.1
+PyQtWebEngine==5.15.0 # rq.filter: == 5.15.0
diff --git a/misc/requirements/requirements-pyqt-5.15.0.txt-raw b/misc/requirements/requirements-pyqt-5.15.0.txt-raw
new file mode 100644
index 000000000..a9d16f08f
--- /dev/null
+++ b/misc/requirements/requirements-pyqt-5.15.0.txt-raw
@@ -0,0 +1,4 @@
+#@ filter: PyQt5 < 6
+#@ filter: PyQtWebEngine == 5.15.0
+PyQt5 >= 5.15, < 6
+PyQtWebEngine == 5.15.0
diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt
index 21745c814..e791bb323 100644
--- a/misc/requirements/requirements-pyqt-5.15.txt
+++ b/misc/requirements/requirements-pyqt-5.15.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-PyQt5==5.15.1 # rq.filter: < 6
+PyQt5==5.15.2 # rq.filter: < 6
PyQt5-sip==12.8.1
-PyQtWebEngine==5.15.1 # rq.filter: < 6
+PyQtWebEngine==5.15.2 # rq.filter: < 6
diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt
index 148e8d8bb..ec6cfd810 100644
--- a/misc/requirements/requirements-pyqt.txt
+++ b/misc/requirements/requirements-pyqt.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-PyQt5==5.15.1
+PyQt5==5.15.2
PyQt5-sip==12.8.1
-PyQtWebEngine==5.15.0
+PyQtWebEngine==5.15.2
diff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw
index 83ebc7671..9c6afbf16 100644
--- a/misc/requirements/requirements-pyqt.txt-raw
+++ b/misc/requirements/requirements-pyqt.txt-raw
@@ -1,2 +1,2 @@
PyQt5
-PyQtWebEngine!=5.15.1
+PyQtWebEngine
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index 83184aa09..164311235 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
alabaster==0.7.12
-Babel==2.8.0
+Babel==2.9.0
certifi==2020.11.8
chardet==3.0.4
docutils==0.16
@@ -9,18 +9,17 @@ idna==2.10
imagesize==1.2.0
Jinja2==2.11.2
MarkupSafe==1.1.1
-packaging==20.4
+packaging==20.7
Pygments==2.7.2
pyparsing==2.4.7
pytz==2020.4
-requests==2.24.0
-six==1.15.0
+requests==2.25.0
snowballstemmer==2.0.0
-Sphinx==3.3.0
+Sphinx==3.3.1
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.4
-urllib3==1.25.11
+urllib3==1.26.2
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 21b290737..bd77427d4 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -5,7 +5,7 @@ attrs==20.3.0
beautifulsoup4==4.9.3
certifi==2020.11.8
chardet==3.0.4
-cheroot==8.4.5
+cheroot==8.4.7
click==7.1.2
# colorama==0.4.4
coverage==5.3
@@ -15,7 +15,7 @@ filelock==3.0.12
Flask==1.1.2
glob2==0.7
hunter==3.3.1
-hypothesis==5.41.2
+hypothesis==5.41.4
icdiff==1.9.1
idna==2.10
iniconfig==1.1.1
@@ -26,7 +26,7 @@ Mako==1.1.3
manhole==1.6.0
# MarkupSafe==1.1.1
more-itertools==8.6.0
-packaging==20.4
+packaging==20.7
parse==1.18.0
parse-type==0.5.2
pluggy==0.13.1
@@ -50,13 +50,13 @@ pytest-rerunfailures==9.1.1
pytest-xdist==2.1.0
pytest-xvfb==2.0.0
PyVirtualDisplay==1.3.2
-requests==2.24.0
+requests==2.25.0
requests-file==1.5.1
six==1.15.0
sortedcontainers==2.3.0
soupsieve==2.0.1
termcolor==1.1.0
-tldextract==3.0.2
+tldextract==3.1.0
toml==0.10.2
urllib3==1.25.11
vulture==2.1
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index fd346d475..0c9e3928f 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -1,5 +1,6 @@
beautifulsoup4
-cheroot
+# https://github.com/cherrypy/cheroot/issues/341
+cheroot!=8.4.8
coverage
Flask
hypothesis
@@ -33,5 +34,7 @@ pytest-clarity
# Needed to test misc/userscripts/qute-lastpass
tldextract
+# https://github.com/urllib3/urllib3/issues/2071
+urllib3!=1.26.0,!=1.26.1,!=1.26.2
#@ ignore: Jinja2, MarkupSafe, colorama
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 9241f07a6..86b3997f4 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -3,13 +3,11 @@
appdirs==1.4.4
distlib==0.3.1
filelock==3.0.12
-packaging==20.4
+packaging==20.7
pluggy==0.13.1
py==1.9.0
pyparsing==2.4.7
six==1.15.0
toml==0.10.2
tox==3.20.1
-tox-pip-version==0.0.7
-tox-venv==0.4.0
-virtualenv==20.1.0
+virtualenv==20.2.1
diff --git a/misc/requirements/requirements-tox.txt-raw b/misc/requirements/requirements-tox.txt-raw
index fab438034..053148f84 100644
--- a/misc/requirements/requirements-tox.txt-raw
+++ b/misc/requirements/requirements-tox.txt-raw
@@ -1,3 +1 @@
tox
-tox-venv
-tox-pip-version
diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md
index a17f7164c..669bfa664 100644
--- a/misc/userscripts/README.md
+++ b/misc/userscripts/README.md
@@ -24,7 +24,7 @@ The following userscripts are included in the current directory.
- [qutedmenu](./qutedmenu): Handle open -s && open -t with bemenu.
- [readability](./readability): Executes python-readability on current page and
opens the summary as new tab.
-- [readability-js](./readability-js): Processes the current page with the readability
+- [readability-js](./readability-js): Processes the current page with the readability
library used in Firefox Reader View and opens the summary as new tab.
- [ripbang](./ripbang): Adds DuckDuckGo bang as searchengine.
- [rss](./rss): Keeps track of URLs in RSS feeds and opens new ones.
@@ -32,6 +32,9 @@ The following userscripts are included in the current directory.
- [tor_identity](./tor_identity): Change your tor identity.
- [view_in_mpv](./view_in_mpv): Views the current web page in mpv using
sensible mpv-flags.
+- [qr](./qr): Show a QR code for the current webpage via
+ [qrencode](https://fukuchi.org/works/qrencode/).
+- [kodi](./kodi): Play videos in Kodi.
[castnow]: https://github.com/xat/castnow
[youtube-dl]: https://rg3.github.io/youtube-dl/
@@ -67,6 +70,8 @@ The following userscripts can be found on their own repositories.
and retrieve they when you want.
- [doi](https://github.com/cadadr/configuration/blob/master/qutebrowser/userscripts/doi):
Opens DOIs on Sci-Hub.
+- [1password](https://github.com/tomoakley/dotfiles/blob/master/qutebrowser/userscripts/1password):
+ Integration with 1password on macOS.
[Zotero]: https://www.zotero.org/
[Pocket]: https://getpocket.com/
diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser
index 84be1b619..57bdb805c 100755
--- a/misc/userscripts/dmenu_qutebrowser
+++ b/misc/userscripts/dmenu_qutebrowser
@@ -38,9 +38,10 @@
# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
#
-[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
-url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser)
+[ -z "$QUTE_URL" ] && QUTE_URL='https://duckduckgo.com'
+
+url=$(printf "%s\n%s" "$QUTE_URL" "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')" | cat "$QUTE_CONFIG_DIR/quickmarks" - | dmenu -l 15 -p qutebrowser)
url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url")
[ -z "${url// }" ] && exit
diff --git a/misc/userscripts/format_json b/misc/userscripts/format_json
index 541408c70..8a83c25fa 100755
--- a/misc/userscripts/format_json
+++ b/misc/userscripts/format_json
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -euo pipefail
#
# Behavior:
diff --git a/misc/userscripts/kodi b/misc/userscripts/kodi
new file mode 100755
index 000000000..63fcc81fe
--- /dev/null
+++ b/misc/userscripts/kodi
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+#
+# Behavior:
+# A qutebrowser userscript that plays Twitch, YouTube or Vimeo videos in Kodi via its
+# API.
+#
+# Requirements:
+# awk
+# bash
+# curl
+#
+# Kodi setup:
+# Settings -> Services -> Control
+# enable 'Allow remote control via HTTP'
+# set Username and Password
+# enable 'Allow remote control from applications on this system'
+# Optional yet recommended, setup SSL within Kodi over via a proxy webserver
+#
+# userscript setup:
+# create ~/.config/qutebrowser/kodi_rc with host and authentication information like:
+#
+# HOST="http://127.0.0.1:8080"
+# or
+# HOST="https://kodi.example.com"
+#
+# AUTH="user:password"
+# or
+# AUTH="bas64authenticationinformation"
+#
+# The base64 authentication is the output of
+# `echo -ne "user:password" |base64 --wrap 0`
+# reminder base64 is not encryption
+#
+# For vim users you might want to add '# vim: set nospell filetype=bash' to the
+# kodi_rc file.
+#
+# qutebrowser setup:
+# in ~/.config/qutebrowser/config.py add something like
+#
+# to send video link via hints:
+# config.bind('X', 'hint links userscript kodi')
+# to send current URL:
+# config.bind('X', 'spawn --userscript kodi')
+#
+# troubleshooting:
+# Errors detected within this userscript with have an exit of 231. All other exit
+# codes will come from curl or awk. To test that the kodi_rc file is set up
+# correctly, run the following command. It will display a 'It works!' notification within Kodi.
+#
+# source ~/.config/qutebrowser/kodi_rc ; curl --request POST "$HOST"/jsonrpc --header "Authorization: Basic $AUTH" --header "Content-Type: application/json" --data '{"id":1,"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"It works!","message":"both HOST and AUTH are correct"}}'
+#
+# In case you miss the notification in Kodi the successful response is:
+#
+# {"id":1,"jsonrpc":"2.0","result":"OK"}
+#
+# Note, curl will display errors for some problems, but not all.
+
+if [[ -z "$QUTE_FIFO" ]] ; then
+ echo "This script is designed to run as a qutebrowser userscript, not as a standalone script."
+ exit 231
+fi
+
+# configuration loading adapted from the password_fill userscript
+QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/}
+KODI_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/kodi_rc}
+if [[ -f "$KODI_CONFIG" ]] ; then
+ # shellcheck source=/dev/null
+ source "$KODI_CONFIG"
+ if [[ -z "$HOST" || -z "$AUTH" ]] ; then
+ echo "message-error 'HOST and/or AUTH not set in $KODI_CONFIG'" > "$QUTE_FIFO"
+ exit 231
+ fi
+else
+ echo "message-error '$KODI_CONFIG not found'" > "$QUTE_FIFO"
+ exit 231
+fi
+
+# get real URL from twitter links
+if [[ "$QUTE_URL" =~ ^https:\/\/t\.co ]] ; then
+ QUTE_URL=$(curl -o /dev/null --silent --head --write-out '%{redirect_url}' "$QUTE_URL" )
+fi
+
+# regex from https://github.com/dirkjanm/firefox-send-to-xbmc/blob/master/webextension/main.js
+if [[ "$QUTE_URL" =~ ^.*twitch.tv\/([a-zA-Z0-9_]+)$ ]] ; then
+ NAME="${BASH_REMATCH[1]}"
+ JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.twitch/?mode=play&channel_name='$NAME'"}},"id":"2"}'
+
+elif [[ "$QUTE_URL" =~ ^.*twitch.tv\/videos\/([0-9]+)$ ]] ; then
+ NAME="${BASH_REMATCH[1]}"
+ JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.twitch/?mode=play&video_id='$NAME'"}},"id":"2"}'
+
+elif [[ "$QUTE_URL" =~ ^.*vimeo.com\/([0-9]+) ]] ; then
+ NAME="${BASH_REMATCH[1]}"
+ JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.vimeo/play/?video_id='$NAME'"}},"id":"2"}'
+
+elif [[ "$QUTE_URL" =~ ^.*youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=([^#\&\?]*).* ]] ; then
+ NAME="${BASH_REMATCH[1]}"
+ JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.youtube/play/?video_id='$NAME'"}},"id":"2"}'
+fi
+
+if [[ "$JSON" ]] ; then
+ curl \
+ --request POST "$HOST"/jsonrpc \
+ --header "Authorization: Basic $AUTH" \
+ --header "Content-Type: application/json" \
+ --data "$JSON" \
+ --silent > /dev/null
+else
+ URL=$(echo "$QUTE_URL" |awk -F/ '{print $3}')
+ echo "message-warning 'kodi userscript does not support this $URL'" > "$QUTE_FIFO"
+fi
diff --git a/misc/userscripts/qr b/misc/userscripts/qr
new file mode 100755
index 000000000..84215249b
--- /dev/null
+++ b/misc/userscripts/qr
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+pngfile=$(mktemp --suffix=.png)
+trap 'rm -f "$pngfile"' EXIT
+
+qrencode -t PNG -o "$pngfile" -s 10 "$QUTE_URL"
+echo ":open -t file:///$pngfile" >> "$QUTE_FIFO"
+sleep 1 # give qutebrowser time to open the file before it gets removed
diff --git a/misc/userscripts/qutedmenu b/misc/userscripts/qutedmenu
index cc5a44413..bdd0d9b27 100755
--- a/misc/userscripts/qutedmenu
+++ b/misc/userscripts/qutedmenu
@@ -6,8 +6,9 @@
# If you would like to set a custom colorscheme/font use these dirs.
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors
-readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
+
+readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
readonly optsfile=$confdir/dmenu/bemenucolors
create_menu() {
@@ -22,15 +23,13 @@ create_menu() {
done < "$QUTE_CONFIG_DIR"/bookmarks/urls
# Finally history
- while read -r _ url; do
- printf -- '%s\n' "$url"
- done < "$QUTE_DATA_DIR"/history
+ printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')"
}
get_selection() {
opts+=(-p qutebrowser)
- #create_menu | dmenu -l 10 "${opts[@]}"
- create_menu | bemenu -l 10 "${opts[@]}"
+ create_menu | dmenu -l 10 "${opts[@]}"
+ #create_menu | bemenu -l 10 "${opts[@]}"
}
# Main
diff --git a/qutebrowser/api/downloads.py b/qutebrowser/api/downloads.py
index 5e5d1916a..55656c5b5 100644
--- a/qutebrowser/api/downloads.py
+++ b/qutebrowser/api/downloads.py
@@ -75,4 +75,6 @@ def download_temp(url: QUrl) -> TempDownload:
fobj.name = 'temporary: ' + url.host()
target = downloads.FileObjDownloadTarget(fobj)
download_manager = objreg.get('qtnetwork-download-manager')
- return download_manager.get(url, target=target, auto_remove=True)
+ # cache=False is set as a WORKAROUND for MS Defender thinking we're a trojan
+ # downloader when caching the hostblock list...
+ return download_manager.get(url, target=target, auto_remove=True, cache=False)
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 34c078d89..18777e250 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -1057,7 +1057,8 @@ class CommandDispatcher:
verbose: Show notifications when the command started/exited.
output: Show the output in a new tab.
output_messages: Show the output as messages.
- detach: Whether the command should be detached from qutebrowser.
+ detach: Detach the command from qutebrowser so that it continues
+ running when qutebrowser quits.
cmdline: The commandline to execute.
count: Given to userscripts as $QUTE_COUNT.
"""
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index 31a9d7f29..96220897c 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -560,8 +560,8 @@ class AbstractDownloadItem(QObject):
elif self.stats.percentage() is None:
return start
else:
- return utils.interpolate_color(start, stop,
- self.stats.percentage(), system)
+ return qtutils.interpolate_color(
+ start, stop, self.stats.percentage(), system)
def _do_cancel(self):
"""Actual cancel implementation."""
diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py
index b852ab29e..bace6fa6a 100644
--- a/qutebrowser/browser/navigate.py
+++ b/qutebrowser/browser/navigate.py
@@ -134,13 +134,13 @@ def path_up(url, count):
"""
urlutils.ensure_valid(url)
url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
- path = url.path()
+ path = url.path(QUrl.FullyEncoded)
if not path or path == '/':
raise Error("Can't go up!")
for _i in range(0, min(count, path.count('/'))):
path = posixpath.join(path, posixpath.pardir)
path = posixpath.normpath(path)
- url.setPath(path)
+ url.setPath(path, QUrl.StrictMode)
return url
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index c5bfc07e6..0bf165965 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -419,11 +419,12 @@ class DownloadManager(downloads.AbstractDownloadManager):
private=config.val.content.private_browsing, parent=self)
@pyqtSlot('QUrl')
- def get(self, url, **kwargs):
+ def get(self, url, cache=True, **kwargs):
"""Start a download with a link URL.
Args:
url: The URL to get, as QUrl
+ cache: If set to False, don't cache the response.
**kwargs: passed to get_request().
Return:
@@ -437,6 +438,9 @@ class DownloadManager(downloads.AbstractDownloadManager):
user_agent = websettings.user_agent(url)
req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)
+ if not cache:
+ req.setAttribute(QNetworkRequest.CacheSaveControlAttribute, False)
+
return self.get_request(req, **kwargs)
def get_mhtml(self, tab, target):
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 193a2a0e0..9234e82d8 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -82,7 +82,7 @@ def javascript_confirm(url, js_msg, abort_on):
raise CallSuper
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
- js_msg)
+ html.escape(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
ans = message.ask('Javascript confirm', msg,
mode=usertypes.PromptMode.yesno,
@@ -99,7 +99,7 @@ def javascript_prompt(url, js_msg, default, abort_on):
return (False, "")
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
- js_msg)
+ html.escape(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
answer = message.ask('Javascript prompt', msg,
mode=usertypes.PromptMode.text,
@@ -122,7 +122,7 @@ def javascript_alert(url, js_msg, abort_on):
return
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
- js_msg)
+ html.escape(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
abort_on=abort_on, url=urlstr)
diff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py
index 5795364b0..630a7bf9e 100644
--- a/qutebrowser/browser/webengine/darkmode.py
+++ b/qutebrowser/browser/webengine/darkmode.py
@@ -73,6 +73,7 @@ Prefix changed to "forceDarkMode".
- As with Qt 5.15.0 / .1, but with "forceDarkMode" as prefix.
"""
+import os
import enum
from typing import Any, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union
@@ -90,7 +91,6 @@ class Variant(enum.Enum):
"""A dark mode variant."""
- unavailable = enum.auto()
qt_511_to_513 = enum.auto()
qt_514 = enum.auto()
qt_515_0 = enum.auto()
@@ -159,8 +159,6 @@ _QT_514_SETTINGS = [
# 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),
@@ -235,6 +233,13 @@ _DARK_MODE_DEFINITIONS: Mapping[Variant, _DarkModeDefinitionType] = {
def _variant() -> Variant:
"""Get the dark mode variant based on the underlying Qt version."""
+ env_var = os.environ.get('QUTE_DARKMODE_VARIANT')
+ if env_var is not None:
+ try:
+ return Variant[env_var]
+ except KeyError:
+ log.init.warning(f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}")
+
if PYQT_WEBENGINE_VERSION is not None:
# Available with Qt >= 5.13
if PYQT_WEBENGINE_VERSION >= 0x050f02:
diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py
index 95e01588b..b27509552 100644
--- a/qutebrowser/browser/webengine/interceptor.py
+++ b/qutebrowser/browser/webengine/interceptor.py
@@ -25,7 +25,7 @@ from PyQt5.QtCore import QUrl, QByteArray
from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
QWebEngineUrlRequestInfo)
-from qutebrowser.config import websettings
+from qutebrowser.config import websettings, config
from qutebrowser.browser import shared
from qutebrowser.utils import utils, log, debug, qtutils
from qutebrowser.extensions import interceptors
@@ -204,5 +204,11 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
for header, value in shared.custom_headers(url=url):
info.setHttpHeader(header, value)
+ # Note this is ignored before Qt 5.12.4 and 5.13.1 due to
+ # https://bugreports.qt.io/browse/QTBUG-60203 - there, we set the
+ # commandline-flag in qtargs.py instead.
+ if config.cache['content.headers.referer'] == 'never':
+ info.setHttpHeader(b'Referer', b'')
+
user_agent = websettings.user_agent(url)
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))
diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py
index a8e58b8a2..4e1290f82 100644
--- a/qutebrowser/completion/completiondelegate.py
+++ b/qutebrowser/completion/completiondelegate.py
@@ -47,6 +47,7 @@ class _Highlighter(QSyntaxHighlighter):
self._expression = QRegularExpression(
pat, QRegularExpression.CaseInsensitiveOption
)
+ qtutils.ensure_valid(self._expression)
def highlightBlock(self, text):
"""Override highlightBlock for custom highlighting."""
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
index 1f5304b61..86de688a0 100644
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -331,7 +331,8 @@ class CompletionView(QTreeView):
QItemSelectionModel.Rows)
# if the last item is focused, try to fetch more
- if idx.row() == self.model().rowCount(idx.parent()) - 1:
+ next_idx = self.indexBelow(idx)
+ if not self.visualRect(next_idx).isValid():
self.expandAll()
count = self.model().count()
diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py
index f0cc21da0..79dc0770a 100644
--- a/qutebrowser/completion/models/listcategory.py
+++ b/qutebrowser/completion/models/listcategory.py
@@ -64,6 +64,7 @@ class ListCategory(QSortFilterProxyModel):
val = re.escape(val)
val = val.replace(r'\ ', '.*')
rx = QRegExp(val, Qt.CaseInsensitive)
+ qtutils.ensure_valid(rx)
self.setFilterRegExp(rx)
self.invalidate()
sortcol = 0
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index da64c0f6a..645342767 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -2074,6 +2074,19 @@ window.title_format:
Format to use for the window title. The same placeholders like for
`tabs.title.format` are defined.
+window.transparent:
+ type: Bool
+ default: false
+ desc: |
+ Set the main window background to transparent.
+
+ This allows having a transparent tab- or statusbar (might require a compositor such
+ as picom). However, it breaks some functionality such as dmenu embedding via its
+ `-w` option. On some systems, it was additionally reported that main window
+ transparency negatively affects performance.
+
+ Note this setting only affects windows opened after setting it.
+
## zoom
zoom.default:
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index 9185ee6ef..6328c3140 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -56,8 +56,8 @@ from typing import (Any, Callable, Dict as DictType, Iterable, Iterator,
import attr
import yaml
from PyQt5.QtCore import QUrl, Qt
-from PyQt5.QtGui import QColor, QFontDatabase
-from PyQt5.QtWidgets import QTabWidget, QTabBar, QApplication
+from PyQt5.QtGui import QColor
+from PyQt5.QtWidgets import QTabWidget, QTabBar
from PyQt5.QtNetwork import QNetworkProxy
from qutebrowser.misc import objects, debugcachestats
@@ -96,9 +96,15 @@ class ValidValues:
generate_docs: Whether to show the values in the docs.
"""
- def __init__(self,
- *values: Union[str, DictType[str, str], Tuple[str, str]],
- generate_docs: bool = True) -> None:
+ def __init__(
+ self,
+ *values: Union[
+ str,
+ DictType[str, Optional[str]],
+ Tuple[str, Optional[str]],
+ ],
+ generate_docs: bool = True,
+ ) -> None:
if not values:
raise ValueError("ValidValues with no values makes no sense!")
self.descriptions: DictType[str, str] = {}
@@ -107,17 +113,18 @@ class ValidValues:
for value in values:
if isinstance(value, str):
# Value without description
- self.values.append(value)
+ val = value
+ desc = None
elif isinstance(value, dict):
# List of dicts from configdata.yml
assert len(value) == 1, value
- value, desc = list(value.items())[0]
- self.values.append(value)
- self.descriptions[value] = desc
+ val, desc = list(value.items())[0]
else:
- # (value, description) tuple
- self.values.append(value[0])
- self.descriptions[value[0]] = value[1]
+ val, desc = value
+
+ self.values.append(val)
+ if desc is not None:
+ self.descriptions[val] = desc
def __contains__(self, val: str) -> bool:
return val in self.values
@@ -308,17 +315,10 @@ class BaseType:
"""
if self.valid_values is None:
return None
- else:
- out = []
- for val in self.valid_values:
- try:
- desc = self.valid_values.descriptions[val]
- except KeyError:
- # Some values are self-explaining and don't need a
- # description.
- desc = ""
- out.append((val, desc))
- return out
+ return [
+ (val, self.valid_values.descriptions.get(val, ""))
+ for val in self.valid_values
+ ]
def __repr__(self) -> str:
return utils.get_repr(self, none_ok=self.none_ok)
@@ -329,14 +329,15 @@ class MappingType(BaseType):
"""Base class for any setting which has a mapping to the given values.
Attributes:
- MAPPING: The mapping to use.
+ MAPPING: A mapping from config values to (translated_value, docs) tuples.
"""
- MAPPING: DictType[str, Any] = {}
+ MAPPING: DictType[str, Tuple[Any, Optional[str]]] = {}
- def __init__(self, none_ok: bool = False, valid_values: ValidValues = None) -> None:
+ def __init__(self, none_ok: bool = False) -> None:
super().__init__(none_ok)
- self.valid_values = valid_values
+ self.valid_values = ValidValues(
+ *[(key, doc) for (key, (_val, doc)) in self.MAPPING.items()])
def to_py(self, value: Any) -> Any:
self._basic_py_validation(value, str)
@@ -345,7 +346,8 @@ class MappingType(BaseType):
elif not value:
return None
self._validate_valid_values(value.lower())
- return self.MAPPING[value.lower()]
+ mapped, _doc = self.MAPPING[value.lower()]
+ return mapped
def __repr__(self) -> str:
return utils.get_repr(self, none_ok=self.none_ok,
@@ -998,20 +1000,11 @@ class ColorSystem(MappingType):
"""The color system to use for color interpolation."""
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues(
- ('rgb', "Interpolate in the RGB color system."),
- ('hsv', "Interpolate in the HSV color system."),
- ('hsl', "Interpolate in the HSL color system."),
- ('none', "Don't show a gradient.")))
-
MAPPING = {
- 'rgb': QColor.Rgb,
- 'hsv': QColor.Hsv,
- 'hsl': QColor.Hsl,
- 'none': None,
+ 'rgb': (QColor.Rgb, "Interpolate in the RGB color system."),
+ 'hsv': (QColor.Hsv, "Interpolate in the HSV color system."),
+ 'hsl': (QColor.Hsl, "Interpolate in the HSL color system."),
+ 'none': (None, "Don't show a gradient."),
}
@@ -1019,19 +1012,13 @@ class IgnoreCase(MappingType):
"""Whether to search case insensitively."""
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues(
- ('always', "Search case-insensitively."),
- ('never', "Search case-sensitively."),
- ('smart', ("Search case-sensitively if there are capital "
- "characters."))))
-
MAPPING = {
- 'always': usertypes.IgnoreCase.always,
- 'never': usertypes.IgnoreCase.never,
- 'smart': usertypes.IgnoreCase.smart,
+ 'always': (usertypes.IgnoreCase.always, "Search case-insensitively."),
+ 'never': (usertypes.IgnoreCase.never, "Search case-sensitively."),
+ 'smart': (
+ usertypes.IgnoreCase.smart,
+ "Search case-sensitively if there are capital characters."
+ ),
}
@@ -1172,50 +1159,11 @@ class FontBase(BaseType):
If the given family value (fonts.default_family in the config) is
unset, a system-specific default monospace font is used.
-
- Note that (at least) three ways of getting the default monospace font
- exist:
-
- 1) f = QFont()
- f.setStyleHint(QFont.Monospace)
- print(f.defaultFamily())
-
- 2) f = QFont()
- f.setStyleHint(QFont.TypeWriter)
- print(f.defaultFamily())
-
- 3) f = QFontDatabase.systemFont(QFontDatabase.FixedFont)
- print(f.family())
-
- They yield different results depending on the OS:
-
- QFont.Monospace | QFont.TypeWriter | QFontDatabase
- ------------------------------------------------------
- Windows: Courier New | Courier New | Courier New
- Linux: DejaVu Sans Mono | DejaVu Sans Mono | monospace
- macOS: Menlo | American Typewriter | Monaco
-
- Test script: https://p.cmpl.cc/d4dfe573
-
- On Linux, it seems like both actually resolve to the same font.
-
- On macOS, "American Typewriter" looks like it indeed tries to imitate a
- typewriter, so it's not really a suitable UI font.
-
- Looking at those Wikipedia articles:
-
- https://en.wikipedia.org/wiki/Monaco_(typeface)
- https://en.wikipedia.org/wiki/Menlo_(typeface)
-
- the "right" choice isn't really obvious. Thus, let's go for the
- QFontDatabase approach here, since it's by far the simplest one.
"""
if default_family:
families = configutils.FontFamilies(default_family)
else:
- assert QApplication.instance() is not None
- font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
- families = configutils.FontFamilies([font.family()])
+ families = configutils.FontFamilies.from_system_default()
cls.default_family = families.to_str(quote=True)
cls.default_size = default_size
@@ -1753,33 +1701,23 @@ class Position(MappingType):
"""The position of the tab bar."""
MAPPING = {
- 'top': QTabWidget.North,
- 'bottom': QTabWidget.South,
- 'left': QTabWidget.West,
- 'right': QTabWidget.East,
+ 'top': (QTabWidget.North, None),
+ 'bottom': (QTabWidget.South, None),
+ 'left': (QTabWidget.West, None),
+ 'right': (QTabWidget.East, None),
}
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues('top', 'bottom', 'left', 'right'))
-
class TextAlignment(MappingType):
"""Alignment of text."""
MAPPING = {
- 'left': Qt.AlignLeft,
- 'right': Qt.AlignRight,
- 'center': Qt.AlignCenter,
+ 'left': (Qt.AlignLeft, None),
+ 'right': (Qt.AlignRight, None),
+ 'center': (Qt.AlignCenter, None),
}
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues('left', 'right', 'center'))
-
class VerticalPosition(String):
@@ -1828,21 +1766,22 @@ class SelectOnRemove(MappingType):
"""Which tab to select when the focused tab is removed."""
MAPPING = {
- 'prev': QTabBar.SelectLeftTab,
- 'next': QTabBar.SelectRightTab,
- 'last-used': QTabBar.SelectPreviousTab,
+ 'prev': (
+ QTabBar.SelectLeftTab,
+ ("Select the tab which came before the closed one "
+ "(left in horizontal, above in vertical)."),
+ ),
+ 'next': (
+ QTabBar.SelectRightTab,
+ ("Select the tab which came after the closed one "
+ "(right in horizontal, below in vertical)."),
+ ),
+ 'last-used': (
+ QTabBar.SelectPreviousTab,
+ "Select the previously selected tab.",
+ ),
}
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues(
- ('prev', "Select the tab which came before the closed one "
- "(left in horizontal, above in vertical)."),
- ('next', "Select the tab which came after the closed one "
- "(right in horizontal, below in vertical)."),
- ('last-used', "Select the previously selected tab.")))
-
class ConfirmQuit(FlagList):
diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py
index 7c2d4ee8c..e7a60a7eb 100644
--- a/qutebrowser/config/configutils.py
+++ b/qutebrowser/config/configutils.py
@@ -29,6 +29,8 @@ from typing import (
MutableMapping)
from PyQt5.QtCore import QUrl
+from PyQt5.QtGui import QFontDatabase
+from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import utils, urlmatch, usertypes, qtutils
from qutebrowser.config import configexc
@@ -280,6 +282,9 @@ class FontFamilies:
def __iter__(self) -> Iterator[str]:
yield from self._families
+ def __len__(self) -> int:
+ return len(self._families)
+
def __repr__(self) -> str:
return utils.get_repr(self, families=self._families, constructor=True)
@@ -288,7 +293,7 @@ class FontFamilies:
def _quoted_families(self) -> Iterator[str]:
for f in self._families:
- needs_quoting = any(c in f for c in ', ')
+ needs_quoting = any(c in f for c in '., ')
yield '"{}"'.format(f) if needs_quoting else f
def to_str(self, *, quote: bool = True) -> str:
@@ -296,6 +301,57 @@ class FontFamilies:
return ', '.join(families)
@classmethod
+ def from_system_default(
+ cls,
+ font_type: QFontDatabase.SystemFont = QFontDatabase.FixedFont,
+ ) -> 'FontFamilies':
+ """Get a FontFamilies object for the default system font.
+
+ By default, the monospace font is returned, though via the "font_type" argument,
+ other types can be requested as well.
+
+ Note that (at least) three ways of getting the default monospace font
+ exist:
+
+ 1) f = QFont()
+ f.setStyleHint(QFont.Monospace)
+ print(f.defaultFamily())
+
+ 2) f = QFont()
+ f.setStyleHint(QFont.TypeWriter)
+ print(f.defaultFamily())
+
+ 3) f = QFontDatabase.systemFont(QFontDatabase.FixedFont)
+ print(f.family())
+
+ They yield different results depending on the OS:
+
+ QFont.Monospace | QFont.TypeWriter | QFontDatabase
+ ------------------------------------------------------
+ Windows: Courier New | Courier New | Courier New
+ Linux: DejaVu Sans Mono | DejaVu Sans Mono | monospace
+ macOS: Menlo | American Typewriter | Monaco
+
+ Test script: https://p.cmpl.cc/d4dfe573
+
+ On Linux, it seems like both actually resolve to the same font.
+
+ On macOS, "American Typewriter" looks like it indeed tries to imitate a
+ typewriter, so it's not really a suitable UI font.
+
+ Looking at those Wikipedia articles:
+
+ https://en.wikipedia.org/wiki/Monaco_(typeface)
+ https://en.wikipedia.org/wiki/Menlo_(typeface)
+
+ the "right" choice isn't really obvious. Thus, let's go for the
+ QFontDatabase approach here, since it's by far the simplest one.
+ """
+ assert QApplication.instance() is not None
+ font = QFontDatabase.systemFont(font_type)
+ return cls([font.family()])
+
+ @classmethod
def from_str(cls, family_str: str) -> 'FontFamilies':
"""Parse a CSS-like string of font families."""
families = []
diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py
index 790fbaca3..8ab93c904 100644
--- a/qutebrowser/config/qtargs.py
+++ b/qutebrowser/config/qtargs.py
@@ -109,6 +109,16 @@ def _qtwebengine_enabled_features(feature_flags: Sequence[str]) -> Iterator[str]
if config.val.scrolling.bar == 'overlay':
yield 'OverlayScrollbar'
+ if (qtutils.version_check('5.14', compiled=False) and
+ config.val.content.headers.referer == 'same-domain'):
+ # Handling of reduced-referrer-granularity in Chromium 76+
+ # https://chromium-review.googlesource.com/c/chromium/src/+/1572699
+ #
+ # Note that this is removed entirely (and apparently the default) starting with
+ # Chromium 89 (Qt 5.15.x or 6.x):
+ # https://chromium-review.googlesource.com/c/chromium/src/+/2545444
+ yield 'ReducedReferrerGranularity'
+
def _qtwebengine_args(
namespace: argparse.Namespace,
@@ -145,8 +155,7 @@ def _qtwebengine_args(
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)
+ yield '--blink-settings=' + ','.join(f'{k}={v}' for k, v in blink_settings)
enabled_features = list(_qtwebengine_enabled_features(feature_flags))
if enabled_features:
@@ -191,16 +200,26 @@ def _qtwebengine_settings_args() -> Iterator[str]:
},
'content.headers.referer': {
'always': None,
- 'never': '--no-referrers',
- 'same-domain': '--reduced-referrer-granularity',
}
}
- if qtutils.version_check('5.14'):
+ referrer_setting = settings['content.headers.referer']
+ if qtutils.version_check('5.14', compiled=False):
settings['colors.webpage.prefers_color_scheme_dark'] = {
True: '--force-dark-mode',
False: None,
}
+ # Starting with Qt 5.14, this is handled via --enable-features
+ referrer_setting['same-domain'] = None
+ else:
+ referrer_setting['same-domain'] = '--reduced-referrer-granularity'
+
+ can_override_referer = (
+ qtutils.version_check('5.12.4', compiled=False) and
+ not qtutils.version_check('5.13.0', compiled=False, exact=True)
+ )
+ # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60203
+ referrer_setting['never'] = None if can_override_referer else '--no-referrers'
for setting, args in sorted(settings.items()):
arg = args[config.instance.get(setting)]
diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml
index b84dbeb96..939500aa3 100644
--- a/qutebrowser/javascript/.eslintrc.yaml
+++ b/qutebrowser/javascript/.eslintrc.yaml
@@ -41,7 +41,7 @@ rules:
func-names: "off"
sort-keys: "off"
no-warning-comments: "off"
- max-len: ["error", {"ignoreUrls": true}]
+ max-len: ["error", {"ignoreUrls": true, "code": 88}]
capitalized-comments: "off"
prefer-destructuring: "off"
line-comment-position: "off"
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index b8228545a..6273b3382 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -203,8 +203,10 @@ class MainWindow(QWidget):
from qutebrowser.mainwindow.statusbar import bar
self.setAttribute(Qt.WA_DeleteOnClose)
- self.setAttribute(Qt.WA_TranslucentBackground)
- self.palette().setColor(QPalette.Window, Qt.transparent)
+ if config.val.window.transparent:
+ self.setAttribute(Qt.WA_TranslucentBackground)
+ self.palette().setColor(QPalette.Window, Qt.transparent)
+
self._overlays: MutableSequence[_OverlayInfoType] = []
self.win_id = next(win_id_gen)
self.registry = objreg.ObjectRegistry()
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 9bb8d34ce..c67e5fa0e 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -863,7 +863,7 @@ class TabbedBrowser(QWidget):
start = config.cache['colors.tabs.indicator.start']
stop = config.cache['colors.tabs.indicator.stop']
system = config.cache['colors.tabs.indicator.system']
- color = utils.interpolate_color(start, stop, perc, system)
+ color = qtutils.interpolate_color(start, stop, perc, system)
self.widget.set_tab_indicator_color(idx, color)
self.widget.update_tab_title(idx)
if idx == self.widget.currentIndex():
@@ -880,7 +880,7 @@ class TabbedBrowser(QWidget):
start = config.cache['colors.tabs.indicator.start']
stop = config.cache['colors.tabs.indicator.stop']
system = config.cache['colors.tabs.indicator.system']
- color = utils.interpolate_color(start, stop, 100, system)
+ color = qtutils.interpolate_color(start, stop, 100, system)
else:
color = config.cache['colors.tabs.indicator.error']
self.widget.set_tab_indicator_color(idx, color)
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index b40c59bd5..f853f8fd9 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -639,7 +639,7 @@ class TabBar(QTabBar):
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
perc = int(confwidth.rstrip('%'))
- width = main_window.width() * perc / 100
+ width = main_window.width() * perc // 100
else:
width = int(confwidth)
size = QSize(width, height)
diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py
index 2bdb790e7..52cb8ad0c 100644
--- a/qutebrowser/misc/crashdialog.py
+++ b/qutebrowser/misc/crashdialog.py
@@ -30,7 +30,6 @@ import datetime
import enum
from typing import List, Tuple
-import pkg_resources
from PyQt5.QtCore import pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QCheckBox,
@@ -361,8 +360,8 @@ class _CrashDialog(QDialog):
Args:
newest: The newest version as a string.
"""
- new_version = pkg_resources.parse_version(newest)
- cur_version = pkg_resources.parse_version(qutebrowser.__version__)
+ new_version = utils.parse_version(newest)
+ cur_version = utils.parse_version(qutebrowser.__version__)
lines = ['The report has been sent successfully. Thanks!']
if new_version > cur_version:
lines.append("<b>Note:</b> The newest available version is v{}, "
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index 92920c72c..d1c57760e 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -170,13 +170,16 @@ def qt_version(qversion=None, qt_version_str=None):
def check_qt_version():
"""Check if the Qt version is recent enough."""
- from PyQt5.QtCore import (qVersion, QT_VERSION, PYQT_VERSION,
- PYQT_VERSION_STR)
- from pkg_resources import parse_version
- parsed_qversion = parse_version(qVersion())
-
- if (QT_VERSION < 0x050C00 or PYQT_VERSION < 0x050C00 or
- parsed_qversion < parse_version('5.12.0')):
+ from PyQt5.QtCore import QT_VERSION, PYQT_VERSION, PYQT_VERSION_STR
+ try:
+ from PyQt5.QtCore import QVersionNumber, QLibraryInfo
+ qt_ver = QLibraryInfo.version().normalized()
+ recent_qt_runtime = qt_ver >= QVersionNumber(5, 12) # type: ignore[operator]
+ except (ImportError, AttributeError):
+ # QVersionNumber was added in Qt 5.6, QLibraryInfo.version() in 5.8
+ recent_qt_runtime = False
+
+ if QT_VERSION < 0x050C00 or PYQT_VERSION < 0x050C00 or not recent_qt_runtime:
text = ("Fatal error: Qt >= 5.12.0 and PyQt >= 5.12.0 are required, "
"but Qt {} / PyQt {} is installed.".format(qt_version(),
PYQT_VERSION_STR))
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index 275da7c4c..cd6ea2b32 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -31,9 +31,8 @@ Module attributes:
import io
import operator
import contextlib
-from typing import TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, cast
+from typing import TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, Tuple, cast
-import pkg_resources
from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR,
PYQT_VERSION_STR, QObject, QUrl)
@@ -48,7 +47,7 @@ if TYPE_CHECKING:
from PyQt5.QtWebEngineWidgets import QWebEngineHistory
from qutebrowser.misc import objects
-from qutebrowser.utils import usertypes
+from qutebrowser.utils import usertypes, utils
MAXVALS = {
@@ -100,15 +99,15 @@ def version_check(version: str,
if compiled and exact:
raise ValueError("Can't use compiled=True with exact=True!")
- parsed = pkg_resources.parse_version(version)
+ parsed = utils.parse_version(version)
op = operator.eq if exact else operator.ge
- result = op(pkg_resources.parse_version(qVersion()), parsed)
+ result = op(utils.parse_version(qVersion()), parsed)
if compiled and result:
# qVersion() ==/>= parsed, now check if QT_VERSION_STR ==/>= parsed.
- result = op(pkg_resources.parse_version(QT_VERSION_STR), parsed)
+ result = op(utils.parse_version(QT_VERSION_STR), parsed)
if compiled and result:
# Finally, check PYQT_VERSION_STR as well.
- result = op(pkg_resources.parse_version(PYQT_VERSION_STR), parsed)
+ result = op(utils.parse_version(PYQT_VERSION_STR), parsed)
return result
@@ -118,8 +117,8 @@ MAX_WORLD_ID = 256
def is_new_qtwebkit() -> bool:
"""Check if the given version is a new QtWebKit."""
assert qWebKitVersion is not None
- return (pkg_resources.parse_version(qWebKitVersion()) >
- pkg_resources.parse_version('538.1'))
+ return (utils.parse_version(qWebKitVersion()) >
+ utils.parse_version('538.1'))
def is_single_process() -> bool:
@@ -157,19 +156,15 @@ def check_overflow(arg: int, ctype: str, fatal: bool = True) -> int:
return arg
-if TYPE_CHECKING:
- # Protocol was added in Python 3.8
- from typing import Protocol
-
- class Validatable(Protocol):
+class Validatable(utils.Protocol):
- """An object with an isValid() method (e.g. QUrl)."""
+ """An object with an isValid() method (e.g. QUrl)."""
- def isValid(self) -> bool:
- ...
+ def isValid(self) -> bool:
+ ...
-def ensure_valid(obj: 'Validatable') -> None:
+def ensure_valid(obj: Validatable) -> None:
"""Ensure a Qt object with an .isValid() method is valid."""
if not obj.isValid():
raise QtValueError(obj)
@@ -440,7 +435,7 @@ class QtValueError(ValueError):
"""Exception which gets raised by ensure_valid."""
- def __init__(self, obj: 'Validatable') -> None:
+ def __init__(self, obj: Validatable) -> None:
try:
self.reason = obj.errorString() # type: ignore[attr-defined]
except AttributeError:
@@ -474,3 +469,78 @@ class EventLoop(QEventLoop):
status = super().exec_(flags)
self._executing = False
return status
+
+
+def _get_color_percentage(x1: int, y1: int, z1: int, a1: int,
+ x2: int, y2: int, z2: int, a2: int,
+ percent: int) -> Tuple[int, int, int, int]:
+ """Get a color which is percent% interpolated between start and end.
+
+ Args:
+ x1, y1, z1, a1 : Start color components (R, G, B, A / H, S, V, A / H, S, L, A)
+ x2, y2, z2, a2 : End color components (R, G, B, A / H, S, V, A / H, S, L, A)
+ percent: Percentage to interpolate, 0-100.
+ 0: Start color will be returned.
+ 100: End color will be returned.
+
+ Return:
+ A (x, y, z, alpha) tuple with the interpolated color components.
+ """
+ if not 0 <= percent <= 100:
+ raise ValueError("percent needs to be between 0 and 100!")
+ x = round(x1 + (x2 - x1) * percent / 100)
+ y = round(y1 + (y2 - y1) * percent / 100)
+ z = round(z1 + (z2 - z1) * percent / 100)
+ a = round(a1 + (a2 - a1) * percent / 100)
+ return (x, y, z, a)
+
+
+def interpolate_color(
+ start: QColor,
+ end: QColor,
+ percent: int,
+ colorspace: Optional[QColor.Spec] = QColor.Rgb
+) -> QColor:
+ """Get an interpolated color value.
+
+ Args:
+ start: The start color.
+ end: The end color.
+ percent: Which value to get (0 - 100)
+ colorspace: The desired interpolation color system,
+ QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum)
+ If None, start is used except when percent is 100.
+
+ Return:
+ The interpolated QColor, with the same spec as the given start color.
+ """
+ ensure_valid(start)
+ ensure_valid(end)
+
+ if colorspace is None:
+ if percent == 100:
+ return QColor(*end.getRgb())
+ else:
+ return QColor(*start.getRgb())
+
+ out = QColor()
+ if colorspace == QColor.Rgb:
+ r1, g1, b1, a1 = start.getRgb()
+ r2, g2, b2, a2 = end.getRgb()
+ components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent)
+ out.setRgb(*components)
+ elif colorspace == QColor.Hsv:
+ h1, s1, v1, a1 = start.getHsv()
+ h2, s2, v2, a2 = end.getHsv()
+ components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent)
+ out.setHsv(*components)
+ elif colorspace == QColor.Hsl:
+ h1, s1, l1, a1 = start.getHsl()
+ h2, s2, l2, a2 = end.getHsl()
+ components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent)
+ out.setHsl(*components)
+ else:
+ raise ValueError("Invalid colorspace!")
+ out = out.convertTo(start.spec())
+ ensure_valid(out)
+ return out
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index 977bd7cc6..41d20e734 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -467,12 +467,19 @@ def same_domain(url1: QUrl, url2: QUrl) -> bool:
For example example.com and www.example.com are considered the same. but
example.co.uk and test.co.uk are not.
+ If the URL's schemes or ports are different, they are always treated as not equal.
+
Return:
True if the domains are the same, False otherwise.
"""
ensure_valid(url1)
ensure_valid(url2)
+ if url1.scheme() != url2.scheme():
+ return False
+ if url1.port() != url2.port():
+ return False
+
suffix1 = url1.topLevelDomain()
suffix2 = url2.topLevelDomain()
if not suffix1:
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index 9ecae9e92..893dae877 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -21,7 +21,7 @@
import operator
import enum
-from typing import TYPE_CHECKING, Any, Optional, Sequence, TypeVar, Union
+from typing import Any, Optional, Sequence, TypeVar, Union
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
@@ -30,19 +30,7 @@ from PyQt5.QtCore import QUrl
from qutebrowser.utils import log, qtutils, utils
-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')
+_T = TypeVar('_T', bound=utils.SupportsLessThan)
class Unset:
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 7c2bf843d..31ff5bf50 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -36,10 +36,11 @@ import glob
import mimetypes
import ctypes
import ctypes.util
-from typing import Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union
+from typing import (Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union,
+ TYPE_CHECKING, cast)
-from PyQt5.QtCore import QUrl
-from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
+from PyQt5.QtCore import QUrl, QVersionNumber
+from PyQt5.QtGui import QClipboard, QDesktopServices
from PyQt5.QtWidgets import QApplication
import pkg_resources
import yaml
@@ -53,7 +54,7 @@ except ImportError: # pragma: no cover
YAML_C_EXT = False
import qutebrowser
-from qutebrowser.utils import qtutils, log
+from qutebrowser.utils import log
fake_clipboard = None
@@ -66,6 +67,34 @@ is_windows = sys.platform.startswith('win')
is_posix = os.name == 'posix'
+try:
+ # Protocol was added in Python 3.8
+ from typing import Protocol
+except ImportError: # pragma: no cover
+ if not TYPE_CHECKING:
+ class Protocol:
+
+ """Empty stub at runtime."""
+
+
+class SupportsLessThan(Protocol):
+
+ """Protocol for a "comparable" object."""
+
+ def __lt__(self, other: Any) -> bool:
+ ...
+
+
+if TYPE_CHECKING:
+ class VersionNumber(SupportsLessThan, QVersionNumber):
+
+ """WORKAROUND for incorrect PyQt stubs."""
+else:
+ class VersionNumber:
+
+ """We can't inherit from Protocol and QVersionNumber at runtime."""
+
+
class Unreachable(Exception):
"""Raised when there was unreachable code."""
@@ -210,79 +239,10 @@ def resource_filename(filename: str) -> str:
return pkg_resources.resource_filename(qutebrowser.__name__, filename)
-def _get_color_percentage(x1: int, y1: int, z1: int, a1: int,
- x2: int, y2: int, z2: int, a2: int,
- percent: int) -> Tuple[int, int, int, int]:
- """Get a color which is percent% interpolated between start and end.
-
- Args:
- x1, y1, z1, a1 : Start color components (R, G, B, A / H, S, V, A / H, S, L, A)
- x2, y2, z2, a2 : End color components (R, G, B, A / H, S, V, A / H, S, L, A)
- percent: Percentage to interpolate, 0-100.
- 0: Start color will be returned.
- 100: End color will be returned.
-
- Return:
- A (x, y, z, alpha) tuple with the interpolated color components.
- """
- if not 0 <= percent <= 100:
- raise ValueError("percent needs to be between 0 and 100!")
- x = round(x1 + (x2 - x1) * percent / 100)
- y = round(y1 + (y2 - y1) * percent / 100)
- z = round(z1 + (z2 - z1) * percent / 100)
- a = round(a1 + (a2 - a1) * percent / 100)
- return (x, y, z, a)
-
-
-def interpolate_color(
- start: QColor,
- end: QColor,
- percent: int,
- colorspace: Optional[QColor.Spec] = QColor.Rgb
-) -> QColor:
- """Get an interpolated color value.
-
- Args:
- start: The start color.
- end: The end color.
- percent: Which value to get (0 - 100)
- colorspace: The desired interpolation color system,
- QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum)
- If None, start is used except when percent is 100.
-
- Return:
- The interpolated QColor, with the same spec as the given start color.
- """
- qtutils.ensure_valid(start)
- qtutils.ensure_valid(end)
-
- if colorspace is None:
- if percent == 100:
- return QColor(*end.getRgb())
- else:
- return QColor(*start.getRgb())
-
- out = QColor()
- if colorspace == QColor.Rgb:
- r1, g1, b1, a1 = start.getRgb()
- r2, g2, b2, a2 = end.getRgb()
- components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent)
- out.setRgb(*components)
- elif colorspace == QColor.Hsv:
- h1, s1, v1, a1 = start.getHsv()
- h2, s2, v2, a2 = end.getHsv()
- components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent)
- out.setHsv(*components)
- elif colorspace == QColor.Hsl:
- h1, s1, l1, a1 = start.getHsl()
- h2, s2, l2, a2 = end.getHsl()
- components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent)
- out.setHsl(*components)
- else:
- raise ValueError("Invalid colorspace!")
- out = out.convertTo(start.spec())
- qtutils.ensure_valid(out)
- return out
+def parse_version(version: str) -> VersionNumber:
+ """Parse a version string."""
+ v_q, _suffix = QVersionNumber.fromString(version)
+ return cast(VersionNumber, v_q.normalized())
def format_seconds(total_seconds: int) -> str:
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 032563478..64efe4c4f 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -34,7 +34,6 @@ import functools
from typing import Mapping, Optional, Sequence, Tuple, cast
import attr
-import pkg_resources
from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo
from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile,
@@ -84,7 +83,7 @@ class DistributionInfo:
id: Optional[str] = attr.ib()
parsed: 'Distribution' = attr.ib()
- version: Optional[Tuple[str, ...]] = attr.ib()
+ version: Optional[utils.VersionNumber] = attr.ib()
pretty: str = attr.ib()
@@ -139,8 +138,8 @@ def distribution() -> Optional[DistributionInfo]:
assert pretty is not None
if 'VERSION_ID' in info:
- dist_version: Optional[Tuple[str, ...]] = pkg_resources.parse_version(
- info['VERSION_ID'])
+ version_id = info['VERSION_ID']
+ dist_version: Optional[utils.VersionNumber] = utils.parse_version(version_id)
else:
dist_version = None
@@ -401,7 +400,7 @@ def _chromium_version() -> str:
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)
+ Security fixes up to 86.0.4240.183 (2020-11-02)
Also see:
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index 8030d61a1..6044a1e18 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -252,7 +252,7 @@ def _get_windows_python_path(x64):
return fallback
-def build_windows():
+def build_windows(*, skip_packaging):
"""Build windows executables/setups."""
utils.print_title("Updating 3rdparty content")
update_3rdparty.run(nsis=True, ace=False, pdfjs=True, fancy_dmg=False)
@@ -289,6 +289,14 @@ def build_windows():
utils.print_title("Running 64bit smoke test")
smoke_test(os.path.join(out_64, 'qutebrowser.exe'))
+ if not skip_packaging:
+ artifacts += _package_windows(out_32, out_64)
+
+ return artifacts
+
+
+def _package_windows(out_32, out_64):
+ """Build installers/zips for Windows."""
utils.print_title("Building installers")
subprocess.run(['makensis.exe',
'/DVERSION={}'.format(qutebrowser.__version__),
@@ -301,7 +309,7 @@ def build_windows():
name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__)
name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
- artifacts += [
+ artifacts = [
(os.path.join('dist', name_32),
'application/vnd.microsoft.portable-executable',
'Windows 32bit installer'),
@@ -465,7 +473,9 @@ def main():
"If not given, the current Python interpreter is used.",
nargs='?')
parser.add_argument('--upload', action='store_true', required=False,
- help="Toggle to upload the release to GitHub")
+ help="Toggle to upload the release to GitHub.")
+ parser.add_argument('--skip-packaging', action='store_true', required=False,
+ help="Skip Windows installer/zip generation.")
args = parser.parse_args()
utils.change_cwd()
@@ -487,7 +497,7 @@ def main():
run_asciidoc2html(args)
if os.name == 'nt':
- artifacts = build_windows()
+ artifacts = build_windows(skip_packaging=args.skip_packaging)
elif sys.platform == 'darwin':
artifacts = build_mac()
else:
diff --git a/scripts/dev/ci/docker/Dockerfile.j2 b/scripts/dev/ci/docker/Dockerfile.j2
new file mode 100644
index 000000000..1835f0a2f
--- /dev/null
+++ b/scripts/dev/ci/docker/Dockerfile.j2
@@ -0,0 +1,27 @@
+FROM thecompiler/archlinux
+MAINTAINER Florian Bruhin <me@the-compiler.org>
+
+{% if unstable %}
+RUN sed -i '/^# after the header/a[kde-unstable]\nInclude = /etc/pacman.d/mirrorlist\n\n[testing]\nInclude = /etc/pacman.d/mirrorlist' /etc/pacman.conf
+{% endif %}
+RUN pacman -Suyy --noconfirm \
+ git \
+ python-tox \
+ python-distlib \
+ qt5-base \
+ qt5-declarative \
+ {% if webengine %}qt5-webengine python-pyqtwebengine{% else %}qt5-webkit{% endif %} \
+ python-pyqt5 \
+ xorg-xinit \
+ xorg-server-xvfb \
+ ttf-bitstream-vera \
+ gcc \
+ libyaml \
+ xorg-xdpyinfo
+
+USER user
+WORKDIR /home/user
+
+CMD git clone /outside qutebrowser.git && \
+ cd qutebrowser.git && \
+ tox -e py
diff --git a/scripts/dev/ci/docker/README.md b/scripts/dev/ci/docker/README.md
new file mode 100644
index 000000000..eb2b8db91
--- /dev/null
+++ b/scripts/dev/ci/docker/README.md
@@ -0,0 +1,9 @@
+This directory contains a Dockerfile template for containers used to test
+qutebrowser on CI.
+
+The `generate.py` script uses that template to generate various image
+configuration.
+
+The images are rebuilt via Github Actions in this directory, and qutebrowser
+then downloads them during the CI run. Note that means that it'll take a while
+until builds will use the newer image if you make a change to this directory.
diff --git a/scripts/dev/ci/docker/generate.py b/scripts/dev/ci/docker/generate.py
new file mode 100644
index 000000000..7d09fdb20
--- /dev/null
+++ b/scripts/dev/ci/docker/generate.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2019-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/>.
+
+"""Generate Dockerfiles for qutebrowser's CI."""
+
+import sys
+
+import jinja2
+
+
+def main():
+ with open('Dockerfile.j2') as f:
+ template = jinja2.Template(f.read())
+
+ image = sys.argv[1]
+ config = {
+ 'archlinux-webkit': {'webengine': False, 'unstable': False},
+ 'archlinux-webengine': {'webengine': True, 'unstable': False},
+ 'archlinux-webengine-unstable': {'webengine': True, 'unstable': True},
+ }[image]
+
+ with open('Dockerfile', 'w') as f:
+ f.write(template.render(**config))
+ f.write('\n')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index 14373f94f..7b9ce769b 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -274,12 +274,35 @@ def check_userscripts_descriptions(_args: argparse.Namespace = None) -> bool:
return ok
+def check_userscript_shebangs(_args: argparse.Namespace) -> bool:
+ """Check that we're using /usr/bin/env in shebangs."""
+ ok = True
+ folder = pathlib.Path('misc/userscripts')
+
+ for sub in folder.iterdir():
+ if sub.is_dir() or sub.name == 'README.md':
+ continue
+
+ with sub.open('r', encoding='utf-8') as f:
+ shebang = f.readline()
+ assert shebang.startswith('#!'), shebang
+ binary = shebang.split()[0][2:]
+
+ if binary not in ['/bin/sh', '/usr/bin/env']:
+ bin_name = pathlib.Path(binary).name
+ print(f"In {sub}, use #!/usr/bin/env {bin_name} instead of #!{binary}")
+ ok = False
+
+ return ok
+
+
def main() -> int:
checkers = {
'git': check_git,
'vcs': check_vcs_conflict,
'spelling': check_spelling,
- 'userscripts': check_userscripts_descriptions,
+ 'userscript-descriptions': check_userscripts_descriptions,
+ 'userscript-shebangs': check_userscript_shebangs,
'changelog-urls': check_changelog_urls,
}
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index c63441047..87740c5bb 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -73,7 +73,7 @@ CHANGELOG_URLS = {
'pytest-bdd': 'https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst',
'snowballstemmer': 'https://github.com/snowballstem/snowball/blob/master/NEWS',
'virtualenv': 'https://virtualenv.pypa.io/en/latest/changelog.html',
- 'packaging': 'https://pypi.org/project/packaging/',
+ 'packaging': 'https://packaging.pypa.io/en/latest/changelog.html',
'build': 'https://github.com/pypa/build/commits/master',
'attrs': 'http://www.attrs.org/en/stable/changelog.html',
'Jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst',
@@ -82,7 +82,7 @@ CHANGELOG_URLS = {
'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/',
'flake8-debugger': 'https://github.com/JBKahn/flake8-debugger/',
'flake8-builtins': 'https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst',
- 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear',
+ 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear#change-log',
'flake8-tidy-imports': 'https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst',
'flake8-tuple': 'https://github.com/ar4s/flake8_tuple/blob/master/HISTORY.rst',
'flake8-comprehensions': 'https://github.com/adamchainz/flake8-comprehensions/blob/master/HISTORY.rst',
@@ -105,7 +105,7 @@ CHANGELOG_URLS = {
'more-itertools': 'https://github.com/erikrose/more-itertools/blob/master/docs/versions.rst',
'pydocstyle': 'http://www.pydocstyle.org/en/latest/release_notes.html',
'Sphinx': 'https://www.sphinx-doc.org/en/master/changes.html',
- 'Babel': 'http://babel.pocoo.org/en/latest/changelog.html',
+ 'Babel': 'https://github.com/python-babel/babel/blob/master/CHANGES',
'alabaster': 'https://alabaster.readthedocs.io/en/latest/changelog.html',
'imagesize': 'https://github.com/shibukawa/imagesize_py/commits/master',
'pytz': 'https://mm.icann.org/pipermail/tz-announce/',
@@ -130,9 +130,8 @@ CHANGELOG_URLS = {
'six': 'https://github.com/benjaminp/six/blob/master/CHANGES',
'altgraph': 'https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst',
'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst',
- 'lxml': 'https://lxml.de/4.6/changes-4.6.0.html',
+ 'lxml': 'https://lxml.de/index.html#old-versions',
'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/blob/master/doc/changelog.rst',
'cryptography': 'https://cryptography.io/en/latest/changelog.html',
@@ -170,7 +169,6 @@ CHANGELOG_URLS = {
'python-dateutil': 'https://dateutil.readthedocs.io/en/stable/changelog.html',
'appdirs': 'https://github.com/ActiveState/appdirs/blob/master/CHANGES.rst',
'pluggy': 'https://github.com/pytest-dev/pluggy/blob/master/CHANGELOG.rst',
- 'tox-venv': 'https://github.com/tox-dev/tox-venv/blob/master/CHANGELOG.rst',
'inflect': 'https://github.com/jazzband/inflect/blob/master/CHANGES.rst',
'jinja2-pluralize': 'https://github.com/audreyfeldroy/jinja2_pluralize/blob/master/HISTORY.rst',
'mypy-extensions': 'https://github.com/python/mypy_extensions/commits/master',
@@ -359,6 +357,7 @@ def _get_changed_files():
def parse_versioned_line(line):
"""Parse a requirements.txt line into name/version."""
if '==' in line:
+ line = line.rsplit('#', maxsplit=1)[0] # Strip comments
name, version = line.split('==')
if ';' in version: # pip environment markers
version = version.split(';')[0].strip()
diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py
index f582e23b0..bfddff736 100644
--- a/scripts/mkvenv.py
+++ b/scripts/mkvenv.py
@@ -24,12 +24,13 @@
import argparse
import pathlib
import sys
+import re
import os
import os.path
import shutil
-import venv
+import venv as pyvenv
import subprocess
-from typing import List, Optional, Tuple
+from typing import List, Optional, Tuple, Dict, Union
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils, link_pyqt
@@ -38,7 +39,22 @@ from scripts import utils, link_pyqt
REPO_ROOT = pathlib.Path(__file__).parent.parent
-def parse_args() -> argparse.Namespace:
+class Error(Exception):
+
+ """Exception for errors in this script."""
+
+ def __init__(self, msg, code=1):
+ super().__init__(msg)
+ self.code = code
+
+
+def print_command(*cmd: Union[str, pathlib.Path], venv: bool) -> None:
+ """Print a command being run."""
+ prefix = 'venv$ ' if venv else '$ '
+ utils.print_col(prefix + ' '.join([str(e) for e in cmd]), 'blue')
+
+
+def parse_args(argv: List[str] = None) -> argparse.Namespace:
"""Parse commandline arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--keep',
@@ -74,7 +90,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument('--tox-error',
action='store_true',
help=argparse.SUPPRESS)
- return parser.parse_args()
+ return parser.parse_args(argv)
def pyqt_versions() -> List[str]:
@@ -93,22 +109,40 @@ def pyqt_versions() -> List[str]:
return versions + ['auto']
-def run_venv(venv_dir: pathlib.Path, executable, *args: str) -> None:
+def run_venv(
+ venv_dir: pathlib.Path,
+ executable,
+ *args: str,
+ capture_output=False,
+ capture_error=False,
+ env=None,
+) -> subprocess.CompletedProcess:
"""Run the given command inside the virtualenv."""
subdir = 'Scripts' if os.name == 'nt' else 'bin'
+ if env is None:
+ proc_env = None
+ else:
+ proc_env = os.environ.copy()
+ proc_env.update(env)
+
try:
- subprocess.run([str(venv_dir / subdir / executable)] +
- [str(arg) for arg in args], check=True)
+ return subprocess.run(
+ [str(venv_dir / subdir / executable)] + [str(arg) for arg in args],
+ check=True,
+ universal_newlines=capture_output or capture_error,
+ stdout=subprocess.PIPE if capture_output else None,
+ stderr=subprocess.PIPE if capture_error else None,
+ env=proc_env,
+ )
except subprocess.CalledProcessError as e:
- utils.print_error("Subprocess failed, exiting")
- sys.exit(e.returncode)
+ raise Error("Subprocess failed, exiting") from e
def pip_install(venv_dir: pathlib.Path, *args: str) -> None:
"""Run a pip install command inside the virtualenv."""
arg_str = ' '.join(str(arg) for arg in args)
- utils.print_col('venv$ pip install {}'.format(arg_str), 'blue')
+ print_command('pip install', arg_str, venv=True)
run_venv(venv_dir, 'python', '-m', 'pip', 'install', *args)
@@ -125,27 +159,25 @@ def delete_old_venv(venv_dir: pathlib.Path) -> None:
]
if not any(m.exists() for m in markers):
- utils.print_error('{} does not look like a virtualenv, '
- 'cowardly refusing to remove it.'.format(venv_dir))
- sys.exit(1)
+ raise Error('{} does not look like a virtualenv, cowardly refusing to '
+ 'remove it.'.format(venv_dir))
- utils.print_col('$ rm -r {}'.format(venv_dir), 'blue')
+ print_command('rm -r', venv_dir, venv=False)
shutil.rmtree(str(venv_dir))
def create_venv(venv_dir: pathlib.Path, use_virtualenv: bool = False) -> None:
"""Create a new virtualenv."""
if use_virtualenv:
- utils.print_col('$ python3 -m virtualenv {}'.format(venv_dir), 'blue')
+ print_command('python3 -m virtualenv', venv_dir, venv=False)
try:
subprocess.run([sys.executable, '-m', 'virtualenv', venv_dir],
check=True)
except subprocess.CalledProcessError as e:
- utils.print_error("virtualenv failed, exiting")
- sys.exit(e.returncode)
+ raise Error("virtualenv failed, exiting", e.returncode)
else:
- utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue')
- venv.create(str(venv_dir), with_pip=True)
+ print_command('python3 -m venv', venv_dir, venv=False)
+ pyvenv.create(str(venv_dir), with_pip=True)
def upgrade_seed_pkgs(venv_dir: pathlib.Path) -> None:
@@ -202,6 +234,129 @@ def install_pyqt_wheels(venv_dir: pathlib.Path,
pip_install(venv_dir, *wheels)
+def apply_xcb_util_workaround(
+ venv_dir: pathlib.Path,
+ pyqt_type: str,
+ pyqt_version: str,
+) -> None:
+ """If needed (Debian Stable), symlink libxcb-util.so.0 -> .1.
+
+ WORKAROUND for https://bugreports.qt.io/browse/QTBUG-88688
+ """
+ utils.print_title("Running xcb-util workaround")
+
+ if not sys.platform.startswith('linux'):
+ print("Workaround not needed: Not on Linux.")
+ return
+ if pyqt_type != 'binary':
+ print("Workaround not needed: Not installing from PyQt binaries.")
+ return
+ if pyqt_version not in ['auto', '5.15']:
+ print("Workaround not needed: Not installing Qt 5.15.")
+ return
+
+ libs = _find_libs()
+ abi_type = 'libc6,x86-64' # the only one PyQt wheels are available for
+
+ if ('libxcb-util.so.1', abi_type) in libs:
+ print("Workaround not needed: libxcb-util.so.1 found.")
+ return
+
+ try:
+ libxcb_util_libs = libs['libxcb-util.so.0', abi_type]
+ except KeyError:
+ utils.print_error('Workaround failed: libxcb-util.so.0 not found.')
+ return
+
+ if len(libxcb_util_libs) > 1:
+ utils.print_error(
+ f'Workaround failed: Multiple matching libxcb-util found: '
+ f'{libxcb_util_libs}')
+ return
+
+ libxcb_util_path = pathlib.Path(libxcb_util_libs[0])
+
+ code = [
+ 'from PyQt5.QtCore import QLibraryInfo',
+ 'print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))',
+ ]
+ proc = run_venv(venv_dir, 'python', '-c', '; '.join(code), capture_output=True)
+ venv_lib_path = pathlib.Path(proc.stdout.strip())
+
+ link_path = venv_lib_path / libxcb_util_path.with_suffix('.1').name
+
+ # This gives us a nicer path to print, and also conveniently makes sure we
+ # didn't accidentally end up with a path outside the venv.
+ rel_link_path = venv_dir / link_path.relative_to(venv_dir.resolve())
+ print_command('ln -s', libxcb_util_path, rel_link_path, venv=False)
+
+ link_path.symlink_to(libxcb_util_path)
+
+
+def _find_libs() -> Dict[Tuple[str, str], List[str]]:
+ """Find all system-wide .so libraries."""
+ all_libs: Dict[Tuple[str, str], List[str]] = {}
+
+ if pathlib.Path("/sbin/ldconfig").exists():
+ # /sbin might not be in PATH on e.g. Debian
+ ldconfig_bin = "/sbin/ldconfig"
+ else:
+ ldconfig_bin = "ldconfig"
+ ldconfig_proc = subprocess.run(
+ [ldconfig_bin, '-p'],
+ check=True,
+ stdout=subprocess.PIPE,
+ encoding=sys.getfilesystemencoding(),
+ )
+
+ pattern = re.compile(r'(?P<name>\S+) \((?P<abi_type>[^)]+)\) => (?P<path>.*)')
+ for line in ldconfig_proc.stdout.splitlines():
+ match = pattern.fullmatch(line.strip())
+ if match is None:
+ if 'libs found in cache' not in line:
+ utils.print_col(f'Failed to match ldconfig output: {line}', 'yellow')
+ continue
+
+ key = match.group('name'), match.group('abi_type')
+ path = match.group('path')
+
+ libs = all_libs.setdefault(key, [])
+ libs.append(path)
+
+ return all_libs
+
+
+def run_qt_smoke_test(venv_dir: pathlib.Path) -> None:
+ """Make sure the Qt installation works."""
+ utils.print_title("Running Qt smoke test")
+ code = [
+ 'import sys',
+ 'from PyQt5.QtWidgets import QApplication',
+ 'from PyQt5.QtCore import qVersion, QT_VERSION_STR, PYQT_VERSION_STR',
+ 'print(f"Python: {sys.version}")',
+ 'print(f"qVersion: {qVersion()}")',
+ 'print(f"QT_VERSION_STR: {QT_VERSION_STR}")',
+ 'print(f"PYQT_VERSION_STR: {PYQT_VERSION_STR}")',
+ 'QApplication([])',
+ 'print("Qt seems to work properly!")',
+ 'print()',
+ ]
+ try:
+ run_venv(
+ venv_dir,
+ 'python', '-c', '; '.join(code),
+ env={'QT_DEBUG_PLUGINS': '1'},
+ capture_error=True
+ )
+ except Error as e:
+ proc_e = e.__cause__
+ assert isinstance(proc_e, subprocess.CalledProcessError), proc_e
+ print(proc_e.stderr)
+ raise Error(
+ f"Smoke test failed with status {proc_e.returncode}. "
+ "You might find additional information in the debug output above.")
+
+
def install_requirements(venv_dir: pathlib.Path) -> None:
"""Install qutebrowser's requirement.txt."""
utils.print_title("Installing other qutebrowser dependencies")
@@ -233,27 +388,24 @@ def regenerate_docs(venv_dir: pathlib.Path,
a2h_args = []
script_path = pathlib.Path(__file__).parent / 'asciidoc2html.py'
- utils.print_col('venv$ python3 scripts/asciidoc2html.py {}'
- .format(' '.join(a2h_args)), 'blue')
+ print_command('python3 scripts/asciidoc2html.py', *a2h_args, venv=True)
run_venv(venv_dir, 'python', str(script_path), *a2h_args)
-def main() -> None:
+def run(args) -> None:
"""Install qutebrowser in a virtualenv.."""
- args = parse_args()
venv_dir = pathlib.Path(args.venv_dir)
wheels_dir = pathlib.Path(args.pyqt_wheels_dir)
utils.change_cwd()
if (args.pyqt_version != 'auto' and
args.pyqt_type not in ['binary', 'source']):
- utils.print_error('The --pyqt-version option is only available when '
- 'installing PyQt from binary or source')
- sys.exit(1)
- elif args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels':
- utils.print_error('The --pyqt-wheels-dir option is only available '
- 'when installing PyQt from wheels')
- sys.exit(1)
+ raise Error('The --pyqt-version option is only available when installing PyQt '
+ 'from binary or source')
+
+ if args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels':
+ raise Error('The --pyqt-wheels-dir option is only available when installing '
+ 'PyQt from wheels')
if not args.keep:
utils.print_title("Creating virtual environment")
@@ -275,6 +427,10 @@ def main() -> None:
else:
raise AssertionError
+ apply_xcb_util_workaround(venv_dir, args.pyqt_type, args.pyqt_version)
+ if args.pyqt_type != 'skip':
+ run_qt_smoke_test(venv_dir)
+
install_requirements(venv_dir)
install_qutebrowser(venv_dir)
if args.dev:
@@ -284,5 +440,14 @@ def main() -> None:
regenerate_docs(venv_dir, args.asciidoc)
+def main():
+ args = parse_args()
+ try:
+ run(args)
+ except Error as e:
+ utils.print_error(str(e))
+ sys.exit(e.code)
+
+
if __name__ == '__main__':
main()
diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py
index 11066fb92..17b457521 100644
--- a/tests/end2end/conftest.py
+++ b/tests/end2end/conftest.py
@@ -35,7 +35,7 @@ from PyQt5.QtCore import PYQT_VERSION, QCoreApplication
pytest.register_assert_rewrite('end2end.fixtures')
-from end2end.fixtures.webserver import server, server_per_test, ssl_server
+from end2end.fixtures.webserver import server, server_per_test, server2, ssl_server
from end2end.fixtures.quteprocess import (quteproc_process, quteproc,
quteproc_new)
from end2end.fixtures.testprocess import pytest_runtest_makereport
diff --git a/tests/end2end/features/test_downloads_bdd.py b/tests/end2end/features/test_downloads_bdd.py
index 951ebcfea..1f27d2794 100644
--- a/tests/end2end/features/test_downloads_bdd.py
+++ b/tests/end2end/features/test_downloads_bdd.py
@@ -96,12 +96,6 @@ def wait_for_download_prompt(tmpdir, quteproc, path):
"(reason: question asked)")
-@bdd.when("I download an SSL page")
-def download_ssl_page(quteproc, ssl_server):
- quteproc.send_cmd(':download https://localhost:{}/'
- .format(ssl_server.port))
-
-
@bdd.then(bdd.parsers.parse("The downloaded file {filename} should not exist"))
def download_should_not_exist(filename, tmpdir):
path = tmpdir / 'downloads' / filename
diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py
index 85778c3e0..51352c539 100644
--- a/tests/end2end/fixtures/testprocess.py
+++ b/tests/end2end/fixtures/testprocess.py
@@ -101,7 +101,7 @@ def pytest_runtest_makereport(item, call):
return
quteproc_log = getattr(item, '_quteproc_log', None)
- server_log = getattr(item, '_server_log', None)
+ server_logs = getattr(item, '_server_logs', [])
if not hasattr(report.longrepr, 'addsection'):
# In some conditions (on macOS and Windows it seems), report.longrepr
@@ -114,11 +114,11 @@ def pytest_runtest_makereport(item, call):
verbose = item.config.getoption('--verbose')
if quteproc_log is not None:
- report.longrepr.addsection("qutebrowser output",
- _render_log(quteproc_log, verbose=verbose))
- if server_log is not None:
- report.longrepr.addsection("server output",
- _render_log(server_log, verbose=verbose))
+ report.longrepr.addsection(
+ "qutebrowser output", _render_log(quteproc_log, verbose=verbose))
+ for name, content in server_logs:
+ report.longrepr.addsection(
+ f"{name} output", _render_log(content, verbose=verbose))
class Process(QObject):
diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py
index 9f4383b35..d40739724 100644
--- a/tests/end2end/fixtures/webserver.py
+++ b/tests/end2end/fixtures/webserver.py
@@ -192,21 +192,41 @@ def server(qapp, request):
@pytest.fixture(autouse=True)
def server_per_test(server, request):
"""Fixture to clean server request list after each test."""
- request.node._server_log = server.captured_log
+ if not hasattr(request.node, '_server_logs'):
+ request.node._server_logs = []
+ request.node._server_logs.append(('server', server.captured_log))
+
server.before_test()
yield
server.after_test()
@pytest.fixture
+def server2(qapp, request):
+ """Fixture for a second server object for cross-origin tests."""
+ server = WebserverProcess(request, 'webserver_sub')
+
+ if not hasattr(request.node, '_server_logs'):
+ request.node._server_logs = []
+ request.node._server_logs.append(('secondary server', server.captured_log))
+
+ server.start()
+ yield server
+ server.terminate()
+
+
+@pytest.fixture
def ssl_server(request, qapp):
"""Fixture for a webserver with a self-signed SSL certificate.
- This needs to be explicitly used in a test, and overwrites the server log
- used in that test.
+ This needs to be explicitly used in a test.
"""
server = WebserverProcess(request, 'webserver_sub_ssl')
- request.node._server_log = server.captured_log
+
+ if not hasattr(request.node, '_server_logs'):
+ request.node._server_logs = []
+ request.node._server_logs.append(('SSL server', server.captured_log))
+
server.start()
yield server
server.after_test()
diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py
index cf8ffd006..e38d64bb8 100644
--- a/tests/end2end/fixtures/webserver_sub.py
+++ b/tests/end2end/fixtures/webserver_sub.py
@@ -29,8 +29,8 @@ parameters or headers with the same name properly.
import sys
import json
import time
-import os
import threading
+import pathlib
from http import HTTPStatus
import cheroot.wsgi
@@ -40,6 +40,9 @@ app = flask.Flask(__name__)
_redirect_later_event = None
+END2END_DIR = pathlib.Path(__file__).resolve().parents[1]
+
+
@app.route('/')
def root():
"""Show simple text."""
@@ -54,15 +57,8 @@ def send_data(path):
If a directory is requested, its index.html is sent.
"""
- if hasattr(sys, 'frozen'):
- basedir = os.path.realpath(os.path.dirname(sys.executable))
- data_dir = os.path.join(basedir, 'end2end', 'data')
- else:
- basedir = os.path.join(os.path.realpath(os.path.dirname(__file__)),
- '..')
- data_dir = os.path.join(basedir, 'data')
- print(basedir)
- if os.path.isdir(os.path.join(data_dir, path)):
+ data_dir = END2END_DIR / 'data'
+ if (data_dir / path).is_dir():
path += '/index.html'
return flask.send_from_directory(data_dir, path)
@@ -248,6 +244,12 @@ def view_headers():
return flask.jsonify(headers=dict(flask.request.headers))
+@app.route('/headers-link/<int:port>')
+def headers_link(port):
+ """Get a (possibly cross-origin) link to /headers."""
+ return flask.render_template('headers-link.html', port=port)
+
+
@app.route('/response-headers')
def response_headers():
"""Return a set of response headers from the query string."""
@@ -273,11 +275,9 @@ def view_user_agent():
@app.route('/favicon.ico')
def favicon():
- basedir = os.path.join(os.path.realpath(os.path.dirname(__file__)),
- '..', '..', '..')
- return flask.send_from_directory(os.path.join(basedir, 'icons'),
- 'qutebrowser.ico',
- mimetype='image/vnd.microsoft.icon')
+ icon_dir = END2END_DIR.parents[1] / 'icons'
+ return flask.send_from_directory(
+ icon_dir, 'qutebrowser.ico', mimetype='image/vnd.microsoft.icon')
@app.after_request
@@ -321,9 +321,9 @@ class WSGIServer(cheroot.wsgi.Server):
def main():
- if hasattr(sys, 'frozen'):
- basedir = os.path.realpath(os.path.dirname(sys.executable))
- app.template_folder = os.path.join(basedir, 'end2end', 'templates')
+ app.template_folder = END2END_DIR / 'templates'
+ assert app.template_folder.is_dir(), app.template_folder
+
port = int(sys.argv[1])
server = WSGIServer(('127.0.0.1', port), app)
server.start()
diff --git a/tests/end2end/templates/headers-link.html b/tests/end2end/templates/headers-link.html
new file mode 100644
index 000000000..fece530b1
--- /dev/null
+++ b/tests/end2end/templates/headers-link.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Link to header page</title>
+ </head>
+ <body>
+ <a href="http://localhost:{{ port }}/headers" id="link">headers</a>
+ </body>
+</html>
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
index abffc9350..e34bd912d 100644
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -23,6 +23,7 @@ import subprocess
import sys
import logging
import re
+import json
import pytest
from PyQt5.QtCore import QProcess
@@ -383,3 +384,36 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
quteproc_new.send_cmd(':quit')
quteproc_new.wait_for_quit()
+
+
+@pytest.mark.parametrize('value, expected', [
+ ('always', 'http://localhost:(port2)/headers-link/(port)'),
+ ('never', None),
+ ('same-domain', 'http://localhost:(port2)/'), # None with QtWebKit
+])
+def test_referrer(quteproc_new, server, server2, request, value, expected):
+ """Check referrer settings."""
+ args = _base_args(request.config) + [
+ '--temp-basedir',
+ '-s', 'content.headers.referer', value,
+ ]
+ quteproc_new.start(args)
+
+ quteproc_new.open_path(f'headers-link/{server.port}', port=server2.port)
+ quteproc_new.send_cmd(':click-element id link')
+ quteproc_new.wait_for_load_finished('headers')
+
+ content = quteproc_new.get_content()
+ data = json.loads(content)
+ print(data)
+ headers = data['headers']
+
+ if not request.config.webengine and value == 'same-domain':
+ # With QtWebKit and same-domain, we don't send a referer at all.
+ expected = None
+
+ if expected is not None:
+ for key, val in [('(port)', server.port), ('(port2)', server2.port)]:
+ expected = expected.replace(key, str(val))
+
+ assert headers.get('Referer') == expected
diff --git a/tests/end2end/test_mkvenv.py b/tests/end2end/test_mkvenv.py
new file mode 100644
index 000000000..430be0279
--- /dev/null
+++ b/tests/end2end/test_mkvenv.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+# 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/>.
+
+
+from scripts import mkvenv
+
+
+def test_smoke(tmp_path):
+ """Simple smoke test of mkvenv.py."""
+ args = mkvenv.parse_args(['--venv-dir', str(tmp_path / 'venv'), '--skip-docs'])
+ mkvenv.run(args)
diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py
index c70b858f5..c1990de0d 100644
--- a/tests/unit/browser/test_history.py
+++ b/tests/unit/browser/test_history.py
@@ -464,7 +464,8 @@ class TestCompletionMetaInfo:
def test_contains_keyerror(self, metainfo):
with pytest.raises(KeyError):
- 'does_not_exist' in metainfo # pylint: disable=pointless-statement
+ # pylint: disable=pointless-statement
+ 'does_not_exist' in metainfo # noqa: B015
def test_getitem_keyerror(self, metainfo):
with pytest.raises(KeyError):
diff --git a/tests/unit/browser/test_navigate.py b/tests/unit/browser/test_navigate.py
index 5fe0acbf6..5a93a517c 100644
--- a/tests/unit/browser/test_navigate.py
+++ b/tests/unit/browser/test_navigate.py
@@ -187,6 +187,8 @@ class TestUp:
('/one/two/three', 1, '/one/two'),
('/one/two/three?foo=bar', 1, '/one/two'),
('/one/two/three', 2, '/one'),
+ ('/one/two%2Fthree', 1, '/one'),
+ ('/one/two%2Fthree/four', 1, '/one/two%2Fthree'),
])
def test_up(self, url_suffix, count, expected_suffix):
url_base = 'https://example.com'
diff --git a/tests/unit/browser/webengine/test_darkmode.py b/tests/unit/browser/webengine/test_darkmode.py
index 05cd7737a..3e62000d2 100644
--- a/tests/unit/browser/webengine/test_darkmode.py
+++ b/tests/unit/browser/webengine/test_darkmode.py
@@ -169,6 +169,22 @@ def test_variant(monkeypatch, qversion, webengine_version, expected):
assert darkmode._variant() == expected
+@pytest.mark.parametrize('value, is_valid, expected', [
+ ('invalid_value', False, darkmode.Variant.qt_515_0),
+ ('qt_515_2', True, darkmode.Variant.qt_515_2),
+])
+def test_variant_override(monkeypatch, caplog, value, is_valid, expected):
+ monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: None)
+ monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', 0x050f00)
+ monkeypatch.setenv('QUTE_DARKMODE_VARIANT', value)
+
+ with caplog.at_level(logging.WARNING):
+ assert darkmode._variant() == expected
+
+ log_msg = 'Ignoring invalid QUTE_DARKMODE_VARIANT=invalid_value'
+ assert (log_msg in caplog.messages) != is_valid
+
+
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'
diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py
index 0aa3943e7..e1c9d04f8 100644
--- a/tests/unit/browser/webkit/network/test_networkreply.py
+++ b/tests/unit/browser/webkit/network/test_networkreply.py
@@ -52,9 +52,8 @@ class TestFixedDataNetworkReply:
b'Hello World! This is a test.'])
def test_data(self, qtbot, req, data):
reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo')
- with qtbot.waitSignal(reply.metaDataChanged), \
- qtbot.waitSignal(reply.readyRead), \
- qtbot.waitSignal(reply.finished):
+ with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead,
+ reply.finished], order='strict'):
pass
assert reply.bytesAvailable() == len(data)
@@ -79,7 +78,7 @@ def test_error_network_reply(qtbot, req):
reply = networkreply.ErrorNetworkReply(
req, "This is an error", QNetworkReply.UnknownNetworkError)
- with qtbot.waitSignal(reply.error), qtbot.waitSignal(reply.finished):
+ with qtbot.waitSignals([reply.error, reply.finished], order='strict'):
pass
reply.abort() # shouldn't do anything
diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py
index 41dfee98b..4732837a1 100644
--- a/tests/unit/completion/test_completiondelegate.py
+++ b/tests/unit/completion/test_completiondelegate.py
@@ -18,6 +18,8 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
from unittest import mock
+import hypothesis
+import hypothesis.strategies
import pytest
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextDocument, QColor
@@ -69,6 +71,13 @@ def test_benchmark_highlight(benchmark):
benchmark(bench)
+@hypothesis.given(text=hypothesis.strategies.text())
+def test_pattern_hypothesis(text):
+ """Make sure we can't produce invalid patterns."""
+ doc = QTextDocument()
+ completiondelegate._Highlighter(doc, text, Qt.red)
+
+
def test_highlighted(qtbot):
"""Make sure highlighting works.
diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py
index 1fc0b4d73..98e70dc01 100644
--- a/tests/unit/completion/test_completionmodel.py
+++ b/tests/unit/completion/test_completionmodel.py
@@ -77,8 +77,8 @@ def test_set_pattern(pat, qtbot):
for c in cats:
c.set_pattern = mock.Mock(spec=[])
model.add_category(c)
- with qtbot.waitSignal(model.layoutAboutToBeChanged), \
- qtbot.waitSignal(model.layoutChanged):
+ with qtbot.waitSignals([model.layoutAboutToBeChanged, model.layoutChanged],
+ order='strict'):
model.set_pattern(pat)
for c in cats:
c.set_pattern.assert_called_with(pat)
diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py
index 356b854c5..c0ef4b47f 100644
--- a/tests/unit/completion/test_completionwidget.py
+++ b/tests/unit/completion/test_completionwidget.py
@@ -161,6 +161,7 @@ def test_completion_item_focus_no_model(which, completionview, model, qtbot):
completionview.completion_item_focus(which)
+@pytest.mark.skip("Seems to disagree with reality, see #5897")
def test_completion_item_focus_fetch(completionview, model, qtbot):
"""Test that on_next_prev_item moves the selection properly.
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index 2ac7084dd..8b4653b58 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -26,6 +26,8 @@ import time
from datetime import datetime
from unittest import mock
+import hypothesis
+import hypothesis.strategies
import pytest
from PyQt5.QtCore import QUrl, QDateTime
try:
@@ -37,7 +39,8 @@ except ImportError:
from qutebrowser.misc import objects
from qutebrowser.completion import completer
-from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
+from qutebrowser.completion.models import (
+ miscmodels, urlmodel, configmodel, listcategory)
from qutebrowser.config import configdata, configtypes
from qutebrowser.utils import usertypes
from qutebrowser.mainwindow import tabbedbrowser
@@ -1324,3 +1327,10 @@ def test_undo_completion(tabbed_browser_stubs, info):
"2020-01-01 00:00"),
],
})
+
+
+@hypothesis.given(text=hypothesis.strategies.text())
+def test_listcategory_hypothesis(text):
+ """Make sure we can't produce invalid patterns."""
+ cat = listcategory.ListCategory("test", [])
+ cat.set_pattern(text)
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
index 588e4a5cf..b30ab4bee 100644
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -432,12 +432,11 @@ class TestConfig:
assert conf.get_obj(name1) == 'never'
assert conf.get_obj(name2) is True
- with qtbot.waitSignal(conf.changed), qtbot.waitSignal(conf.changed):
+ with qtbot.waitSignals([conf.changed, conf.changed]) as blocker:
conf.clear(save_yaml=save_yaml)
- # Doesn't work with PyQt 5.15.1 workaround
- # options = {blocker1.args[0], blocker2.args[0]}
- # assert options == {name1, name2}
+ options = {e.args[0] for e in blocker.all_signals_and_args}
+ assert options == {name1, name2}
if save_yaml:
assert yaml_value(name1) is usertypes.UNSET
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index 76d3ac094..710018604 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -138,10 +138,16 @@ class TestValidValues:
def test_descriptions(self, klass):
"""Test descriptions."""
- vv = klass(('foo', "foo desc"), ('bar', "bar desc"), 'baz')
- assert vv.descriptions['foo'] == "foo desc"
- assert vv.descriptions['bar'] == "bar desc"
- assert 'baz' not in vv.descriptions
+ vv = klass(
+ ('one-with', "desc 1"),
+ ('two-with', "desc 2"),
+ 'three-without',
+ ('four-without', None)
+ )
+ assert vv.descriptions['one-with'] == "desc 1"
+ assert vv.descriptions['two-with'] == "desc 2"
+ assert 'three-without' not in vv.descriptions
+ assert 'four-without' not in vv.descriptions
@pytest.mark.parametrize('args, expected', [
(['a', 'b'], "<qutebrowser.config.configtypes.ValidValues "
@@ -396,14 +402,11 @@ class MappingSubclass(configtypes.MappingType):
"""A MappingType we use in TestMappingType which is valid/good."""
MAPPING = {
- 'one': 1,
- 'two': 2,
+ 'one': (1, 'one doc'),
+ 'two': (2, 'two doc'),
+ 'three': (3, None),
}
- def __init__(self, none_ok=False):
- super().__init__(none_ok)
- self.valid_values = configtypes.ValidValues('one', 'two')
-
class TestMappingType:
@@ -429,11 +432,12 @@ class TestMappingType:
def test_to_str(self, klass):
assert klass().to_str('one') == 'one'
- @pytest.mark.parametrize('typ', [configtypes.ColorSystem(),
- configtypes.Position(),
- configtypes.SelectOnRemove()])
- def test_mapping_type_matches_valid_values(self, typ):
- assert sorted(typ.MAPPING) == sorted(typ.valid_values)
+ def test_valid_values(self, klass):
+ assert klass().valid_values == configtypes.ValidValues(
+ ('one', 'one doc'),
+ ('two', 'two doc'),
+ ('three', None),
+ )
class TestString:
diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py
index 7e1a7c744..4830340cf 100644
--- a/tests/unit/config/test_configutils.py
+++ b/tests/unit/config/test_configutils.py
@@ -21,6 +21,7 @@ import hypothesis
from hypothesis import strategies
import pytest
from PyQt5.QtCore import QUrl
+from PyQt5.QtWidgets import QLabel
from qutebrowser.config import configutils, configdata, configtypes, configexc
from qutebrowser.utils import urlmatch, usertypes, qtutils
@@ -364,3 +365,45 @@ class TestFontFamilies:
assert family
str(families)
+
+ def test_system_default_basics(self, qapp):
+ families = configutils.FontFamilies.from_system_default()
+ assert len(families) == 1
+ assert str(families)
+
+ def test_system_default_rendering(self, qtbot):
+ families = configutils.FontFamilies.from_system_default()
+
+ label = QLabel()
+ qtbot.add_widget(label)
+ label.setText("Hello World")
+
+ stylesheet = f'font-family: {families.to_str(quote=True)}'
+ print(stylesheet)
+ label.setStyleSheet(stylesheet)
+
+ with qtbot.waitExposed(label):
+ # Needed so the font gets calculated
+ label.show()
+ info = label.fontInfo()
+
+ # Check the requested font to make sure CSS parsing worked
+ assert label.font().family() == families.family
+
+ # Try to find out whether the monospace font did a fallback on a non-monospace
+ # font...
+ fallback_label = QLabel()
+ qtbot.add_widget(label)
+ fallback_label.setText("fallback")
+
+ with qtbot.waitExposed(fallback_label):
+ # Needed so the font gets calculated
+ fallback_label.show()
+
+ fallback_family = fallback_label.fontInfo().family()
+ print(f'fallback: {fallback_family}')
+ if info.family() == fallback_family:
+ return
+
+ # If we didn't fall back, we should've gotten a fixed-pitch font.
+ assert info.fixedPitch(), info.family()
diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py
index dc6b655b3..051956a00 100644
--- a/tests/unit/config/test_qtargs.py
+++ b/tests/unit/config/test_qtargs.py
@@ -116,7 +116,7 @@ class TestQtArgs:
def test_in_process_stack_traces(self, monkeypatch, parser, backend,
version_check, debug_flag, expected):
monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, compiled=False: version_check)
+ lambda version, compiled=False, exact=False: version_check)
monkeypatch.setattr(qtargs.objects, 'backend', backend)
parsed = parser.parse_args(['--debug-flag', 'stack'] if debug_flag
else [])
@@ -252,14 +252,31 @@ class TestQtArgs:
else:
assert arg in args
- @pytest.mark.parametrize('referer, arg', [
- ('always', None),
- ('never', '--no-referrers'),
- ('same-domain', '--reduced-referrer-granularity'),
+ @pytest.mark.parametrize('qt_version, referer, arg', [
+ # 'always' -> no arguments
+ ('5.15.0', 'always', None),
+
+ # 'never' is handled via interceptor for most Qt versions
+ ('5.12.3', 'never', '--no-referrers'),
+ ('5.12.4', 'never', None),
+ ('5.13.0', 'never', '--no-referrers'),
+ ('5.13.1', 'never', None),
+ ('5.14.0', 'never', None),
+ ('5.15.0', 'never', None),
+
+ # 'same-domain' - arguments depend on Qt versions
+ ('5.13.0', 'same-domain', '--reduced-referrer-granularity'),
+ ('5.14.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'),
+ ('5.15.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'),
])
- def test_referer(self, config_stub, monkeypatch, parser, referer, arg):
- monkeypatch.setattr(qtargs.objects, 'backend',
- usertypes.Backend.QtWebEngine)
+ def test_referer(self, config_stub, monkeypatch, parser, qt_version, referer, arg):
+ monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
+ monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: qt_version)
+
+ # Avoid WebRTC pipewire feature
+ monkeypatch.setattr(qtargs.utils, 'is_linux', False)
+ # Avoid overlay scrollbar feature
+ config_stub.val.scrolling.bar = 'never'
config_stub.val.content.headers.referer = referer
parsed = parser.parse_args([])
@@ -268,6 +285,7 @@ class TestQtArgs:
if arg is None:
assert '--no-referrers' not in args
assert '--reduced-referrer-granularity' not in args
+ assert '--enable-features=ReducedReferrerGranularity' not in args
else:
assert arg in args
diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py
index 88d9ae66c..cd57d33cb 100644
--- a/tests/unit/keyinput/test_basekeyparser.py
+++ b/tests/unit/keyinput/test_basekeyparser.py
@@ -21,7 +21,7 @@
from unittest import mock
-from PyQt5.QtCore import Qt, PYQT_VERSION
+from PyQt5.QtCore import Qt
import pytest
from qutebrowser.keyinput import basekeyparser, keyutils
@@ -305,8 +305,6 @@ class TestCount:
# https://github.com/qutebrowser/qutebrowser/issues/3743
handle_text(prompt_keyparser, Qt.Key_twosuperior, Qt.Key_B, Qt.Key_A)
- @pytest.mark.skipif(PYQT_VERSION == 0x050F01,
- reason='waitSignals is broken in PyQt 5.15.1')
def test_count_keystring_update(self, qtbot,
handle_text, prompt_keyparser):
"""Make sure the keystring is updated correctly when entering count."""
diff --git a/tests/unit/misc/test_earlyinit.py b/tests/unit/misc/test_earlyinit.py
index 728b4eb26..af229a40a 100644
--- a/tests/unit/misc/test_earlyinit.py
+++ b/tests/unit/misc/test_earlyinit.py
@@ -31,3 +31,20 @@ def test_init_faulthandler_stderr_none(monkeypatch, attr):
"""Make sure init_faulthandler works when sys.stderr/__stderr__ is None."""
monkeypatch.setattr(sys, attr, None)
earlyinit.init_faulthandler()
+
+
+@pytest.mark.parametrize('same', [True, False])
+def test_qt_version(same):
+ if same:
+ qt_version_str = '5.14.0'
+ expected = '5.14.0'
+ else:
+ qt_version_str = '5.13.0'
+ expected = '5.14.0 (compiled 5.13.0)'
+ actual = earlyinit.qt_version(qversion='5.14.0', qt_version_str=qt_version_str)
+ assert actual == expected
+
+
+def test_qt_version_no_args():
+ """Make sure qt_version without arguments at least works."""
+ earlyinit.qt_version()
diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py
index 7e71478ce..4ed19f64e 100644
--- a/tests/unit/misc/test_guiprocess.py
+++ b/tests/unit/misc/test_guiprocess.py
@@ -54,8 +54,8 @@ def fake_proc(monkeypatch, stubs):
def test_start(proc, qtbot, message_mock, py_proc):
"""Test simply starting a process."""
- with qtbot.waitSignal(proc.started, timeout=10000), \
- qtbot.waitSignal(proc.finished, timeout=10000):
+ with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
+ order='strict'):
argv = py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv)
@@ -70,8 +70,8 @@ def test_start_verbose(proc, qtbot, message_mock, py_proc):
"""Test starting a process verbosely."""
proc.verbose = True
- with qtbot.waitSignal(proc.started, timeout=10000), \
- qtbot.waitSignal(proc.finished, timeout=10000):
+ with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
+ order='strict'):
argv = py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv)
@@ -99,8 +99,9 @@ def test_start_output_message(proc, qtbot, caplog, message_mock, py_proc,
code.append("sys.exit(0)")
with caplog.at_level(logging.ERROR, 'message'):
- with qtbot.waitSignal(proc.started, timeout=10000), \
- qtbot.waitSignal(proc.finished, timeout=10000):
+ with qtbot.waitSignals([proc.started, proc.finished],
+ timeout=10000,
+ order='strict'):
argv = py_proc(';'.join(code))
proc.start(*argv)
@@ -146,8 +147,8 @@ def test_start_env(monkeypatch, qtbot, py_proc):
sys.exit(0)
""")
- with qtbot.waitSignal(proc.started, timeout=10000), \
- qtbot.waitSignal(proc.finished, timeout=10000):
+ with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
+ order='strict'):
proc.start(*argv)
data = qutescheme.spawn_output
@@ -186,12 +187,12 @@ def test_double_start(qtbot, proc, py_proc):
def test_double_start_finished(qtbot, proc, py_proc):
"""Test starting a GUIProcess twice (with the first call finished)."""
- with qtbot.waitSignal(proc.started, timeout=10000), \
- qtbot.waitSignal(proc.finished, timeout=10000):
+ with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
+ order='strict'):
argv = py_proc("import sys; sys.exit(0)")
proc.start(*argv)
- with qtbot.waitSignal(proc.started, timeout=10000), \
- qtbot.waitSignal(proc.finished, timeout=10000):
+ with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
+ order='strict'):
argv = py_proc("import sys; sys.exit(0)")
proc.start(*argv)
@@ -266,8 +267,8 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream):
def test_stdout_not_decodable(proc, qtbot, message_mock, py_proc):
"""Test handling malformed utf-8 in stdout."""
- with qtbot.waitSignal(proc.started, timeout=10000), \
- qtbot.waitSignal(proc.finished, timeout=10000):
+ with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
+ order='strict'):
argv = py_proc(r"""
import sys
# Using \x81 because it's invalid in UTF-8 and CP1252
diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py
index 2c9f7ea7f..3f53ca238 100644
--- a/tests/unit/misc/test_ipc.py
+++ b/tests/unit/misc/test_ipc.py
@@ -493,10 +493,10 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8')
(b'{"args": [], "target_arg": null}\n', 'invalid version'),
])
def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
+ signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
with caplog.at_level(logging.ERROR):
with qtbot.assertNotEmitted(ipc_server.got_args):
- with qtbot.waitSignal(ipc_server.got_invalid_data), \
- qtbot.waitSignal(connected_socket.disconnected):
+ with qtbot.waitSignals(signals, order='strict'):
connected_socket.write(data)
invalid_msg = 'Ignoring invalid IPC data from socket '
@@ -514,8 +514,8 @@ def test_multiline(qtbot, ipc_server, connected_socket):
version=ipc.PROTOCOL_VERSION))
with qtbot.assertNotEmitted(ipc_server.got_invalid_data):
- with qtbot.waitSignal(ipc_server.got_args), \
- qtbot.waitSignal(ipc_server.got_args):
+ with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args],
+ order='strict'):
connected_socket.write(data.encode('utf-8'))
assert len(spy) == 2
diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py
index 81d198946..2e54fb42e 100644
--- a/tests/unit/utils/test_qtutils.py
+++ b/tests/unit/utils/test_qtutils.py
@@ -26,6 +26,7 @@ import os.path
import unittest
import unittest.mock
+import attr
import pytest
from PyQt5.QtCore import (QDataStream, QPoint, QUrl, QByteArray, QIODevice,
QTimer, QBuffer, QFile, QProcess, QFileDevice)
@@ -53,23 +54,25 @@ else:
@pytest.mark.parametrize(['qversion', 'compiled', 'pyqt', 'version', 'exact',
'expected'], [
# equal versions
- ('5.4.0', None, None, '5.4.0', False, True),
- ('5.4.0', None, None, '5.4.0', True, True), # exact=True
- ('5.4.0', None, None, '5.4', True, True), # without trailing 0
+ ('5.14.0', None, None, '5.14.0', False, True),
+ ('5.14.0', None, None, '5.14.0', True, True), # exact=True
+ ('5.14.0', None, None, '5.14', True, True), # without trailing 0
# newer version installed
- ('5.4.1', None, None, '5.4', False, True),
- ('5.4.1', None, None, '5.4', True, False), # exact=True
+ ('5.14.1', None, None, '5.14', False, True),
+ ('5.14.1', None, None, '5.14', True, False), # exact=True
# older version installed
- ('5.3.2', None, None, '5.4', False, False),
- ('5.3.0', None, None, '5.3.2', False, False),
- ('5.3.0', None, None, '5.3.2', True, False), # exact=True
+ ('5.13.2', None, None, '5.14', False, False),
+ ('5.13.0', None, None, '5.13.2', False, False),
+ ('5.13.0', None, None, '5.13.2', True, False), # exact=True
# compiled=True
# new Qt runtime, but compiled against older version
- ('5.4.0', '5.3.0', '5.4.0', '5.4.0', False, False),
+ ('5.14.0', '5.13.0', '5.14.0', '5.14.0', False, False),
# new Qt runtime, compiled against new version, but old PyQt
- ('5.4.0', '5.4.0', '5.3.0', '5.4.0', False, False),
+ ('5.14.0', '5.14.0', '5.13.0', '5.14.0', False, False),
# all up-to-date
- ('5.4.0', '5.4.0', '5.4.0', '5.4.0', False, True),
+ ('5.14.0', '5.14.0', '5.14.0', '5.14.0', False, True),
+ # dev suffix
+ ('5.15.1', '5.15.1', '5.15.2.dev2009281246', '5.15.0', False, True),
])
# pylint: enable=bad-continuation
def test_version_check(monkeypatch, qversion, compiled, pyqt, version, exact,
@@ -936,3 +939,107 @@ class TestEventLoop:
QTimer.singleShot(400, self.loop.quit)
self.loop.exec_()
assert not self.loop._executing
+
+
+class Color(QColor):
+
+ """A QColor with a nicer repr()."""
+
+ def __repr__(self):
+ return utils.get_repr(self, constructor=True, red=self.red(),
+ green=self.green(), blue=self.blue(),
+ alpha=self.alpha())
+
+
+class TestInterpolateColor:
+
+ @attr.s
+ class Colors:
+
+ white = attr.ib()
+ black = attr.ib()
+
+ @pytest.fixture
+ def colors(self):
+ """Example colors to be used."""
+ return self.Colors(Color('white'), Color('black'))
+
+ def test_invalid_start(self, colors):
+ """Test an invalid start color."""
+ with pytest.raises(qtutils.QtValueError):
+ qtutils.interpolate_color(Color(), colors.white, 0)
+
+ def test_invalid_end(self, colors):
+ """Test an invalid end color."""
+ with pytest.raises(qtutils.QtValueError):
+ qtutils.interpolate_color(colors.white, Color(), 0)
+
+ @pytest.mark.parametrize('perc', [-1, 101])
+ def test_invalid_percentage(self, colors, perc):
+ """Test an invalid percentage."""
+ with pytest.raises(ValueError):
+ qtutils.interpolate_color(colors.white, colors.white, perc)
+
+ def test_invalid_colorspace(self, colors):
+ """Test an invalid colorspace."""
+ with pytest.raises(ValueError):
+ qtutils.interpolate_color(colors.white, colors.black, 10, QColor.Cmyk)
+
+ @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv,
+ QColor.Hsl])
+ def test_0_100(self, colors, colorspace):
+ """Test 0% and 100% in different colorspaces."""
+ white = qtutils.interpolate_color(colors.white, colors.black, 0, colorspace)
+ black = qtutils.interpolate_color(colors.white, colors.black, 100, colorspace)
+ assert Color(white) == colors.white
+ assert Color(black) == colors.black
+
+ def test_interpolation_rgb(self):
+ """Test an interpolation in the RGB colorspace."""
+ color = qtutils.interpolate_color(
+ Color(0, 40, 100), Color(0, 20, 200), 50, QColor.Rgb)
+ assert Color(color) == Color(0, 30, 150)
+
+ def test_interpolation_hsv(self):
+ """Test an interpolation in the HSV colorspace."""
+ start = Color()
+ stop = Color()
+ start.setHsv(0, 40, 100)
+ stop.setHsv(0, 20, 200)
+ color = qtutils.interpolate_color(start, stop, 50, QColor.Hsv)
+ expected = Color()
+ expected.setHsv(0, 30, 150)
+ assert Color(color) == expected
+
+ def test_interpolation_hsl(self):
+ """Test an interpolation in the HSL colorspace."""
+ start = Color()
+ stop = Color()
+ start.setHsl(0, 40, 100)
+ stop.setHsl(0, 20, 200)
+ color = qtutils.interpolate_color(start, stop, 50, QColor.Hsl)
+ expected = Color()
+ expected.setHsl(0, 30, 150)
+ assert Color(color) == expected
+
+ @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv,
+ QColor.Hsl])
+ def test_interpolation_alpha(self, colorspace):
+ """Test interpolation of colorspace's alpha."""
+ start = Color(0, 0, 0, 30)
+ stop = Color(0, 0, 0, 100)
+ color = qtutils.interpolate_color(start, stop, 50, colorspace)
+ expected = Color(0, 0, 0, 65)
+ assert Color(color) == expected
+
+ @pytest.mark.parametrize('percentage, expected', [
+ (0, (0, 0, 0)),
+ (99, (0, 0, 0)),
+ (100, (255, 255, 255)),
+ ])
+ def test_interpolation_none(self, percentage, expected):
+ """Test an interpolation with a gradient turned off."""
+ color = qtutils.interpolate_color(
+ Color(0, 0, 0), Color(255, 255, 255), percentage, None)
+ assert isinstance(color, QColor)
+ assert Color(color) == Color(*expected)
diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py
index eac2b6c1d..0167f6cee 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -615,7 +615,7 @@ class TestInvalidUrlError:
@pytest.mark.parametrize('are_same, url1, url2', [
(True, 'http://example.com', 'http://www.example.com'),
- (True, 'http://bbc.co.uk', 'https://www.bbc.co.uk'),
+ (True, 'http://bbc.co.uk', 'http://www.bbc.co.uk'),
(True, 'http://many.levels.of.domains.example.com', 'http://www.example.com'),
(True, 'http://idn.иком.museum', 'http://idn2.иком.museum'),
(True, 'http://one.not_a_valid_tld', 'http://one.not_a_valid_tld'),
@@ -624,6 +624,9 @@ class TestInvalidUrlError:
(False, 'https://example.kids.museum', 'http://example.kunst.museum'),
(False, 'http://idn.иком.museum', 'http://idn.ירושלים.museum'),
(False, 'http://one.not_a_valid_tld', 'http://two.not_a_valid_tld'),
+
+ (False, 'http://example.org', 'https://example.org'), # different scheme
+ (False, 'http://example.org:80', 'http://example.org:8080'), # different port
])
def test_same_domain(are_same, url1, url2):
"""Test same_domain."""
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
index 0c39ad183..ac7ed5ce7 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -29,10 +29,8 @@ import re
import shlex
import math
-import pkg_resources
-import attr
from PyQt5.QtCore import QUrl
-from PyQt5.QtGui import QColor, QClipboard
+from PyQt5.QtGui import QClipboard
import pytest
import hypothesis
from hypothesis import strategies
@@ -40,22 +38,12 @@ import yaml
import qutebrowser
import qutebrowser.utils # for test_qualname
-from qutebrowser.utils import utils, qtutils, version, usertypes
+from qutebrowser.utils import utils, version, usertypes
ELLIPSIS = '\u2026'
-class Color(QColor):
-
- """A QColor with a nicer repr()."""
-
- def __repr__(self):
- return utils.get_repr(self, constructor=True, red=self.red(),
- green=self.green(), blue=self.blue(),
- alpha=self.alpha())
-
-
class TestCompactText:
"""Test compact_text."""
@@ -160,110 +148,6 @@ def test_resource_filename():
assert f.read().splitlines()[0] == "Hello World!"
-class TestInterpolateColor:
-
- """Tests for interpolate_color.
-
- Attributes:
- white: The Color white as a valid Color for tests.
- white: The Color black as a valid Color for tests.
- """
-
- @attr.s
- class Colors:
-
- white = attr.ib()
- black = attr.ib()
-
- @pytest.fixture
- def colors(self):
- """Example colors to be used."""
- return self.Colors(Color('white'), Color('black'))
-
- def test_invalid_start(self, colors):
- """Test an invalid start color."""
- with pytest.raises(qtutils.QtValueError):
- utils.interpolate_color(Color(), colors.white, 0)
-
- def test_invalid_end(self, colors):
- """Test an invalid end color."""
- with pytest.raises(qtutils.QtValueError):
- utils.interpolate_color(colors.white, Color(), 0)
-
- @pytest.mark.parametrize('perc', [-1, 101])
- def test_invalid_percentage(self, colors, perc):
- """Test an invalid percentage."""
- with pytest.raises(ValueError):
- utils.interpolate_color(colors.white, colors.white, perc)
-
- def test_invalid_colorspace(self, colors):
- """Test an invalid colorspace."""
- with pytest.raises(ValueError):
- utils.interpolate_color(colors.white, colors.black, 10,
- QColor.Cmyk)
-
- @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv,
- QColor.Hsl])
- def test_0_100(self, colors, colorspace):
- """Test 0% and 100% in different colorspaces."""
- white = utils.interpolate_color(colors.white, colors.black, 0,
- colorspace)
- black = utils.interpolate_color(colors.white, colors.black, 100,
- colorspace)
- assert Color(white) == colors.white
- assert Color(black) == colors.black
-
- def test_interpolation_rgb(self):
- """Test an interpolation in the RGB colorspace."""
- color = utils.interpolate_color(Color(0, 40, 100), Color(0, 20, 200),
- 50, QColor.Rgb)
- assert Color(color) == Color(0, 30, 150)
-
- def test_interpolation_hsv(self):
- """Test an interpolation in the HSV colorspace."""
- start = Color()
- stop = Color()
- start.setHsv(0, 40, 100)
- stop.setHsv(0, 20, 200)
- color = utils.interpolate_color(start, stop, 50, QColor.Hsv)
- expected = Color()
- expected.setHsv(0, 30, 150)
- assert Color(color) == expected
-
- def test_interpolation_hsl(self):
- """Test an interpolation in the HSL colorspace."""
- start = Color()
- stop = Color()
- start.setHsl(0, 40, 100)
- stop.setHsl(0, 20, 200)
- color = utils.interpolate_color(start, stop, 50, QColor.Hsl)
- expected = Color()
- expected.setHsl(0, 30, 150)
- assert Color(color) == expected
-
- @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv,
- QColor.Hsl])
- def test_interpolation_alpha(self, colorspace):
- """Test interpolation of colorspace's alpha."""
- start = Color(0, 0, 0, 30)
- stop = Color(0, 0, 0, 100)
- color = utils.interpolate_color(start, stop, 50, colorspace)
- expected = Color(0, 0, 0, 65)
- assert Color(color) == expected
-
- @pytest.mark.parametrize('percentage, expected', [
- (0, (0, 0, 0)),
- (99, (0, 0, 0)),
- (100, (255, 255, 255)),
- ])
- def test_interpolation_none(self, percentage, expected):
- """Test an interpolation with a gradient turned off."""
- color = utils.interpolate_color(Color(0, 0, 0), Color(255, 255, 255),
- percentage, None)
- assert isinstance(color, QColor)
- assert Color(color) == Color(*expected)
-
-
@pytest.mark.parametrize('seconds, out', [
(-1, '-0:01'),
(0, '0:00'),
@@ -807,7 +691,7 @@ class TestOpenFile:
info = version.DistributionInfo(
id='org.kde.Platform',
parsed=version.Distribution.kde_flatpak,
- version=pkg_resources.parse_version('5.12'),
+ version=utils.parse_version('5.12'),
pretty='Unknown')
monkeypatch.setattr(version, 'distribution',
lambda: info)
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 2bfdf10d7..593557ae8 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -33,7 +33,6 @@ import textwrap
import datetime
import attr
-import pkg_resources
import pytest
import hypothesis
import hypothesis.strategies
@@ -77,7 +76,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='ubuntu', parsed=version.Distribution.ubuntu,
- version=pkg_resources.parse_version('14.4'),
+ version=utils.parse_version('14.4'),
pretty='Ubuntu 14.04.5 LTS')),
# Ubuntu 17.04
("""
@@ -90,7 +89,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='ubuntu', parsed=version.Distribution.ubuntu,
- version=pkg_resources.parse_version('17.4'),
+ version=utils.parse_version('17.4'),
pretty='Ubuntu 17.04')),
# Debian Jessie
("""
@@ -102,7 +101,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='debian', parsed=version.Distribution.debian,
- version=pkg_resources.parse_version('8'),
+ version=utils.parse_version('8'),
pretty='Debian GNU/Linux 8 (jessie)')),
# Void Linux
("""
@@ -133,7 +132,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='fedora', parsed=version.Distribution.fedora,
- version=pkg_resources.parse_version('25'),
+ version=utils.parse_version('25'),
pretty='Fedora 25 (Twenty Five)')),
# OpenSUSE
("""
@@ -146,7 +145,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='opensuse', parsed=version.Distribution.opensuse,
- version=pkg_resources.parse_version('42.2'),
+ version=utils.parse_version('42.2'),
pretty='openSUSE Leap 42.2')),
# Linux Mint
("""
@@ -159,7 +158,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='linuxmint', parsed=version.Distribution.linuxmint,
- version=pkg_resources.parse_version('18.1'),
+ version=utils.parse_version('18.1'),
pretty='Linux Mint 18.1')),
# Manjaro
("""
@@ -188,7 +187,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='org.kde.Platform', parsed=version.Distribution.kde_flatpak,
- version=pkg_resources.parse_version('5.12'),
+ version=utils.parse_version('5.12'),
pretty='KDE')),
# No PRETTY_NAME
("""
@@ -221,7 +220,7 @@ def test_distribution(tmpdir, monkeypatch, os_release, expected):
(None, False),
(version.DistributionInfo(
id='org.kde.Platform', parsed=version.Distribution.kde_flatpak,
- version=pkg_resources.parse_version('5.12'),
+ version=utils.parse_version('5.12'),
pretty='Unknown'), True),
(version.DistributionInfo(
id='arch', parsed=version.Distribution.arch, version=None,
diff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py
index 014ef7f0c..59c3f7d43 100644
--- a/tests/unit/utils/usertypes/test_question.py
+++ b/tests/unit/utils/usertypes/test_question.py
@@ -53,25 +53,23 @@ def test_done(mode, answer, signal_names, question, qtbot):
question.mode = mode
question.answer = answer
signals = [getattr(question, name) for name in signal_names]
- blockers = [qtbot.waitSignal(signal) for signal in signals]
-
- question.done()
- for blocker in blockers:
- blocker.wait()
-
+ with qtbot.waitSignals(signals, order='strict'):
+ question.done()
assert not question.is_aborted
def test_cancel(question, qtbot):
"""Test Question.cancel()."""
- with qtbot.waitSignal(question.cancelled), qtbot.waitSignal(question.completed):
+ with qtbot.waitSignals([question.cancelled, question.completed],
+ order='strict'):
question.cancel()
assert not question.is_aborted
def test_abort(question, qtbot):
"""Test Question.abort()."""
- with qtbot.waitSignal(question.aborted), qtbot.waitSignal(question.completed):
+ with qtbot.waitSignals([question.aborted, question.completed],
+ order='strict'):
question.abort()
assert question.is_aborted
diff --git a/tox.ini b/tox.ini
index 8d9eedeac..abdce0b5b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,17 +12,17 @@ minversion = 3.15
[testenv]
setenv =
PYTEST_QT_API=pyqt5
- pyqt{,512,513,514,515}: LINK_PYQT_SKIP=true
- pyqt{,512,513,514,515}: QUTE_BDD_WEBENGINE=true
+ pyqt{,512,513,514,515,5150}: LINK_PYQT_SKIP=true
+ pyqt{,512,513,514,515,5150}: QUTE_BDD_WEBENGINE=true
cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report=
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS
basepython =
+ py: {env:PYTHON:python3}
py3: {env:PYTHON:python3}
py36: {env:PYTHON:python3.6}
py37: {env:PYTHON:python3.7}
py38: {env:PYTHON:python3.8}
py39: {env:PYTHON:python3.9}
-pip_version = pip
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-tests.txt
@@ -31,6 +31,7 @@ deps =
pyqt513: -r{toxinidir}/misc/requirements/requirements-pyqt-5.13.txt
pyqt514: -r{toxinidir}/misc/requirements/requirements-pyqt-5.14.txt
pyqt515: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.txt
+ pyqt5150: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.0.txt
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -bb -m pytest {posargs:tests}
@@ -41,16 +42,14 @@ commands =
[testenv:misc]
ignore_errors = true
basepython = {env:PYTHON:python3}
-pip_version = pip
# For global .gitignore files
passenv = HOME
deps =
commands =
- {envpython} scripts/dev/misc_checks.py all
+ {envpython} scripts/dev/misc_checks.py {posargs:all}
[testenv:vulture]
basepython = {env:PYTHON:python3}
-pip_version = pip
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-vulture.txt
@@ -61,7 +60,6 @@ commands =
[testenv:vulture-pyqtlink]
basepython = {env:PYTHON:python3}
-pip_version = pip
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-vulture.txt
@@ -72,7 +70,6 @@ commands =
[testenv:pylint]
basepython = {env:PYTHON:python3}
-pip_version = pip
ignore_errors = true
passenv =
deps =
@@ -86,7 +83,6 @@ commands =
[testenv:pylint-pyqtlink]
basepython = {env:PYTHON:python3}
-pip_version = pip
ignore_errors = true
passenv =
deps =
@@ -100,7 +96,6 @@ commands =
[testenv:pylint-master]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = {[testenv:pylint]passenv}
deps =
-r{toxinidir}/requirements.txt
@@ -113,7 +108,6 @@ commands =
[testenv:flake8]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv =
deps =
-r{toxinidir}/requirements.txt
@@ -123,7 +117,6 @@ commands =
[testenv:pyroma]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv =
deps =
-r{toxinidir}/misc/requirements/requirements-pyroma.txt
@@ -132,7 +125,6 @@ commands =
[testenv:check-manifest]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv =
deps =
-r{toxinidir}/misc/requirements/requirements-check-manifest.txt
@@ -141,7 +133,6 @@ commands =
[testenv:docs]
basepython = {env:PYTHON:python3}
-pip_version = pip
whitelist_externals = git
passenv = CI GITHUB_REF
deps =
@@ -154,7 +145,6 @@ commands =
[testenv:pyinstaller-{64,32}]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = APPDATA HOME PYINSTALLER_DEBUG
deps =
-r{toxinidir}/requirements.txt
@@ -179,7 +169,6 @@ commands = bash scripts/dev/run_shellcheck.sh {posargs}
[testenv:mypy]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = TERM MYPY_FORCE_TERMINAL_WIDTH
deps =
-r{toxinidir}/requirements.txt
@@ -191,14 +180,12 @@ commands =
[testenv:yamllint]
basepython = {env:PYTHON:python3}
-pip_version = pip
deps = -r{toxinidir}/misc/requirements/requirements-yamllint.txt
commands =
{envpython} -m yamllint -f colored --strict . {posargs}
[testenv:mypy-diff]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = {[testenv:mypy]passenv}
deps = {[testenv:mypy]deps}
commands =
@@ -207,7 +194,6 @@ commands =
[testenv:sphinx]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv =
usedevelop = true
deps =
@@ -219,7 +205,6 @@ commands =
[testenv:build-release]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = *
usedevelop = true
deps =