summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÁrni Dagur <arni@dagur.eu>2020-12-19 20:29:21 +0000
committerÁrni Dagur <arni@dagur.eu>2020-12-19 20:29:21 +0000
commitbf4dbef8063a079f594d8321726b3608ebd2fe26 (patch)
tree293ba450b24d631d12bb3d743d655cc8d631ec04
parent4204fb5917d65d7f89ade67897f9d03afd1e91c8 (diff)
parent1642fa252ae9427eef3acdba3ce45782bad608e2 (diff)
downloadqutebrowser-bf4dbef8063a079f594d8321726b3608ebd2fe26.tar.gz
qutebrowser-bf4dbef8063a079f594d8321726b3608ebd2fe26.zip
Merge branch 'master' into more-sophisticated-adblock
-rw-r--r--doc/changelog.asciidoc30
-rw-r--r--doc/faq.asciidoc2
-rw-r--r--doc/help/commands.asciidoc11
-rw-r--r--doc/help/configuring.asciidoc1
-rw-r--r--doc/help/settings.asciidoc2
-rw-r--r--doc/stacktrace.asciidoc24
-rw-r--r--misc/Makefile2
-rwxr-xr-xmisc/nsis/qutebrowser.nsi1
-rw-r--r--misc/requirements/requirements-dev.txt8
-rw-r--r--misc/requirements/requirements-flake8.txt4
-rw-r--r--misc/requirements/requirements-mypy.txt5
-rw-r--r--misc/requirements/requirements-pyinstaller.txt2
-rw-r--r--misc/requirements/requirements-pylint.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.12.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.13.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.14.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.15.txt6
-rw-r--r--misc/requirements/requirements-pyqt.txt6
-rw-r--r--misc/requirements/requirements-pyroma.txt2
-rw-r--r--misc/requirements/requirements-sphinx.txt2
-rw-r--r--misc/requirements/requirements-tests.txt27
-rw-r--r--misc/requirements/requirements-tests.txt-raw5
-rw-r--r--misc/requirements/requirements-tox.txt2
-rw-r--r--misc/userscripts/README.md2
-rwxr-xr-xmisc/userscripts/cast2
-rwxr-xr-xmisc/userscripts/qute-bitwarden2
-rwxr-xr-xmisc/userscripts/qute-pass54
-rwxr-xr-xmisc/userscripts/readability-js10
-rw-r--r--pytest.ini2
-rw-r--r--qutebrowser/browser/browsertab.py6
-rw-r--r--qutebrowser/browser/commands.py15
-rw-r--r--qutebrowser/browser/navigate.py11
-rw-r--r--qutebrowser/browser/webelem.py4
-rw-r--r--qutebrowser/browser/webengine/webengineelem.py4
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py22
-rw-r--r--qutebrowser/browser/webkit/webkitelem.py4
-rw-r--r--qutebrowser/browser/webkit/webview.py4
-rw-r--r--qutebrowser/completion/models/miscmodels.py4
-rw-r--r--qutebrowser/config/configdata.yml1
-rw-r--r--qutebrowser/config/configfiles.py4
-rw-r--r--qutebrowser/html/warning-sessions.html2
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py27
-rw-r--r--qutebrowser/mainwindow/tabwidget.py27
-rw-r--r--qutebrowser/misc/crashdialog.py2
-rw-r--r--qutebrowser/misc/sessions.py3
-rw-r--r--qutebrowser/utils/urlutils.py8
-rw-r--r--requirements.txt4
-rwxr-xr-xscripts/asciidoc2html.py21
-rwxr-xr-xscripts/dev/build_release.py19
-rw-r--r--scripts/dev/misc_checks.py115
-rw-r--r--scripts/dev/recompile_requirements.py19
-rw-r--r--scripts/dev/update_version.py9
-rw-r--r--tests/end2end/features/tabs.feature9
-rw-r--r--tests/manual/hints/hide_unmatched_rapid_hints.html2
-rw-r--r--tests/unit/browser/webkit/network/test_networkreply.py7
-rw-r--r--tests/unit/completion/test_completionmodel.py4
-rw-r--r--tests/unit/completion/test_models.py45
-rw-r--r--tests/unit/config/test_config.py7
-rw-r--r--tests/unit/config/test_configfiles.py20
-rw-r--r--tests/unit/keyinput/test_basekeyparser.py4
-rw-r--r--tests/unit/mainwindow/test_tabwidget.py5
-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_jinja.py2
-rw-r--r--tests/unit/utils/test_urlmatch.py4
-rw-r--r--tests/unit/utils/test_urlutils.py2
-rw-r--r--tests/unit/utils/usertypes/test_question.py14
-rw-r--r--tox.ini14
68 files changed, 474 insertions, 259 deletions
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 17635fd50..c29595d9e 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -36,8 +36,7 @@ Changed
partially transparent qutebrowser window on a setup which supports doing so.
- If QtWebEngine is compiled with PipeWire support and libpipewire is
installed, qutebrowser will now support screen sharing on Wayland. Note that
- QtWebEngine 5.15.1 (planned for August 2020) is needed, though the Archlinux
- qt5-webengine package backports the patch.
+ QtWebEngine 5.15.1 is needed.
- When `:undo` is used with a count, it now reopens the count-th to last tab
instead of the last one. The depth can instead be passed as an argument,
which is also completed.
@@ -52,6 +51,23 @@ Changed
- `:completion-item-focus` now understands `next-page` and `prev-page` with
corresponding `<PgDown>` / `<PgUp>` default bindings.
- When the last private window is closed, all private browsing data is now cleared.
+- When `config.source(...)` is used with a `--config-py` argument given,
+ qutebrowser used to search relative files in the config basedir, leading to them
+ not being found when using a shared `config.py` for different basedirs. Instead,
+ they are now searched relative to the given `config.py` file.
+- `navigate prev` (`[[`) and `navigate next` (`]]`) now recognize links with
+ `nav-prev` and `nav-next` classes, such as those used by the Hugo static site
+ generator.
+- When `tabs.favicons` is disabled but `tabs.tabs_are_windows` is set, the
+ window icon is still set to the page's favicon now.
+- The `--asciidoc` argument to `src2asciidoc.py` and `build_release.py` now
+ only takes the path to `asciidoc.py`, using the current Python interpreter by
+ default. To configure the Python interpreter as well, use
+ `--asciidoc-python path/to/python --asciidoc path/to/asciidoc.py`
+ instead of the former
+ `--asciidoc path/to/python path/to/asciidoc.py`.
+- The `readability-js` userscript now adds some CSS to better deal
+ with images, similarly to what Firefox' reader mode does.
Added
~~~~~
@@ -113,6 +129,14 @@ Fixed
- Highlighting in the completion now works properly when UTF-16 surrogate pairs (such as
emoji) are involved.
- When a windowed inspector is clicked, insert mode now isn't entered anymore.
+- When `:undo` to re-open a tab but `tabs.tabs_are_windows` was set between
+ closing and undoing the close, qutebrowser crashed. This is now fixed.
+- Fixes for the `qute-pass` userscript:
+ * With newer `gopass` versions, a deprecation notice was copied as
+ password due to `qute-pass` using it in a deprecated way.
+ * The `--password-store` argument didn't actually set
+ `PASSWORD_STORE_DIR` for `pass`, resulting in `qute-pass` finding matches but the
+ underlying `pass` not finding matching passwords. This is now fixed.
v1.13.1 (2020-07-17)
--------------------
@@ -1128,7 +1152,7 @@ Fixed
- `qute://` pages now work properly on Qt 5.11.2
- Error when passing a substring with spaces to `:tab-take`.
-- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly.
+- Greasemonkey scripts which start with a UTF-8 BOM are now handled correctly.
- When no documentation has been generated, the plaintext documentation now can
be shown for more files such as `qute://help/userscripts.html`.
- Crash when doing initial run on Wayland without XWayland.
diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc
index e0b102683..be946fb83 100644
--- a/doc/faq.asciidoc
+++ b/doc/faq.asciidoc
@@ -226,7 +226,7 @@ Why does it take longer to open a URL in qutebrowser than in chromium?::
One workaround is to use this
https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script]
and place it in your $PATH with the name "qutebrowser". This
- script passes the URL via an unix socket to qutebrowser (if its
+ script passes the URL via a unix socket to qutebrowser (if its
running already) using socat which is much faster and starts a new
qutebrowser if it is not running already.
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index 299b34b5e..5dd6c7718 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -763,7 +763,16 @@ Evaluate a JavaScript string.
* +*-u*+, +*--url*+: Interpret js-code as a `javascript:...` URL.
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
-* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in.
+* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in. Predefined world names are:
+
+
+ - `main` (same world as the web page's JavaScript and
+ Greasemonkey, unless overridden via `@qute-js-world`)
+ - `application` (used for internal qutebrowser JS code,
+ should not be used via `:jseval` unless you know what
+ you're doing)
+ - `user` (currently unused)
+ - `jseval` (used for this command by default)
==== note
diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc
index 90b7ed65b..4f1d872f7 100644
--- a/doc/help/configuring.asciidoc
+++ b/doc/help/configuring.asciidoc
@@ -398,6 +398,7 @@ Pre-built colorschemes
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
- https://github.com/dracula/qutebrowser-dracula-theme[Dracula]
- https://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized]
+- https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[gruvbox]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index d4de80d06..035c7881d 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -1737,6 +1737,8 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.prefers_color_scheme_dark
Force `prefers-color-scheme: dark` colors for websites.
+This setting requires a restart.
+
Type: <<types,Bool>>
Default: +pass:[false]+
diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc
index 9bda4ab87..f295a5305 100644
--- a/doc/stacktrace.asciidoc
+++ b/doc/stacktrace.asciidoc
@@ -28,10 +28,20 @@ Debian/Ubuntu/...
^^^^^^^^^^^^^^^^^
For Debian based systems (Debian, Ubuntu, Linux Mint, ...), debug information
-is available in the repositories:
+is for QtWebEngine is available in a dedicated repository. Enable that repository
+(https://wiki.debian.org/HowToGetABacktrace#Installing_the_debugging_symbols[Debian],
+https://wiki.ubuntu.com/Debug%20Symbol%20Packages[Ubuntu],
+https://www.linuxmint.com/rel_tessa_mate_whatsnew.php[Linux Mint]) and install
+the debug packages:
----
-# apt-get install python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg python3-dbg libqt5webkit5-dbg
+# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebengine-dbg libqt5webengine5-dbgsym
+----
+
+or with the QtWebKit backend:
+
+----
+# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg libqt5webkit5-dbg
----
Fedora
@@ -116,7 +126,7 @@ First install `gdb` on your system if it's not installed already.
Then run qutebrowser directly inside gdb like this:
----
-$ gdb $(readlink -f $(which python3)) -ex 'run -m qutebrowser --debug'
+$ gdb -ex r --args $(readlink -f $(which python3)) -m qutebrowser --debug --temp-basedir
----
Note qutebrowser/gdb will take a long time to start. After you reproduce the
@@ -131,9 +141,10 @@ Program received signal SIGSEGV, Segmentation fault.
Now enter these commands at the gdb prompt:
----
+(gdb) set pagination off
+(gdb) set logging overwrite on
(gdb) set logging on
-(gdb) bt full
-# you might have to press enter a few times until you get the prompt back
+(gdb) bt
(gdb) quit
----
@@ -176,9 +187,10 @@ Getting the stack trace
Now enter these commands at the gdb prompt:
----
+(gdb) set pagination off
+(gdb) set logging overwrite on
(gdb) set logging on
(gdb) bt
-# you might have to press enter a few times until you get the prompt back
(gdb) quit
----
diff --git a/misc/Makefile b/misc/Makefile
index 985541fdd..b916a20d5 100644
--- a/misc/Makefile
+++ b/misc/Makefile
@@ -29,7 +29,7 @@ install: man
install -Dm644 icons/qutebrowser.svg \
"$(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps/qutebrowser.svg"
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/userscripts/" \
- $(wildcard misc/userscripts/*)
+ $(filter-out misc/userscripts/__pycache__,$(wildcard misc/userscripts/*))
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/scripts/" \
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
diff --git a/misc/nsis/qutebrowser.nsi b/misc/nsis/qutebrowser.nsi
index d9b8fbf8d..77fd373eb 100755
--- a/misc/nsis/qutebrowser.nsi
+++ b/misc/nsis/qutebrowser.nsi
@@ -1,4 +1,5 @@
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+# encoding: iso-8859-1
#
# This file is part of qutebrowser.
#
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index c543088c6..798fecad6 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -5,10 +5,10 @@ certifi==2020.6.20
cffi==1.14.2
chardet==3.0.4
colorama==0.4.3
-cryptography==3.0
+cryptography==3.1
cssutils==1.0.2
github3.py==1.3.0
-hunter==3.2.1
+hunter==3.2.2
idna==2.10
jwcrypto==0.8
manhole==1.6.0
@@ -16,10 +16,10 @@ packaging==20.4
pycparser==2.20
Pympler==0.8
pyparsing==2.4.7
-PyQt-builder==1.4.0
+PyQt-builder==1.5.0
python-dateutil==2.8.1
requests==2.24.0
-sip==5.3.0
+sip==5.4.0
six==1.15.0
toml==0.10.1
uritemplate==3.0.1
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index 00d22b236..afd4f0bd6 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==20.1.0
+attrs==20.2.0
flake8==3.8.3
flake8-bugbear==20.1.4
flake8-builtins==1.5.3
@@ -18,7 +18,7 @@ flake8-tuple==0.4.1
mccabe==0.6.1
pep8-naming==0.11.1
pycodestyle==2.6.0
-pydocstyle==5.1.0
+pydocstyle==5.1.1
pyflakes==2.2.0
six==1.15.0
snowballstemmer==2.0.0
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index 09d11ea6c..863f48c6f 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-diff-cover==3.0.1
+diff-cover==4.0.0
inflect==4.1.0
Jinja2==2.11.2
jinja2-pluralize==0.3.0
@@ -9,8 +9,7 @@ MarkupSafe==1.1.1
mypy==0.782
mypy-extensions==0.4.3
pluggy==0.13.1
-Pygments==2.6.1
+Pygments==2.7.1
-e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5_stubs
-six==1.15.0
typed-ast==1.4.1
typing-extensions==3.7.4.3
diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt
index 6de7b6fa8..06405d96a 100644
--- a/misc/requirements/requirements-pyinstaller.txt
+++ b/misc/requirements/requirements-pyinstaller.txt
@@ -2,4 +2,4 @@
altgraph==0.17
pyinstaller==4.0
-pyinstaller-hooks-contrib==2020.7
+pyinstaller-hooks-contrib==2020.8
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index 3c7440627..08c1d2c10 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -4,7 +4,7 @@ astroid==2.3.3 # rq.filter: < 2.4
certifi==2020.6.20
cffi==1.14.2
chardet==3.0.4
-cryptography==3.0
+cryptography==3.1
github3.py==1.3.0
idna==2.10
isort==4.3.21
diff --git a/misc/requirements/requirements-pyqt-5.12.txt b/misc/requirements/requirements-pyqt-5.12.txt
index 9b458cd98..80a700f09 100644
--- a/misc/requirements/requirements-pyqt-5.12.txt
+++ b/misc/requirements/requirements-pyqt-5.12.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.12.3 # rq.filter: < 5.13
-PyQt5-sip==12.8.0
+PyQt5-sip==12.8.1
PyQtWebEngine==5.12.1 # rq.filter: < 5.13
diff --git a/misc/requirements/requirements-pyqt-5.13.txt b/misc/requirements/requirements-pyqt-5.13.txt
index 7c07eac3d..438c600da 100644
--- a/misc/requirements/requirements-pyqt-5.13.txt
+++ b/misc/requirements/requirements-pyqt-5.13.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.13.2 # rq.filter: < 5.14
-PyQt5-sip==12.8.0
+PyQt5-sip==12.8.1
PyQtWebEngine==5.13.2 # rq.filter: < 5.14
diff --git a/misc/requirements/requirements-pyqt-5.14.txt b/misc/requirements/requirements-pyqt-5.14.txt
index c82acedb0..d515e717f 100644
--- a/misc/requirements/requirements-pyqt-5.14.txt
+++ b/misc/requirements/requirements-pyqt-5.14.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.14.2 # rq.filter: < 5.15
-PyQt5-sip==12.8.0
+PyQt5-sip==12.8.1
PyQtWebEngine==5.14.0 # rq.filter: < 5.15
diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt
index c21b7b742..21745c814 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.0 # rq.filter: < 6
-PyQt5-sip==12.8.0
-PyQtWebEngine==5.15.0 # rq.filter: < 6
+PyQt5==5.15.1 # rq.filter: < 6
+PyQt5-sip==12.8.1
+PyQtWebEngine==5.15.1 # rq.filter: < 6
diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt
index 74d86e8d5..88a04230d 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.0
-PyQt5-sip==12.8.0
-PyQtWebEngine==5.15.0
+PyQt5==5.15.1
+PyQt5-sip==12.8.1
+PyQtWebEngine==5.15.1
diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt
index 6b131e155..c6e751e19 100644
--- a/misc/requirements/requirements-pyroma.txt
+++ b/misc/requirements/requirements-pyroma.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.16
-Pygments==2.6.1
+Pygments==2.7.1
pyroma==2.6
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index da6447009..7b43a72a1 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -10,7 +10,7 @@ imagesize==1.2.0
Jinja2==2.11.2
MarkupSafe==1.1.1
packaging==20.4
-Pygments==2.6.1
+Pygments==2.7.1
pyparsing==2.4.7
pytz==2020.1
requests==2.24.0
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index a82ba796c..789c176e6 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -1,18 +1,20 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==20.1.0
+apipkg==1.5
+attrs==20.2.0
beautifulsoup4==4.9.1
certifi==2020.6.20
chardet==3.0.4
cheroot==8.4.5
click==7.1.2
# colorama==0.4.3
-coverage==5.2.1
+coverage==5.3
EasyProcess==0.3
+execnet==1.7.1
Flask==1.1.2
glob2==0.7
-hunter==3.2.1
-hypothesis==5.29.0
+hunter==3.2.2
+hypothesis==5.35.3 ; python_version>="3.6"
idna==2.10
iniconfig==1.0.1
itsdangerous==1.1.0
@@ -21,24 +23,26 @@ jaraco.functools==3.0.1 ; python_version>="3.6"
Mako==1.1.3
manhole==1.6.0
# MarkupSafe==1.1.1
-more-itertools==8.4.0
+more-itertools==8.5.0
packaging==20.4
-parse==1.16.0
+parse==1.18.0
parse-type==0.5.2
pluggy==0.13.1
py==1.9.0
py-cpuinfo==7.0.0
-Pygments==2.6.1
+Pygments==2.7.1
pyparsing==2.4.7
-pytest==6.0.1
-pytest-bdd==3.4.0
+pytest==6.0.2
+pytest-bdd==4.0.1
pytest-benchmark==3.2.3
pytest-cov==2.10.1
+pytest-forked==1.3.0
pytest-instafail==0.4.2
-pytest-mock==3.3.0
+pytest-mock==3.3.1
pytest-qt==3.3.0
pytest-repeat==0.8.0
-pytest-rerunfailures==9.0
+pytest-rerunfailures==9.1
+pytest-xdist==2.1.0
pytest-xvfb==2.0.0
PyVirtualDisplay==1.3.2
requests==2.24.0
@@ -53,3 +57,4 @@ vulture==2.1 ; python_version>="3.6"
Werkzeug==1.0.1
jaraco.functools==2.0; python_version<"3.6"
vulture==1.6; python_version<"3.6"
+hypothesis<5.34.0; python_version<"3.6"
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index f063a3512..73f58461c 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -25,6 +25,8 @@ pytest-cov
# To avoid windows from popping up
pytest-xvfb
PyVirtualDisplay
+# To run on multiple cores with -n
+pytest-xdist
# Needed to test misc/userscripts/qute-lastpass
tldextract
@@ -35,4 +37,7 @@ tldextract
#@ markers: vulture python_version>="3.6"
#@ add: vulture==1.6; python_version<"3.6"
+#@ markers: hypothesis python_version>="3.6"
+#@ add: hypothesis<5.34.0; python_version<"3.6"
+
#@ ignore: Jinja2, MarkupSafe, colorama
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 3fb7595ad..c7f99a2da 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -9,7 +9,7 @@ py==1.9.0
pyparsing==2.4.7
six==1.15.0
toml==0.10.1
-tox==3.19.0
+tox==3.20.0
tox-pip-version==0.0.7
tox-venv==0.4.0
virtualenv==20.0.31
diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md
index 729e63f6e..d2519a672 100644
--- a/misc/userscripts/README.md
+++ b/misc/userscripts/README.md
@@ -40,7 +40,7 @@ The following userscripts are included in the current directory.
The following userscripts can be found on their own repositories.
-- [qurlshare](https://github.com/sim590/qurlshare): *secure* sharing of an URL between qutebrowser
+- [qurlshare](https://github.com/sim590/qurlshare): *secure* sharing of a URL between qutebrowser
instances using a distributed hash table.
- [qutebrowser-userscripts](https://github.com/cryzed/qutebrowser-userscripts):
a small pack of userscripts.
diff --git a/misc/userscripts/cast b/misc/userscripts/cast
index f7b64df70..8bbf05a40 100755
--- a/misc/userscripts/cast
+++ b/misc/userscripts/cast
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
-# Behaviour
+# Behavior
# Userscript for qutebrowser which casts the url passed in $1 to the default
# ChromeCast device in the network using the program `castnow`
#
diff --git a/misc/userscripts/qute-bitwarden b/misc/userscripts/qute-bitwarden
index d5c4b1e2d..ca9d646e4 100755
--- a/misc/userscripts/qute-bitwarden
+++ b/misc/userscripts/qute-bitwarden
@@ -281,7 +281,7 @@ def main(arguments):
qute_command('enter-mode insert')
# If it finds a TOTP code, it copies it to the clipboard,
- # which is the same behaviour as the Firefox add-on.
+ # which is the same behavior as the Firefox add-on.
if not arguments.totp_only and totp and arguments.totp:
# The import is done here, to make pyperclip an optional dependency
import pyperclip
diff --git a/misc/userscripts/qute-pass b/misc/userscripts/qute-pass
index 9d078e94f..b49e87dd8 100755
--- a/misc/userscripts/qute-pass
+++ b/misc/userscripts/qute-pass
@@ -61,11 +61,19 @@ import sys
import tldextract
+
+def expanded_path(path):
+ # Expand potential ~ in paths, since this script won't be called from a shell that does it for us
+ expanded = os.path.expanduser(path)
+ # Add trailing slash if not present
+ return os.path.join(expanded, '')
+
+
argument_parser = argparse.ArgumentParser(description=__doc__, usage=USAGE, epilog=EPILOG)
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
argument_parser.add_argument('--password-store', '-p',
- default=os.getenv('PASSWORD_STORE_DIR', default=os.path.expanduser('~/.password-store')),
- help='Path to your pass password-store (only used in pass-mode)')
+ default=expanded_path(os.getenv('PASSWORD_STORE_DIR', default='~/.password-store')),
+ help='Path to your pass password-store (only used in pass-mode)', type=expanded_path)
argument_parser.add_argument('--mode', '-M', choices=['pass', 'gopass'], default="pass",
help='Select mode [gopass] to use gopass instead of the standard pass.')
argument_parser.add_argument('--username-pattern', '-u', default=r'.*/(.+)',
@@ -107,7 +115,7 @@ def qute_command(command):
fifo.flush()
-def find_pass_candidates(domain, password_store_path):
+def find_pass_candidates(domain):
candidates = []
if arguments.mode == "gopass":
@@ -117,13 +125,13 @@ def find_pass_candidates(domain, password_store_path):
if domain in password:
candidates.append(password)
else:
- for path, directories, file_names in os.walk(password_store_path, followlinks=True):
+ for path, directories, file_names in os.walk(arguments.password_store, followlinks=True):
secrets = fnmatch.filter(file_names, '*.gpg')
if not secrets:
continue
# Strip password store path prefix to get the relative pass path
- pass_path = path[len(password_store_path):]
+ pass_path = path[len(arguments.password_store):]
split_path = pass_path.split(os.path.sep)
for secret in secrets:
secret_base = os.path.splitext(secret)[0]
@@ -134,25 +142,27 @@ def find_pass_candidates(domain, password_store_path):
return candidates
-def _run_pass(pass_arguments, encoding):
+def _run_pass(pass_arguments):
# The executable is conveniently named after it's mode [pass|gopass].
pass_command = [arguments.mode]
- process = subprocess.run(pass_command + pass_arguments, stdout=subprocess.PIPE)
- return process.stdout.decode(encoding).strip()
+ env = os.environ.copy()
+ env['PASSWORD_STORE_DIR'] = arguments.password_store
+ process = subprocess.run(pass_command + pass_arguments, env=env, stdout=subprocess.PIPE)
+ return process.stdout.decode(arguments.io_encoding).strip()
-def pass_(path, encoding):
- return _run_pass([path], encoding)
+def pass_(path):
+ return _run_pass(['show', path])
-def pass_otp(path, encoding):
- return _run_pass(['otp', path], encoding)
+def pass_otp(path):
+ return _run_pass(['otp', path])
-def dmenu(items, invocation, encoding):
+def dmenu(items, invocation):
command = shlex.split(invocation)
- process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE)
- return process.stdout.decode(encoding).strip()
+ process = subprocess.run(command, input='\n'.join(items).encode(arguments.io_encoding), stdout=subprocess.PIPE)
+ return process.stdout.decode(arguments.io_encoding).strip()
def fake_key_raw(text):
@@ -170,11 +180,6 @@ def main(arguments):
extractor = tldextract.TLDExtract(extra_suffixes=arguments.extra_url_suffixes.split(','))
extract_result = extractor(arguments.url)
- # Expand potential ~ in paths, since this script won't be called from a shell that does it for us
- password_store_path = os.path.expanduser(arguments.password_store)
- # Add trailing slash if not present
- password_store_path = os.path.join(password_store_path, '')
-
# Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
# the registered domain name, the IPv4 address if that's what the URL represents and finally the private domain
# (if a non-public suffix was used).
@@ -188,7 +193,7 @@ def main(arguments):
for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.ipv4, private_domain]):
attempted_targets.append(target)
- target_candidates = find_pass_candidates(target, password_store_path)
+ target_candidates = find_pass_candidates(target)
if not target_candidates:
continue
@@ -200,8 +205,7 @@ def main(arguments):
stderr('No pass candidates for URL {!r} found! (I tried {!r})'.format(arguments.url, attempted_targets))
return ExitCodes.NO_PASS_CANDIDATES
- selection = candidates.pop() if len(candidates) == 1 else dmenu(sorted(candidates), arguments.dmenu_invocation,
- arguments.io_encoding)
+ selection = candidates.pop() if len(candidates) == 1 else dmenu(sorted(candidates), arguments.dmenu_invocation)
# Nothing was selected, simply return
if not selection:
return ExitCodes.SUCCESS
@@ -209,7 +213,7 @@ def main(arguments):
# If username-target is path and user asked for username-only, we don't need to run pass
secret = None
if not (arguments.username_target == 'path' and arguments.username_only):
- secret = pass_(selection, arguments.io_encoding)
+ secret = pass_(selection)
# Match password
match = re.match(arguments.password_pattern, secret)
@@ -231,7 +235,7 @@ def main(arguments):
elif arguments.password_only:
fake_key_raw(password)
elif arguments.otp_only:
- otp = pass_otp(selection, arguments.io_encoding)
+ otp = pass_otp(selection)
fake_key_raw(otp)
else:
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js
index 4b9446ed5..207b29b4a 100755
--- a/misc/userscripts/readability-js
+++ b/misc/userscripts/readability-js
@@ -44,6 +44,16 @@ const HEADER = `
h1, h2, h3 {
line-height: 1.2;
}
+ img {
+ max-width:100%;
+ height:auto;
+ }
+ p > img:only-child,
+ p > a:only-child > img:only-child,
+ .wp-caption img,
+ figure img {
+ display: block;
+ }
</style>
<!-- This icon is licensed under the Mozilla Public License 2.0 (available at: https://www.mozilla.org/en-US/MPL/2.0/).
The original icon can be found here: https://dxr.mozilla.org/mozilla-central/source/browser/themes/shared/reader/readerMode.svg -->
diff --git a/pytest.ini b/pytest.ini
index 1235efb4b..51411e11e 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -35,7 +35,7 @@ markers =
no_invalid_lines: Don't fail on unparseable lines in end2end tests
qtbug60673: Tests which are broken if the conversion from orange selection to real selection is flaky
fake_os: Fake utils.is_* to a fake operating system
- unicode_locale: Tests which need an unicode locale to work
+ unicode_locale: Tests which need a unicode locale to work
qtwebkit6021_xfail: Tests which would fail on WebKit version 602.1
js_headers: Sets JS headers dynamically on QtWebEngine (unsupported on some versions)
qtwebkit_pdf_imageformat_skip: Broken on QtWebKit with PDF image format plugin installed
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index b7b2f3d91..f7d951b33 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -897,6 +897,8 @@ class AbstractTab(QWidget):
icon_changed = pyqtSignal(QIcon)
#: Signal emitted when a page's title changed (new title as str)
title_changed = pyqtSignal(str)
+ #: Signal emitted when this tab was pinned/unpinned (new pinned state as bool)
+ pinned_changed = pyqtSignal(bool)
#: Signal emitted when a new tab should be opened (url as QUrl)
new_tab_requested = pyqtSignal(QUrl)
#: Signal emitted when a page's URL changed (url as QUrl)
@@ -1191,6 +1193,10 @@ class AbstractTab(QWidget):
def set_html(self, html: str, base_url: QUrl = QUrl()) -> None:
raise NotImplementedError
+ def set_pinned(self, pinned: bool) -> None:
+ self.data.pinned = pinned
+ self.pinned_changed.emit(pinned)
+
def __repr__(self) -> str:
try:
qurl = self.url()
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index ff18b5408..13f57e7dc 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -278,7 +278,7 @@ class CommandDispatcher:
return
to_pin = not tab.data.pinned
- self._tabbed_browser.widget.set_tab_pinned(tab, to_pin)
+ tab.set_pinned(to_pin)
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window')
@@ -421,7 +421,8 @@ class CommandDispatcher:
newtab.data.keep_icon = True
newtab.history.private_api.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor())
- new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
+
+ newtab.set_pinned(curtab.data.pinned)
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window',
@@ -1663,7 +1664,15 @@ class CommandDispatcher:
url: Interpret js-code as a `javascript:...` URL.
quiet: Don't show resulting JS object.
world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to
- run the snippet in.
+ run the snippet in. Predefined world names are:
+
+ - `main` (same world as the web page's JavaScript and
+ Greasemonkey, unless overridden via `@qute-js-world`)
+ - `application` (used for internal qutebrowser JS code,
+ should not be used via `:jseval` unless you know what
+ you're doing)
+ - `user` (currently unused)
+ - `jseval` (used for this command by default)
"""
cmdutils.check_exclusive((file, url), 'fu')
diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py
index 194246344..ddc100d14 100644
--- a/qutebrowser/browser/navigate.py
+++ b/qutebrowser/browser/navigate.py
@@ -154,14 +154,19 @@ def strip(url, count):
def _find_prevnext(prev, elems):
"""Find a prev/next element in the given list of elements."""
- # First check for <link rel="prev(ious)|next">
+ # First check for <link rel="prev(ious)|next"> as well as
+ # e.g. <a class="nav-(prev|next)"> (Hugo)
rel_values = {'prev', 'previous'} if prev else {'next'}
+ classes = {'nav-prev'} if prev else {'nav-next'}
for e in elems:
- if e.tag_name() not in ['link', 'a'] or 'rel' not in e:
+ if e.tag_name() not in ['link', 'a']:
continue
- if set(e['rel'].split(' ')) & rel_values:
+ if 'rel' in e and set(e['rel'].split(' ')) & rel_values:
log.hints.debug("Found {!r} with rel={}".format(e, e['rel']))
return e
+ elif e.classes() & classes:
+ log.hints.debug("Found {!r} with class={}".format(e, e.classes()))
+ return e
# Then check for regular links/buttons.
elems = [e for e in elems if e.tag_name() != 'link']
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index 98c5bd6d1..e79b5145e 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -102,8 +102,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Get the geometry for this element."""
raise NotImplementedError
- def classes(self) -> typing.List[str]:
- """Get a list of classes assigned to this element."""
+ def classes(self) -> typing.Set[str]:
+ """Get a set of classes assigned to this element."""
raise NotImplementedError
def tag_name(self) -> str:
diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py
index db5335a3c..e524b36d2 100644
--- a/qutebrowser/browser/webengine/webengineelem.py
+++ b/qutebrowser/browser/webengine/webengineelem.py
@@ -118,9 +118,9 @@ class WebEngineElement(webelem.AbstractWebElement):
log.stub()
return QRect()
- def classes(self) -> typing.List[str]:
+ def classes(self) -> typing.Set[str]:
"""Get a list of classes assigned to this element."""
- return self._js_dict['class_name'].split()
+ return set(self._js_dict['class_name'].split())
def tag_name(self) -> str:
"""Get the tag name of this element.
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index f5f4e9c31..336540ba0 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -390,9 +390,14 @@ def init_private_profile():
def _init_site_specific_quirks():
+ """Add custom user-agent settings for problematic sites.
+
+ See https://github.com/qutebrowser/qutebrowser/issues/4810
+ """
if not config.val.content.site_specific_quirks:
return
+ # Please leave this here as a template for new UAs.
# default_ua = ("Mozilla/5.0 ({os_info}) "
# "AppleWebKit/{webkit_version} (KHTML, like Gecko) "
# "{qt_key}/{qt_version} "
@@ -402,7 +407,6 @@ def _init_site_specific_quirks():
"AppleWebKit/{webkit_version} (KHTML, like Gecko) "
"{upstream_browser_key}/{upstream_browser_version} "
"Safari/{webkit_version}")
- firefox_ua = "Mozilla/5.0 ({os_info}; rv:71.0) Gecko/20100101 Firefox/71.0"
new_chrome_ua = ("Mozilla/5.0 ({os_info}) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/99 "
@@ -414,14 +418,26 @@ def _init_site_specific_quirks():
"Edg/{upstream_browser_version}")
user_agents = {
+ # Needed to avoid a ""WhatsApp works with Google Chrome 36+" error
+ # page which doesn't allow to use WhatsApp Web at all. Also see the
+ # additional JS quirk: qutebrowser/javascript/whatsapp_web_quirk.user.js
+ # https://github.com/qutebrowser/qutebrowser/issues/4445
'https://web.whatsapp.com/': no_qtwe_ua,
+
+ # Needed to avoid a "you're using a browser [...] that doesn't allow us
+ # to keep your account secure" error.
+ # https://github.com/qutebrowser/qutebrowser/issues/5182
'https://accounts.google.com/*': edge_ua,
+
+ # Needed because Slack adds an error which prevents using it relatively
+ # aggressively, despite things actually working fine.
+ # September 2020: Qt 5.12 works, but Qt <= 5.11 shows the error.
+ # https://github.com/qutebrowser/qutebrowser/issues/4669
'https://*.slack.com/*': new_chrome_ua,
- 'https://docs.google.com/*': firefox_ua,
- 'https://drive.google.com/*': firefox_ua,
}
if not qtutils.version_check('5.9'):
+ # Shows 502 Bad Gateway with the Qt 5.7 UA.
user_agents['https://www.dell.com/support/*'] = new_chrome_ua
for pattern, ua in user_agents.items():
diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py
index 9f58031de..e73d6a9e8 100644
--- a/qutebrowser/browser/webkit/webkitelem.py
+++ b/qutebrowser/browser/webkit/webkitelem.py
@@ -101,9 +101,9 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished()
return self._elem.geometry()
- def classes(self) -> typing.List[str]:
+ def classes(self) -> typing.Set[str]:
self._check_vanished()
- return self._elem.classes()
+ return set(self._elem.classes())
def tag_name(self) -> str:
"""Get the tag name for the current element."""
diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py
index 6706848dd..980ae78d0 100644
--- a/qutebrowser/browser/webkit/webview.py
+++ b/qutebrowser/browser/webkit/webview.py
@@ -181,9 +181,9 @@ class WebView(QWebView):
This is not needed for QtWebEngine, so it's in here.
"""
menu = self.page().createStandardContextMenu()
- self.shutting_down.connect(menu.close) # type: ignore[arg-type]
+ self.shutting_down.connect(menu.close)
mm = modeman.instance(self.win_id)
- mm.entered.connect(menu.close) # type: ignore[arg-type]
+ mm.entered.connect(menu.close)
menu.exec_(e.globalPos())
def showEvent(self, e):
diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py
index 36e334955..9cf2d5fd6 100644
--- a/qutebrowser/completion/models/miscmodels.py
+++ b/qutebrowser/completion/models/miscmodels.py
@@ -39,11 +39,11 @@ def command(*, info):
def helptopic(*, info):
"""A CompletionModel filled with help topics."""
- model = completionmodel.CompletionModel()
+ model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
cmdlist = util.get_cmd_completions(info, include_aliases=False,
include_hidden=True, prefix=':')
- settings = ((opt.name, opt.description)
+ settings = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Commands", cmdlist))
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 55f600593..cf3100582 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -2714,6 +2714,7 @@ colors.webpage.prefers_color_scheme_dark:
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
+ restart: true
## dark mode
diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py
index 9940a64ac..a1b0e75bd 100644
--- a/qutebrowser/config/configfiles.py
+++ b/qutebrowser/config/configfiles.py
@@ -606,7 +606,9 @@ class ConfigAPI:
def source(self, filename: str) -> None:
"""Read the given config file from disk."""
if not os.path.isabs(filename):
- filename = str(self.configdir / filename)
+ # We don't use self.configdir here so we get the proper file when starting
+ # with a --config-py argument given.
+ filename = os.path.join(os.path.dirname(standarddir.config_py()), filename)
try:
read_config_py(filename)
diff --git a/qutebrowser/html/warning-sessions.html b/qutebrowser/html/warning-sessions.html
index fadc9908d..6f447483f 100644
--- a/qutebrowser/html/warning-sessions.html
+++ b/qutebrowser/html/warning-sessions.html
@@ -9,7 +9,7 @@ qute://warning/sessions</span> to show it again at a later time.</span>
<p>Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.</p>
-<p>At the time of writing (April 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v1.14.0.</p>
+<p>At the time of writing (September 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v1.15.0.</p>
<p>As a stop-gap measure:</p>
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 0f9cafac0..57a9ae018 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -28,7 +28,6 @@ import datetime
import attr
from PyQt5.QtWidgets import QSizePolicy, QWidget, QApplication
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
-from PyQt5.QtGui import QIcon
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
@@ -212,8 +211,7 @@ class TabbedBrowser(QWidget):
self._tab_insert_idx_right = -1
self.is_shutting_down = False
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
- self.widget.new_tab_requested.connect(
- self.tabopen) # type: ignore[arg-type]
+ self.widget.new_tab_requested.connect(self.tabopen)
self.widget.currentChanged.connect(self._on_current_changed)
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@@ -351,6 +349,8 @@ class TabbedBrowser(QWidget):
functools.partial(self._on_title_changed, tab))
tab.icon_changed.connect(
functools.partial(self._on_icon_changed, tab))
+ tab.pinned_changed.connect(
+ functools.partial(self._on_pinned_changed, tab))
tab.load_progress.connect(
functools.partial(self._on_load_progress, tab))
tab.load_finished.connect(
@@ -530,7 +530,7 @@ class TabbedBrowser(QWidget):
newtab = self.tabopen(background=False, idx=entry.index)
newtab.history.private_api.deserialize(entry.history)
- self.widget.set_tab_pinned(newtab, entry.pinned)
+ newtab.set_pinned(entry.pinned)
@pyqtSlot('QUrl', bool)
def load_url(self, url, newtab):
@@ -788,26 +788,21 @@ class TabbedBrowser(QWidget):
if not self.widget.page_title(idx):
self.widget.set_page_title(idx, url.toDisplayString())
- @pyqtSlot(browsertab.AbstractTab, QIcon)
- def _on_icon_changed(self, tab, icon):
+ @pyqtSlot(browsertab.AbstractTab)
+ def _on_icon_changed(self, tab):
"""Set the icon of a tab.
Slot for the iconChanged signal of any tab.
Args:
tab: The WebView where the title was changed.
- icon: The new icon
"""
- if not tab.data.should_show_icon():
- return
try:
- idx = self._tab_index(tab)
+ self._tab_index(tab)
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
- self.widget.setTabIcon(idx, icon)
- if config.val.tabs.tabs_are_windows:
- self.widget.window().setWindowIcon(icon)
+ self.widget.update_tab_favicon(tab)
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
@@ -922,6 +917,12 @@ class TabbedBrowser(QWidget):
self._update_window_title('scroll_pos')
self.widget.update_tab_title(idx, 'scroll_pos')
+ def _on_pinned_changed(self, tab):
+ """Update the tab's pinned status."""
+ idx = self.widget.indexOf(tab)
+ self.widget.update_tab_favicon(tab)
+ self.widget.update_tab_title(idx)
+
def _on_audio_changed(self, tab, _muted):
"""Update audio field in tab when mute or recentlyAudible changed."""
try:
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index 3f94f9901..fdefa075e 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -99,19 +99,6 @@ class TabWidget(QTabWidget):
bar.set_tab_data(idx, 'indicator-color', color)
bar.update(bar.tabRect(idx))
- def set_tab_pinned(self, tab: QWidget,
- pinned: bool) -> None:
- """Set the tab status as pinned.
-
- Args:
- tab: The tab to pin
- pinned: Pinned tab state to set.
- """
- idx = self.indexOf(tab)
- tab.data.pinned = pinned
- self.update_tab_favicon(tab)
- self.update_tab_title(idx)
-
def tab_indicator_color(self, idx):
"""Get the tab indicator color for the given index."""
return self.tabBar().tab_indicator_color(idx)
@@ -139,6 +126,7 @@ class TabWidget(QTabWidget):
field: A field name which was updated. If given, the title
is only set if the given field is in the template.
"""
+ assert idx != -1
tab = self.widget(idx)
if tab.data.pinned:
fmt = config.cache['tabs.title.format_pinned']
@@ -344,14 +332,11 @@ class TabWidget(QTabWidget):
"""Update favicon of the given tab."""
idx = self.indexOf(tab)
- if tab.data.should_show_icon():
- self.setTabIcon(idx, tab.icon())
- if config.val.tabs.tabs_are_windows:
- self.window().setWindowIcon(tab.icon())
- else:
- self.setTabIcon(idx, QIcon())
- if config.val.tabs.tabs_are_windows:
- self.window().setWindowIcon(self.window().windowIcon())
+ icon = tab.icon() if tab.data.should_show_icon() else QIcon()
+ self.setTabIcon(idx, icon)
+
+ if config.val.tabs.tabs_are_windows:
+ self.window().setWindowIcon(tab.icon())
def setTabIcon(self, idx: int, icon: QIcon) -> None:
"""Always show tab icons for pinned tabs in some circumstances."""
diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py
index 4387e479a..850883bf4 100644
--- a/qutebrowser/misc/crashdialog.py
+++ b/qutebrowser/misc/crashdialog.py
@@ -635,7 +635,7 @@ class ReportErrorDialog(QDialog):
hbox = QHBoxLayout()
hbox.addStretch()
btn = QPushButton("Close")
- btn.clicked.connect(self.close) # type: ignore[arg-type]
+ btn.clicked.connect(self.close)
hbox.addWidget(btn)
vbox.addLayout(hbox)
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index dcdc0821b..0ebb415ac 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -470,8 +470,7 @@ class SessionManager(QObject):
if tab.get('active', False):
tab_to_focus = i
if new_tab.data.pinned:
- tabbed_browser.widget.set_tab_pinned(new_tab,
- new_tab.data.pinned)
+ new_tab.set_pinned(True)
if tab_to_focus is not None:
tabbed_browser.widget.setCurrentIndex(tab_to_focus)
if win.get('active', False):
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index a691b2cbc..a14be78a8 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -644,12 +644,10 @@ def parse_javascript_url(url: QUrl) -> str:
raise Error("URL contains unexpected components: {}"
.format(url.authority()))
- code = url.path(QUrl.FullyDecoded)
- if url.hasQuery():
- code += '?' + url.query(QUrl.FullyDecoded)
- if url.hasFragment():
- code += '#' + url.fragment(QUrl.FullyDecoded)
+ urlstr = url.toString(QUrl.FullyEncoded) # type: ignore[arg-type]
+ urlstr = urllib.parse.unquote(urlstr)
+ code = urlstr[len('javascript:'):]
if not code:
raise Error("Resulted in empty JavaScript code")
diff --git a/requirements.txt b/requirements.txt
index 9edb189ec..992b3c38b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,11 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
adblock==0.3.1
-attrs==20.1.0
+attrs==20.2.0
colorama==0.4.3
cssutils==1.0.2
Jinja2==2.11.2
MarkupSafe==1.1.1
-Pygments==2.6.1
+Pygments==2.7.1
pyPEG2==2.15.2
PyYAML==5.3.1
diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py
index 5cb49c767..17c3fb367 100755
--- a/scripts/asciidoc2html.py
+++ b/scripts/asciidoc2html.py
@@ -46,10 +46,12 @@ class AsciiDoc:
FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts']
def __init__(self,
- asciidoc: Optional[List[str]],
+ asciidoc: Optional[str],
+ asciidoc_python: Optional[str],
website: Optional[str]) -> None:
self._cmd = None # type: Optional[List[str]]
self._asciidoc = asciidoc
+ self._asciidoc_python = asciidoc_python
self._website = website
self._homedir = None # type: Optional[pathlib.Path]
self._themedir = None # type: Optional[pathlib.Path]
@@ -218,7 +220,9 @@ class AsciiDoc:
def _get_asciidoc_cmd(self) -> List[str]:
"""Try to find out what commandline to use to invoke asciidoc."""
if self._asciidoc is not None:
- return self._asciidoc
+ python = (sys.executable if self._asciidoc_python is None
+ else self._asciidoc_python)
+ return [python, self._asciidoc]
for executable in ['asciidoc', 'asciidoc.py']:
try:
@@ -270,10 +274,12 @@ def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument('--website', help="Build website into a given "
"directory.")
- parser.add_argument('--asciidoc', help="Full path to python and "
- "asciidoc.py. If not given, it's searched in PATH.",
- nargs=2, required=False,
- metavar=('PYTHON', 'ASCIIDOC'))
+ parser.add_argument('--asciidoc', help="Full path to asciidoc.py. "
+ "If not given, it's searched in PATH.",
+ nargs='?')
+ parser.add_argument('--asciidoc-python', help="Python to use for asciidoc."
+ "If not given, the current Python interpreter is used.",
+ nargs='?')
return parser.parse_args()
@@ -301,7 +307,8 @@ def main(colors: bool = False) -> None:
utils.change_cwd()
utils.use_color = colors
args = parse_args()
- run(asciidoc=args.asciidoc, website=args.website)
+ run(asciidoc=args.asciidoc, asciidoc_python=args.asciidoc_python,
+ website=args.website)
if __name__ == '__main__':
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index ee0ac2c53..8030d61a1 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -78,10 +78,11 @@ def call_tox(toxenv, *args, python=sys.executable):
def run_asciidoc2html(args):
"""Common buildsteps used for all OS'."""
utils.print_title("Running asciidoc2html.py")
+ a2h_args = []
if args.asciidoc is not None:
- a2h_args = ['--asciidoc'] + args.asciidoc
- else:
- a2h_args = []
+ a2h_args += ['--asciidoc', args.asciidoc]
+ if args.asciidoc_python is not None:
+ a2h_args += ['--asciidoc-python', args.asciidoc_python]
call_script('asciidoc2html.py', *a2h_args)
@@ -201,7 +202,7 @@ def build_mac():
utils.print_title("Updating 3rdparty content")
update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
utils.print_title("Building .app via pyinstaller")
- call_tox('pyinstaller', '-r')
+ call_tox('pyinstaller-64', '-r')
utils.print_title("Patching .app")
patch_mac_app()
utils.print_title("Building .dmg")
@@ -457,10 +458,12 @@ def main():
parser = argparse.ArgumentParser()
parser.add_argument('--no-asciidoc', action='store_true',
help="Don't generate docs")
- parser.add_argument('--asciidoc', help="Full path to python and "
- "asciidoc.py. If not given, it's searched in PATH.",
- nargs=2, required=False,
- metavar=('PYTHON', 'ASCIIDOC'))
+ parser.add_argument('--asciidoc', help="Full path to asciidoc.py. "
+ "If not given, it's searched in PATH.",
+ nargs='?')
+ parser.add_argument('--asciidoc-python', help="Python to use for asciidoc."
+ "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")
args = parser.parse_args()
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index 6bb3eb1ca..df2845405 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -30,33 +30,55 @@ import tokenize
import traceback
import collections
import pathlib
+from typing import List, Iterator, Optional
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir))
from scripts import utils
-
-def _get_files(only_py=False):
- """Iterate over all python files and yield filenames."""
- for (dirpath, _dirnames, filenames) in os.walk('.'):
- parts = dirpath.split(os.sep)
- if len(parts) >= 2:
- rootdir = parts[1]
- if rootdir.startswith('.') or rootdir == 'htmlcov':
- # ignore hidden dirs and htmlcov
- continue
-
- if only_py:
- endings = {'.py'}
- else:
- endings = {'.py', '.asciidoc', '.js', '.feature'}
- files = (e for e in filenames if os.path.splitext(e)[1] in endings)
- for name in files:
- yield os.path.join(dirpath, name)
-
-
-def check_git():
+BINARY_EXTS = {'.png', '.icns', '.ico', '.bmp', '.gz', '.bin', '.pdf',
+ '.sqlite', '.woff2', '.whl'}
+
+
+def _get_files(
+ *,
+ verbose: bool,
+ ignored: List[pathlib.Path] = None
+) -> Iterator[pathlib.Path]:
+ """Iterate over all files and yield filenames."""
+ filenames = subprocess.run(
+ ['git', 'ls-files', '--cached', '--others', '--exclude-standard', '-z'],
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ check=True,
+ )
+ all_ignored = ignored or []
+ all_ignored.append(
+ pathlib.Path('tests', 'unit', 'scripts', 'importer_sample', 'chrome'))
+
+ for filename in filenames.stdout.split('\0'):
+ path = pathlib.Path(filename)
+ is_ignored = any(path == p or p in path.parents for p in all_ignored)
+ if not filename or path.suffix in BINARY_EXTS or is_ignored:
+ continue
+
+ try:
+ with tokenize.open(str(path)):
+ pass
+ except SyntaxError as e:
+ # Could not find encoding
+ utils.print_col("{} - maybe {} should be added to BINARY_EXTS?".format(
+ str(e).capitalize(), path.suffix), 'yellow')
+ continue
+
+ if verbose:
+ print(path)
+
+ yield path
+
+
+def check_git(_args: argparse.Namespace = None) -> bool:
"""Check for uncommitted git files.."""
if not os.path.isdir(".git"):
print("No .git dir, ignoring")
@@ -79,7 +101,7 @@ def check_git():
return status
-def check_spelling():
+def check_spelling(args: argparse.Namespace) -> Optional[bool]:
"""Check commonly misspelled words."""
# Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
@@ -90,37 +112,36 @@ def check_spelling():
'exitted', 'mininum', 'resett?ed', 'recieved', 'regularily',
'underlaying', 'inexistant', 'elipsis', 'commiting', 'existant',
'resetted', 'similarily', 'informations', 'an url', 'treshold',
- 'artefact'}
+ 'artefact', 'an unix', 'an utf', 'an unicode'}
# Words which look better when splitted, but might need some fine tuning.
words |= {'webelements', 'mouseevent', 'keysequence', 'normalmode',
'eventloops', 'sizehint', 'statemachine', 'metaobject',
- 'logrecord', 'filetype'}
+ 'logrecord'}
# Files which should be ignored, e.g. because they come from another
# package
+ hint_data = pathlib.Path('tests', 'end2end', 'data', 'hints')
ignored = [
- os.path.join('.', 'scripts', 'dev', 'misc_checks.py'),
- os.path.join('.', 'qutebrowser', '3rdparty', 'pdfjs'),
- os.path.join('.', 'tests', 'end2end', 'data', 'hints', 'ace',
- 'ace.js'),
+ pathlib.Path('scripts', 'dev', 'misc_checks.py'),
+ pathlib.Path('qutebrowser', '3rdparty', 'pdfjs'),
+ hint_data / 'ace' / 'ace.js',
+ hint_data / 'bootstrap' / 'bootstrap.css',
]
seen = collections.defaultdict(list)
try:
ok = True
- for fn in _get_files():
- with tokenize.open(fn) as f:
- if any(fn.startswith(i) for i in ignored):
- continue
+ for path in _get_files(verbose=args.verbose, ignored=ignored):
+ with tokenize.open(str(path)) as f:
for line in f:
for w in words:
pattern = '[{}{}]{}'.format(w[0], w[0].upper(), w[1:])
if (re.search(pattern, line) and
- fn not in seen[w] and
+ path not in seen[w] and
'# pragma: no spellcheck' not in line):
- print('Found "{}" in {}!'.format(w, fn))
- seen[w].append(fn)
+ print('Found "{}" in {}!'.format(w, path))
+ seen[w].append(path)
ok = False
print()
return ok
@@ -129,15 +150,18 @@ def check_spelling():
return None
-def check_vcs_conflict():
+def check_vcs_conflict(args: argparse.Namespace) -> Optional[bool]:
"""Check VCS conflict markers."""
try:
ok = True
- for fn in _get_files(only_py=True):
- with tokenize.open(fn) as f:
+ for path in _get_files(verbose=args.verbose):
+ if path.suffix in {'.rst', '.asciidoc'}:
+ # False positives
+ continue
+ with tokenize.open(str(path)) as f:
for line in f:
if any(line.startswith(c * 7) for c in '<>=|'):
- print("Found conflict marker in {}".format(fn))
+ print("Found conflict marker in {}".format(path))
ok = False
print()
return ok
@@ -146,7 +170,7 @@ def check_vcs_conflict():
return None
-def check_userscripts_descriptions():
+def check_userscripts_descriptions(_args: argparse.Namespace = None) -> bool:
"""Make sure all userscripts are described properly."""
folder = pathlib.Path('misc/userscripts')
readme = folder / 'README.md'
@@ -178,20 +202,21 @@ def check_userscripts_descriptions():
return ok
-def main():
+def main() -> int:
parser = argparse.ArgumentParser()
+ parser.add_argument('--verbose', action='store_true', help='Show checked filenames')
parser.add_argument('checker',
choices=('git', 'vcs', 'spelling', 'userscripts'),
help="Which checker to run.")
args = parser.parse_args()
if args.checker == 'git':
- ok = check_git()
+ ok = check_git(args)
elif args.checker == 'vcs':
- ok = check_vcs_conflict()
+ ok = check_vcs_conflict(args)
elif args.checker == 'spelling':
- ok = check_spelling()
+ ok = check_spelling(args)
elif args.checker == 'userscripts':
- ok = check_userscripts_descriptions()
+ ok = check_userscripts_descriptions(args)
return 0 if ok else 1
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index e36d7ee1d..439976c34 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -43,6 +43,11 @@ CHANGELOG_URLS = {
'pylint': 'http://pylint.pycqa.org/en/latest/whatsnew/changelog.html',
'setuptools': 'https://github.com/pypa/setuptools/blob/master/CHANGES.rst',
'pytest-cov': 'https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst',
+ 'pytest-xdist': 'https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst',
+ 'pytest-forked': 'https://github.com/pytest-dev/pytest-forked/blob/master/CHANGELOG',
+ 'execnet': 'https://execnet.readthedocs.io/en/latest/changelog.html',
+ 'apipkg': 'https://github.com/pytest-dev/apipkg/blob/master/CHANGELOG',
+ 'pytest-rerunfailures': 'https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst',
'requests': 'https://github.com/psf/requests/blob/master/HISTORY.md',
'requests-file': 'https://github.com/dashea/requests-file/blob/master/CHANGES.rst',
'werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst',
@@ -101,7 +106,12 @@ CHANGELOG_URLS = {
'pep517': 'https://github.com/pypa/pep517/commits/master',
'cryptography': 'https://cryptography.io/en/latest/changelog/',
'toml': 'https://github.com/uiri/toml/releases',
- 'pyqt': 'https://www.riverbankcomputing.com/',
+ 'PyQt5': 'https://www.riverbankcomputing.com/news',
+ 'PyQtWebEngine': 'https://www.riverbankcomputing.com/news',
+ 'PyQt-builder': 'https://www.riverbankcomputing.com/news',
+ 'PyQt5-sip': 'https://www.riverbankcomputing.com/news',
+ 'sip': 'https://www.riverbankcomputing.com/news',
+ 'Pygments': 'https://pygments.org/docs/changelog/',
'vulture': 'https://github.com/jendrikseipp/vulture/blob/master/CHANGELOG.md',
'distlib': 'https://bitbucket.org/pypa/distlib/src/master/CHANGES.rst',
'py-cpuinfo': 'https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog',
@@ -111,6 +121,7 @@ CHANGELOG_URLS = {
'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst',
'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md',
'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
+ 'diff_cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG',
}
# PyQt versions which need SIP v4
@@ -265,8 +276,8 @@ class Change:
self.name = name
self.old = None
self.new = None
- if name.lower() in CHANGELOG_URLS:
- self.url = CHANGELOG_URLS[name.lower()]
+ if name in CHANGELOG_URLS:
+ self.url = CHANGELOG_URLS[name]
self.link = '[{}]({})'.format(self.name, self.url)
else:
self.url = '(no changelog)'
@@ -313,6 +324,8 @@ def print_changed_files():
if '==' in line:
name, version = line[1:].split('==')
+ if ';' in version: # pip environment markers
+ version = version.split(';')[0].strip()
else:
name = line[1:]
version = '?'
diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py
index e86ff257d..7c12dab15 100644
--- a/scripts/dev/update_version.py
+++ b/scripts/dev/update_version.py
@@ -78,13 +78,12 @@ if __name__ == "__main__":
print("* Create new release via GitHub (required to upload release "
"artifacts)")
print("* Linux: git fetch && git checkout v{v} && "
- "./.venv/bin/python3 scripts/dev/build_release.py --upload"
+ "tox -e build-release -- --upload"
.format(v=version))
print("* Windows: git fetch; git checkout v{v}; "
- "py -3 scripts\\dev\\build_release.py --asciidoc "
- "C:\\Python27\\python "
- "$env:userprofile\\bin\\asciidoc-8.6.10\\asciidoc.py --upload"
+ "py -3.7 -m tox -e build-release -- --asciidoc "
+ "$env:userprofile\\bin\\asciidoc-9.9.2\\asciidoc.py --upload"
.format(v=version))
print("* macOS: git fetch && git checkout v{v} && "
- "python3 scripts/dev/build_release.py --upload"
+ "tox -e build-release -- --upload"
.format(v=version))
diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature
index 4b645d554..2d3dfe1d1 100644
--- a/tests/end2end/features/tabs.feature
+++ b/tests/end2end/features/tabs.feature
@@ -1613,3 +1613,12 @@ Feature: Tab management
And I open data/hello.txt in a new tab
And I run :fake-key -g hello-world<enter>
Then the message "hello-world" should be shown
+
+ Scenario: Undo after changing tabs_are_windows
+ When I open data/hello.txt
+ And I open data/hello.txt in a new tab
+ And I set tabs.tabs_are_windows to true
+ And I run :tab-close
+ And I run :undo
+ And I run :message-info "Still alive!"
+ Then the message "Still alive!" should be shown
diff --git a/tests/manual/hints/hide_unmatched_rapid_hints.html b/tests/manual/hints/hide_unmatched_rapid_hints.html
index 1630a790e..98affa254 100644
--- a/tests/manual/hints/hide_unmatched_rapid_hints.html
+++ b/tests/manual/hints/hide_unmatched_rapid_hints.html
@@ -7,7 +7,7 @@
<body>
<p>When <code>hints.hide_unmatched_rapid_hints</code> is set to true (default), rapid hints behave like normal hints, i.e. unmatched hints will be hidden as you type. Setting the option to false will disable hiding in rapid mode, which is sometimes useful (see <a href="https://github.com/qutebrowser/qutebrowser/issues/1799">#1799</a>).</p>
<p>Note that when hinting in number mode, the <code>hints.hide_unmatched_rapid_hints</code> option affects typing the hint string (number), but not the filter (letters).</p>
- <p>Here is couple of invalid links to test the behaviour:</p>
+ <p>Here is couple of invalid links to test the behavior:</p>
<p><a href="#foo">one</a></p>
<p><a href="#foo">two</a></p>
<p><a href="#foo">three</a></p>
diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py
index e1c9d04f8..0aa3943e7 100644
--- a/tests/unit/browser/webkit/network/test_networkreply.py
+++ b/tests/unit/browser/webkit/network/test_networkreply.py
@@ -52,8 +52,9 @@ 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.waitSignals([reply.metaDataChanged, reply.readyRead,
- reply.finished], order='strict'):
+ with qtbot.waitSignal(reply.metaDataChanged), \
+ qtbot.waitSignal(reply.readyRead), \
+ qtbot.waitSignal(reply.finished):
pass
assert reply.bytesAvailable() == len(data)
@@ -78,7 +79,7 @@ def test_error_network_reply(qtbot, req):
reply = networkreply.ErrorNetworkReply(
req, "This is an error", QNetworkReply.UnknownNetworkError)
- with qtbot.waitSignals([reply.error, reply.finished], order='strict'):
+ with qtbot.waitSignal(reply.error), qtbot.waitSignal(reply.finished):
pass
reply.abort() # shouldn't do anything
diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py
index 98e70dc01..1fc0b4d73 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.waitSignals([model.layoutAboutToBeChanged, model.layoutChanged],
- order='strict'):
+ with qtbot.waitSignal(model.layoutAboutToBeChanged), \
+ qtbot.waitSignal(model.layoutChanged):
model.set_pattern(pat)
for c in cats:
c.set_pattern.assert_called_with(pat)
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index e602f0ab6..2ac7084dd 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -124,7 +124,7 @@ def configdata_stub(config_stub, monkeypatch, configdata_init):
no_autoconfig=True)),
('bindings.commands', configdata.Option(
name='bindings.commands',
- description='Default keybindings',
+ description='Custom keybindings',
typ=configtypes.Dict(
keytype=configtypes.String(),
valtype=configtypes.Dict(
@@ -269,13 +269,38 @@ def test_help_completion(qtmodeltester, cmdutils_stub, key_config_stub,
(':tab-close', 'Close the current tab.', ''),
],
"Settings": [
- ('aliases', 'Aliases for commands.', None),
- ('bindings.commands', 'Default keybindings', None),
- ('bindings.default', 'Default keybindings', None),
- ('completion.open_categories', 'Which categories to show (in '
- 'which order) in the :open completion.', None),
- ('content.javascript.enabled', 'Enable/Disable JavaScript', None),
- ('url.searchengines', 'searchengines list', None),
+ (
+ 'aliases',
+ 'Aliases for commands.',
+ '{"q": "quit"}',
+ ),
+ (
+ 'bindings.commands',
+ 'Custom keybindings',
+ ('{"normal": {"<Ctrl+q>": "quit", "I": "invalid", "ZQ": "quit", '
+ '"d": "scroll down"}}'),
+ ),
+ (
+ 'bindings.default',
+ 'Default keybindings',
+ '{"normal": {"<Ctrl+q>": "quit", "d": "tab-close"}}',
+ ),
+ (
+ 'completion.open_categories',
+ 'Which categories to show (in which order) in the :open completion.',
+ '["searchengines", "quickmarks", "bookmarks", "history"]',
+ ),
+ (
+ 'content.javascript.enabled',
+ 'Enable/Disable JavaScript',
+ 'true'
+ ),
+ (
+ 'url.searchengines',
+ 'searchengines list',
+ ('{"DEFAULT": "https://duckduckgo.com/?q={}", '
+ '"google": "https://google.com/?q={}"}'),
+ ),
],
})
@@ -909,7 +934,7 @@ def test_setting_option_completion(qtmodeltester, config_stub,
_check_completions(model, {
"Options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
- ('bindings.commands', 'Default keybindings', (
+ ('bindings.commands', 'Custom keybindings', (
'{"normal": {"<Ctrl+q>": "quit", "I": "invalid", '
'"ZQ": "quit", "d": "scroll down"}}')),
('completion.open_categories', 'Which categories to show (in '
@@ -933,7 +958,7 @@ def test_setting_dict_option_completion(qtmodeltester, config_stub,
_check_completions(model, {
"Dict options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
- ('bindings.commands', 'Default keybindings', (
+ ('bindings.commands', 'Custom keybindings', (
'{"normal": {"<Ctrl+q>": "quit", "I": "invalid", '
'"ZQ": "quit", "d": "scroll down"}}')),
('url.searchengines', 'searchengines list',
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
index b30ab4bee..588e4a5cf 100644
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -432,11 +432,12 @@ class TestConfig:
assert conf.get_obj(name1) == 'never'
assert conf.get_obj(name2) is True
- with qtbot.waitSignals([conf.changed, conf.changed]) as blocker:
+ with qtbot.waitSignal(conf.changed), qtbot.waitSignal(conf.changed):
conf.clear(save_yaml=save_yaml)
- options = {e.args[0] for e in blocker.all_signals_and_args}
- assert options == {name1, name2}
+ # Doesn't work with PyQt 5.15.1 workaround
+ # options = {blocker1.args[0], blocker2.args[0]}
+ # assert options == {name1, name2}
if save_yaml:
assert yaml_value(name1) is usertypes.UNSET
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
index 27e96ef7d..bef4ef004 100644
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -28,7 +28,7 @@ from PyQt5.QtCore import QSettings
from qutebrowser.config import (config, configfiles, configexc, configdata,
configtypes)
-from qutebrowser.utils import utils, usertypes, urlmatch
+from qutebrowser.utils import utils, usertypes, urlmatch, standarddir
from qutebrowser.keyinput import keyutils
@@ -1064,6 +1064,24 @@ class TestConfigPy:
assert not config.instance.get_obj('content.javascript.enabled')
+ def test_source_configpy_arg(self, tmpdir, data_tmpdir, monkeypatch):
+ alt_filename = 'alt-config.py'
+
+ alt_confpy_dir = tmpdir / 'alt-confpy-dir'
+ alt_confpy_dir.ensure(dir=True)
+ monkeypatch.setattr(standarddir, 'config_py',
+ lambda: str(alt_confpy_dir / alt_filename))
+
+ subfile = alt_confpy_dir / 'subfile.py'
+ subfile.write_text("c.content.javascript.enabled = False",
+ encoding='utf-8')
+
+ alt_confpy = ConfPy(alt_confpy_dir, alt_filename)
+ alt_confpy.write("config.source('subfile.py')")
+ alt_confpy.read()
+
+ assert not config.instance.get_obj('content.javascript.enabled')
+
def test_source_errors(self, tmpdir, confpy):
subfile = tmpdir / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')
diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py
index f8caaf1af..4e474b4f3 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
+from PyQt5.QtCore import Qt, PYQT_VERSION
import pytest
from qutebrowser.keyinput import basekeyparser, keyutils
@@ -309,6 +309,8 @@ 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/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py
index 659aac7ec..b271c18ab 100644
--- a/tests/unit/mainwindow/test_tabwidget.py
+++ b/tests/unit/mainwindow/test_tabwidget.py
@@ -94,8 +94,9 @@ class TestTabWidget:
config_stub.val.tabs.position = "left"
pinned_num = [1, num_tabs - 1]
- for tab in pinned_num:
- widget.set_tab_pinned(widget.widget(tab), True)
+ for num in pinned_num:
+ tab = widget.widget(num)
+ tab.set_pinned(True)
first_size = widget.tabBar().tabSizeHint(0)
first_size_min = widget.tabBar().minimumTabSizeHint(0)
diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py
index 4ed19f64e..7e71478ce 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.waitSignals([proc.started, proc.finished], timeout=10000,
- order='strict'):
+ with qtbot.waitSignal(proc.started, timeout=10000), \
+ qtbot.waitSignal(proc.finished, timeout=10000):
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.waitSignals([proc.started, proc.finished], timeout=10000,
- order='strict'):
+ with qtbot.waitSignal(proc.started, timeout=10000), \
+ qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv)
@@ -99,9 +99,8 @@ 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.waitSignals([proc.started, proc.finished],
- timeout=10000,
- order='strict'):
+ with qtbot.waitSignal(proc.started, timeout=10000), \
+ qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc(';'.join(code))
proc.start(*argv)
@@ -147,8 +146,8 @@ def test_start_env(monkeypatch, qtbot, py_proc):
sys.exit(0)
""")
- with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
- order='strict'):
+ with qtbot.waitSignal(proc.started, timeout=10000), \
+ qtbot.waitSignal(proc.finished, timeout=10000):
proc.start(*argv)
data = qutescheme.spawn_output
@@ -187,12 +186,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.waitSignals([proc.started, proc.finished], timeout=10000,
- order='strict'):
+ with qtbot.waitSignal(proc.started, timeout=10000), \
+ qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc("import sys; sys.exit(0)")
proc.start(*argv)
- with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
- order='strict'):
+ with qtbot.waitSignal(proc.started, timeout=10000), \
+ qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc("import sys; sys.exit(0)")
proc.start(*argv)
@@ -267,8 +266,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.waitSignals([proc.started, proc.finished], timeout=10000,
- order='strict'):
+ with qtbot.waitSignal(proc.started, timeout=10000), \
+ qtbot.waitSignal(proc.finished, timeout=10000):
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 dd4a5cc40..28c570586 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.waitSignals(signals, order='strict'):
+ with qtbot.waitSignal(ipc_server.got_invalid_data), \
+ qtbot.waitSignal(connected_socket.disconnected):
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.waitSignals([ipc_server.got_args, ipc_server.got_args],
- order='strict'):
+ with qtbot.waitSignal(ipc_server.got_args), \
+ qtbot.waitSignal(ipc_server.got_args):
connected_socket.write(data.encode('utf-8'))
assert len(spy) == 2
diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py
index c398118cb..b64b8e0fe 100644
--- a/tests/unit/utils/test_jinja.py
+++ b/tests/unit/utils/test_jinja.py
@@ -115,7 +115,7 @@ def test_not_found(caplog):
def test_utf8():
- """Test rendering with an UTF8 template.
+ """Test rendering with a UTF8 template.
This was an attempt to get a failing test case for #127 but it seems
the issue is elsewhere.
diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py
index 79504e9b2..8292a09ad 100644
--- a/tests/unit/utils/test_urlmatch.py
+++ b/tests/unit/utils/test_urlmatch.py
@@ -606,9 +606,9 @@ URL_TEXT = hst.text(alphabet=string.ascii_letters)
@hypothesis.given(pattern=hst.builds(
lambda *a: ''.join(a),
# Scheme
- hst.one_of(hst.just('*'), hst.just('http'), hst.just('file')),
+ hst.sampled_from(['*', 'http', 'file']),
# Separator
- hst.one_of(hst.just(':'), hst.just('://')),
+ hst.sampled_from([':', '://']),
# Host
hst.one_of(hst.just('*'),
hst.builds(lambda *a: ''.join(a), hst.just('*.'), URL_TEXT),
diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py
index 04f7f04e6..7fd52152c 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -781,6 +781,8 @@ class TestParseJavascriptUrl:
@pytest.mark.parametrize('url, source', [
(QUrl('javascript:"hello" %0a "world"'), '"hello" \n "world"'),
+ (QUrl('javascript:/'), '/'),
+ (QUrl('javascript:///'), '///'),
# https://github.com/web-platform-tests/wpt/blob/master/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-query-fragment-components.html
(QUrl('javascript:"nope" ? "yep" : "what";'), '"nope" ? "yep" : "what";'),
(QUrl('javascript:"wrong"; // # %0a "ok";'), '"wrong"; // # \n "ok";'),
diff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py
index 59c3f7d43..014ef7f0c 100644
--- a/tests/unit/utils/usertypes/test_question.py
+++ b/tests/unit/utils/usertypes/test_question.py
@@ -53,23 +53,25 @@ def test_done(mode, answer, signal_names, question, qtbot):
question.mode = mode
question.answer = answer
signals = [getattr(question, name) for name in signal_names]
- with qtbot.waitSignals(signals, order='strict'):
- question.done()
+ blockers = [qtbot.waitSignal(signal) for signal in signals]
+
+ question.done()
+ for blocker in blockers:
+ blocker.wait()
+
assert not question.is_aborted
def test_cancel(question, qtbot):
"""Test Question.cancel()."""
- with qtbot.waitSignals([question.cancelled, question.completed],
- order='strict'):
+ with qtbot.waitSignal(question.cancelled), qtbot.waitSignal(question.completed):
question.cancel()
assert not question.is_aborted
def test_abort(question, qtbot):
"""Test Question.abort()."""
- with qtbot.waitSignals([question.aborted, question.completed],
- order='strict'):
+ with qtbot.waitSignal(question.aborted), qtbot.waitSignal(question.completed):
question.abort()
assert question.is_aborted
diff --git a/tox.ini b/tox.ini
index 369d4afe6..a25374672 100644
--- a/tox.ini
+++ b/tox.ini
@@ -224,3 +224,17 @@ deps =
-r{toxinidir}/misc/requirements/requirements-sphinx.txt
commands =
{envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/
+
+[testenv:build-release]
+basepython = {env:PYTHON:python3}
+pip_version = pip
+passenv = *
+usedevelop = true
+deps =
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/misc/requirements/requirements-tox.txt
+ -r{toxinidir}/misc/requirements/requirements-pyqt.txt
+ -r{toxinidir}/misc/requirements/requirements-dev.txt
+ -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt
+commands =
+ {envpython} {toxinidir}/scripts/dev/build_release.py {posargs}