diff options
64 files changed, 788 insertions, 285 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e1e31afc5..cf1c019f7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.3.1 +current_version = 2.4.0 commit = True message = Release v{new_version} tag = True diff --git a/.github/workflows/bleeding.yml b/.github/workflows/bleeding.yml index 766f535d7..435141e56 100644 --- a/.github/workflows/bleeding.yml +++ b/.github/workflows/bleeding.yml @@ -58,7 +58,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - name: Get asciidoc uses: actions/checkout@v2 with: diff --git a/.gitignore b/.gitignore index 31c4ca3b7..ccfc12ccb 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ TODO /scripts/testbrowser/cpp/webengine/testbrowser /scripts/testbrowser/cpp/webengine/.qmake.stash /scripts/dev/pylint_checkers/qute_pylint.egg-info +/scripts/dev/pylint_checkers/build /misc/file_version_info.txt /doc/extapi/_build /misc/nsis/include diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index c17f35eec..aed128a51 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,10 +15,65 @@ breaking changes (such as renamed commands) can happen in minor releases. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. +[[v2.5.0]] +v2.5.0 (unreleased) +------------------- + +Changed +~~~~~~~ + +- Improved message if a spawned process wasn't found and a Flatpak container is + in use. +- The `:tab-move` command now takes `start` and `end` as `index` to move a tab + to the first/last position. +- Tests now automatically pick the backend (QtWebKit/QtWebEngine) based on + what's available. The `QUTE_BDD_WEBENGINE` environment variable and + `--qute-bdd-webengine` argument got replaced by `QUTE_TESTS_BACKEND` and + `--qute-backend` respectively, which can be set to either `webengine` or + `webkit`. +- Using `:tab-give` or `:tab-take` on the last tab in a window now always + closes that window, no matter what `tabs.last_close` is set to. +- Redesigned `qute://settings` (`:set`) page with buttons for options with + fixed values. + +Added +~~~~~ + +- New `input.match_counts` option which allows to turn off count matching for + more emacs-like bindings. + +Fixed +~~~~~ + +- When `search.incremental` is disabled, searching using `/text` followed by a + backwards search via `?text` (or vice-versa) now correctly changes the search + direction. + +[[v2.4.1]] +v2.4.1 (unreleased) +------------------- + +Fixed +~~~~~ + +- Speculative fix for an immediate crash at start with the macOS/Windows + binaries (in certain rare environments). +- Speculative fix for a qutebrowser crash when the notification daemon crashes + while showing the notification. +- Fix crash when using `:screenshot` with an invalid `--rect` argument. + [[v2.4.0]] -v2.4.0 (unreleased) +v2.4.0 (2021-10-21) ------------------- +Security +~~~~~~~~ + +- **CVE-2021-41146**: Fix arbitrary command execution on Windows via URL handler + argument injection. See the + https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-vw27-fwjf-5qxm[security advisory] + for details. + Added ~~~~~ diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 8c11e15cc..442c136a7 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1431,6 +1431,7 @@ If neither is given, move it to the first position. ==== positional arguments * +'index'+: `+` or `-` to move relative to the current tab by count, or a default of 1 space. A tab index to move to that index. + `start` and `end` to move to the start and the end. ==== count diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 23894ddc4..552145023 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -412,6 +412,7 @@ Pre-built colorschemes - https://github.com/dracula/qutebrowser-dracula-theme[Dracula] - https://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized] - https://github.com/morhetz/gruvbox[gruvbox]: https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[The-Compiler], https://gitlab.com/shaneyost/dots-popos-september-2020/-/blob/master/qutebrowser/config.py[Shane Yost] +- https://www.opencode.net/wakellor957/qb-breath/-/blob/main/qb-breath.py[Manjaro Breath-like] Avoiding flake8 errors ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 60c229078..4ca5c2517 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -269,6 +269,7 @@ |<<input.insert_mode.leave_on_load,input.insert_mode.leave_on_load>>|Leave insert mode when starting a new page load. |<<input.insert_mode.plugins,input.insert_mode.plugins>>|Switch to insert mode when clicking flash and other plugins. |<<input.links_included_in_focus_chain,input.links_included_in_focus_chain>>|Include hyperlinks in the keyboard focus chain when tabbing. +|<<input.match_counts,input.match_counts>>|Interpret number prefixes as counts for bindings. |<<input.media_keys,input.media_keys>>|Whether the underlying Chromium should handle media keys. |<<input.mouse.back_forward_buttons,input.mouse.back_forward_buttons>>|Enable back and forward buttons on the mouse. |<<input.mouse.rocker_gestures,input.mouse.rocker_gestures>>|Enable Opera-like mouse rocker gestures. @@ -3557,6 +3558,15 @@ Type: <<types,Bool>> Default: +pass:[true]+ +[[input.match_counts]] +=== input.match_counts +Interpret number prefixes as counts for bindings. +This enables for vi-like bindings that can be prefixed with a number to indicate a count. Disabling it allows for emacs-like bindings where number keys are passed through (according to `input.forward_unbound_keys`) instead. + +Type: <<types,Bool>> + +Default: +pass:[true]+ + [[input.media_keys]] === input.media_keys Whether the underlying Chromium should handle media keys. diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 8db231add..bc312f108 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -65,6 +65,9 @@ show it. *--desktop-file-name* 'DESKTOP_FILE_NAME':: Set the base name of the desktop entry for this application. Used to set the app_id under Wayland. See https://doc.qt.io/qt-5/qguiapplication.html#desktopFileName-prop +*--untrusted-args*:: + Mark all following arguments as untrusted, which enforces that they are URLs/search terms (and not flags or commands) + === debug arguments *-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}':: Override the configured console loglevel diff --git a/misc/nsis/install.nsh b/misc/nsis/install.nsh index f29a0a9a8..9f0cdf446 100755 --- a/misc/nsis/install.nsh +++ b/misc/nsis/install.nsh @@ -351,13 +351,12 @@ Section "Register with Windows" SectionWindowsRegister !insertmacro UpdateRegDWORD SHCTX "SOFTWARE\Classes\$2" "EditFlags" 0x00000002 !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\DefaultIcon" "" "$1,0" !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell" "" "open" - !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\command" "" "$\"$1$\" $\"%1$\"" + !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\command" "" "$\"$1$\" --untrusted-args $\"%1$\"" !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\ddeexec" "" "" StrCmp $2 "${PRODUCT_NAME}HTML" 0 +4 StrCpy $2 "${PRODUCT_NAME}URL" StrCpy $3 "${PRODUCT_NAME} URL" Goto WriteRegHandler - !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2" "URL Protocol" "" ${endif} SectionEnd diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml index 7c382cbb3..9930514d0 100644 --- a/misc/org.qutebrowser.qutebrowser.appdata.xml +++ b/misc/org.qutebrowser.qutebrowser.appdata.xml @@ -44,6 +44,7 @@ </content_rating> <releases> <!-- Add new releases here --> +<release version="2.4.0" date="2021-10-21"/> <release version="2.3.1" date="2021-07-28"/> <release version="2.3.0" date="2021-06-28"/> <release version="2.2.3" date="2021-06-01"/> diff --git a/misc/org.qutebrowser.qutebrowser.desktop b/misc/org.qutebrowser.qutebrowser.desktop index 52144b3c5..d999496ee 100644 --- a/misc/org.qutebrowser.qutebrowser.desktop +++ b/misc/org.qutebrowser.qutebrowser.desktop @@ -45,7 +45,7 @@ Comment[it]= Un browser web vim-like utilizzabile da tastiera basato su PyQt5 Icon=qutebrowser Type=Application Categories=Network;WebBrowser; -Exec=qutebrowser %u +Exec=qutebrowser --untrusted-args %u Terminal=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; diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index 9a783f8b2..c61218ba3 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -2,8 +2,8 @@ build==0.7.0 check-manifest==0.47 -packaging==21.0 -pep517==0.11.0 -pyparsing==2.4.7 +packaging==21.3 +pep517==0.12.0 +pyparsing==3.0.6 toml==0.10.2 -tomli==1.2.1 +tomli==1.2.2 diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 000ed39aa..0dd45cebc 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -2,25 +2,25 @@ bump2version==1.0.1 certifi==2021.10.8 -cffi==1.14.6 -charset-normalizer==2.0.6 -cryptography==35.0.0 +cffi==1.15.0 +charset-normalizer==2.0.8 +cryptography==36.0.0 Deprecated==1.2.13 -github3.py==2.0.0 +github3.py==3.0.0 hunter==3.3.8 -idna==3.2 +idna==3.3 jwcrypto==1.0 manhole==1.8.0 -packaging==21.0 -pycparser==2.20 +packaging==21.3 +pycparser==2.21 Pympler==0.9 -pyparsing==2.4.7 -PyQt-builder==1.11.0 +pyparsing==3.0.6 +PyQt-builder==1.12.2 python-dateutil==2.8.2 requests==2.26.0 -sip==6.2.0 +sip==6.4.0 six==1.16.0 toml==0.10.2 -uritemplate==4.0.0 +uritemplate==4.1.1 # urllib3==1.26.7 -wrapt==1.13.1 +wrapt==1.13.3 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 9d5c0e170..1d1f5eebc 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -1,11 +1,10 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py attrs==21.2.0 -cached-property==1.5.2 -flake8==4.0.0 -flake8-bugbear==21.9.2 +flake8==4.0.1 +flake8-bugbear==21.11.28 flake8-builtins==1.5.3 -flake8-comprehensions==2.3.0 +flake8-comprehensions==3.7.0 flake8-copyright==0.2.2 flake8-debugger==4.0.0 flake8-deprecated==1.3 @@ -14,7 +13,7 @@ flake8-future-import==0.4.6 flake8-mock==0.3 flake8-polyfill==1.0.2 flake8-string-format==0.3.0 -flake8-tidy-imports==3.0.0 +flake8-tidy-imports==4.5.0 flake8-tuple==0.4.1 mccabe==0.6.1 pep8-naming==0.12.1 @@ -22,4 +21,4 @@ pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 six==1.16.0 -snowballstemmer==2.1.0 +snowballstemmer==2.2.0 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index e3a05eac7..ce64972b3 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -2,12 +2,12 @@ chardet==4.0.0 diff-cover==6.4.2 -importlib-metadata==4.8.1 -importlib-resources==5.2.2 +importlib-metadata==4.8.2 +importlib-resources==5.4.0 inflect==5.3.0 -Jinja2==3.0.2 +Jinja2==3.0.3 jinja2-pluralize==0.3.0 -lxml==4.6.3 +lxml==4.6.4 MarkupSafe==2.0.1 mypy==0.910 mypy-extensions==0.4.3 @@ -15,7 +15,7 @@ pluggy==1.0.0 Pygments==2.10.0 PyQt5-stubs==5.15.2.0 toml==0.10.2 -types-dataclasses==0.1.7 -types-PyYAML==5.4.10 -typing-extensions==3.10.0.2 +types-dataclasses==0.6.1 +types-PyYAML==6.0.1 +typing_extensions==4.0.0 zipp==3.6.0 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 81b66393b..9a53c11cd 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.2 -pyinstaller==4.5.1 +pyinstaller==4.7 pyinstaller-hooks-contrib==2021.3 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 9dc56ea29..c26af6406 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -2,25 +2,25 @@ astroid==2.3.3 # rq.filter: < 2.4 certifi==2021.10.8 -cffi==1.14.6 -charset-normalizer==2.0.6 -cryptography==35.0.0 +cffi==1.15.0 +charset-normalizer==2.0.8 +cryptography==36.0.0 Deprecated==1.2.13 future==0.18.2 -github3.py==2.0.0 -idna==3.2 +github3.py==3.0.0 +idna==3.3 isort==4.3.21 jwcrypto==1.0 lazy-object-proxy==1.4.3 mccabe==0.6.1 pefile==2021.9.3 -pycparser==2.20 +pycparser==2.21 pylint==2.4.4 # rq.filter: < 2.5 python-dateutil==2.8.2 ./scripts/dev/pylint_checkers requests==2.26.0 six==1.16.0 typed-ast==1.4.3 ; python_version<"3.8" -uritemplate==4.0.0 +uritemplate==4.1.1 # urllib3==1.26.7 wrapt==1.11.2 diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt index 8b7a53c44..3a3110c8b 100644 --- a/misc/requirements/requirements-pyqt-5.15.txt +++ b/misc/requirements/requirements-pyqt-5.15.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.15.4 # rq.filter: < 5.16 +PyQt5==5.15.6 # rq.filter: < 5.16 PyQt5-Qt5==5.15.2 PyQt5-sip==12.9.0 -PyQtWebEngine==5.15.4 # rq.filter: < 5.16 +PyQtWebEngine==5.15.5 # rq.filter: < 5.16 PyQtWebEngine-Qt5==5.15.2 diff --git a/misc/requirements/requirements-pyqt-pyinstaller.txt b/misc/requirements/requirements-pyqt-pyinstaller.txt deleted file mode 100644 index 678a1d7ea..000000000 --- a/misc/requirements/requirements-pyqt-pyinstaller.txt +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically generated by scripts/dev/recompile_requirements.py - -PyQt5==5.15.3 -PyQt5-Qt==5.15.2 -PyQt5-sip==12.9.0 -PyQtWebEngine==5.15.3 -PyQtWebEngine-Qt==5.15.2 diff --git a/misc/requirements/requirements-pyqt-pyinstaller.txt-raw b/misc/requirements/requirements-pyqt-pyinstaller.txt-raw deleted file mode 100644 index 89b5644da..000000000 --- a/misc/requirements/requirements-pyqt-pyinstaller.txt-raw +++ /dev/null @@ -1,2 +0,0 @@ -PyQt5==5.15.3 -PyQtWebEngine==5.15.3 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 75ef27bf4..3953d27b3 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.15.4 +PyQt5==5.15.6 PyQt5-Qt5==5.15.2 PyQt5-sip==12.9.0 -PyQtWebEngine==5.15.4 +PyQtWebEngine==5.15.5 PyQtWebEngine-Qt5==5.15.2 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index 82a00016c..a76402053 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -1,9 +1,9 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py certifi==2021.10.8 -charset-normalizer==2.0.6 -docutils==0.17.1 -idna==3.2 +charset-normalizer==2.0.8 +docutils==0.18.1 +idna==3.3 Pygments==2.10.0 pyroma==3.2 requests==2.26.0 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index fb01ec30c..b7f013853 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -3,19 +3,19 @@ alabaster==0.7.12 Babel==2.9.1 certifi==2021.10.8 -charset-normalizer==2.0.6 +charset-normalizer==2.0.8 docutils==0.17.1 -idna==3.2 -imagesize==1.2.0 -Jinja2==3.0.2 +idna==3.3 +imagesize==1.3.0 +Jinja2==3.0.3 MarkupSafe==2.0.1 -packaging==21.0 +packaging==21.3 Pygments==2.10.0 -pyparsing==2.4.7 +pyparsing==3.0.6 pytz==2021.3 requests==2.26.0 -snowballstemmer==2.1.0 -Sphinx==4.2.0 +snowballstemmer==2.2.0 +Sphinx==4.3.1 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==2.0.0 diff --git a/misc/requirements/requirements-tests-bleeding.txt b/misc/requirements/requirements-tests-bleeding.txt index 49911c48d..d2a7fcfb6 100644 --- a/misc/requirements/requirements-tests-bleeding.txt +++ b/misc/requirements/requirements-tests-bleeding.txt @@ -9,7 +9,7 @@ git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-py git+https://github.com/pytest-dev/pytest.git # Problematic: https://github.com/pytest-dev/pytest-bdd/issues/447 # git+https://github.com/pytest-dev/pytest-bdd.git -pytest-bdd +pytest-bdd<5 git+https://github.com/ionelmc/pytest-benchmark.git git+https://github.com/pytest-dev/pytest-instafail.git git+https://github.com/pytest-dev/pytest-mock.git diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 83379d700..b15a23c08 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -3,36 +3,36 @@ attrs==21.2.0 beautifulsoup4==4.10.0 certifi==2021.10.8 -charset-normalizer==2.0.6 +charset-normalizer==2.0.8 cheroot==8.5.2 click==8.0.3 -coverage==6.0.1 +coverage==6.2 EasyProcess==0.3 execnet==1.9.0 -filelock==3.3.0 +filelock==3.4.0 Flask==2.0.2 glob2==0.7 hunter==3.3.8 -hypothesis==6.23.2 +hypothesis==6.28.1 icdiff==2.0.4 -idna==3.2 +idna==3.3 iniconfig==1.1.1 itsdangerous==2.0.1 -jaraco.functools==3.3.0 -# Jinja2==3.0.2 -Mako==1.1.5 +jaraco.functools==3.4.0 +# Jinja2==3.0.3 +Mako==1.1.6 manhole==1.8.0 # MarkupSafe==2.0.1 -more-itertools==8.10.0 -packaging==21.0 +more-itertools==8.12.0 +packaging==21.3 parse==1.19.0 parse-type==0.5.2 pluggy==1.0.0 pprintpp==0.4.0 -py==1.10.0 +py==1.11.0 py-cpuinfo==8.0.0 Pygments==2.10.0 -pyparsing==2.4.7 +pyparsing==3.0.6 pytest==6.2.5 pytest-bdd==4.1.0 pytest-benchmark==3.4.1 @@ -51,10 +51,10 @@ requests==2.26.0 requests-file==1.5.1 six==1.16.0 sortedcontainers==2.4.0 -soupsieve==2.2.1 +soupsieve==2.3.1 tldextract==3.1.2 toml==0.10.2 -tomli==1.2.1 +tomli==1.2.2 urllib3==1.26.7 vulture==2.3 Werkzeug==2.0.2 diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw index ab580ac4b..5586a86ef 100644 --- a/misc/requirements/requirements-tests.txt-raw +++ b/misc/requirements/requirements-tests.txt-raw @@ -4,7 +4,8 @@ coverage Flask hypothesis pytest -pytest-bdd +# https://github.com/pytest-dev/pytest-bdd/issues/447 +pytest-bdd<5 pytest-benchmark pytest-instafail pytest-mock diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 4c1cfbe27..a2a57808b 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -1,17 +1,17 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -backports.entry-points-selectable==1.1.0 +backports.entry-points-selectable==1.1.1 distlib==0.3.3 -filelock==3.3.0 -packaging==21.0 -pip==21.2.4 +filelock==3.4.0 +packaging==21.3 +pip==21.3.1 platformdirs==2.4.0 pluggy==1.0.0 -py==1.10.0 -pyparsing==2.4.7 -setuptools==58.2.0 +py==1.11.0 +pyparsing==3.0.6 +setuptools==59.4.0 six==1.16.0 toml==0.10.2 tox==3.24.4 -virtualenv==20.8.1 +virtualenv==20.10.0 wheel==0.37.0 diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt index 897184c74..12553f2b2 100644 --- a/misc/requirements/requirements-yamllint.txt +++ b/misc/requirements/requirements-yamllint.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py pathspec==0.9.0 -PyYAML==5.4.1 +PyYAML==6.0 yamllint==1.26.3 diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index c46253d41..3ea8fd9f6 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -241,7 +241,7 @@ pass_backend() { if $GPG "${GPG_OPTS[@]}" -d "$passfile" \ | grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null then - passfile="${passfile#$PREFIX}" + passfile="${passfile#"$PREFIX"}" passfile="${passfile#/}" files+=( "${passfile%.gpg}" ) fi @@ -250,7 +250,7 @@ pass_backend() { if ((match_filename)) ; then # add entries with matching filepath while read -r passfile ; do - passfile="${passfile#$PREFIX}" + passfile="${passfile#"$PREFIX"}" passfile="${passfile#/}" files+=( "${passfile%.gpg}" ) done < <(find -L "$PREFIX" -iname '*.gpg' | grep "$url") @@ -267,7 +267,7 @@ pass_backend() { else if [[ $line =~ $user_pattern ]] ; then # remove the matching prefix "user: " from the beginning of the line - username=${line#${BASH_REMATCH[0]}} + username=${line#"${BASH_REMATCH[0]}"} break fi fi diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 29a8e4836..c05215792 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2021 Florian Bruhin (The Compiler)" __license__ = "GPL" __maintainer__ = __author__ __email__ = "mail@qutebrowser.org" -__version__ = "2.3.1" +__version__ = "2.4.0" __version_info__ = tuple(int(part) for part in __version__.split('.')) __description__ = "A keyboard-driven, vim-like browser based on PyQt5." diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f3438aaa8..796bb2eb3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -451,7 +451,7 @@ class CommandDispatcher: self._open(tab.url(), tab=True) if not keep: - tabbed_browser.close_tab(tab, add_undo=False) + tabbed_browser.close_tab(tab, add_undo=False, transfer=True) @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('win_id', completion=miscmodels.window) @@ -500,7 +500,8 @@ class CommandDispatcher: tabbed_browser.tabopen(self._current_url()) if not keep: self._tabbed_browser.close_tab(self._current_widget(), - add_undo=False) + add_undo=False, + transfer=True) def _back_forward(self, tab, bg, window, count, forward, index=None): """Helper function for :back/:forward.""" @@ -1004,11 +1005,10 @@ class CommandDispatcher: raise cmdutils.CommandError("There's no tab with index {}!".format( index)) - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('index', choices=['+', '-']) - @cmdutils.argument('count', value=cmdutils.Value.count) - def tab_move(self, index: Union[str, int] = None, - count: int = None) -> None: + @cmdutils.register(instance="command-dispatcher", scope="window") + @cmdutils.argument("index", choices=["+", "-", "start", "end"]) + @cmdutils.argument("count", value=cmdutils.Value.count) + def tab_move(self, index: Union[str, int] = None, count: int = None) -> None: """Move the current tab according to the argument and [count]. If neither is given, move it to the first position. @@ -1017,24 +1017,29 @@ class CommandDispatcher: index: `+` or `-` to move relative to the current tab by count, or a default of 1 space. A tab index to move to that index. + `start` and `end` to move to the start and the end. count: If moving relatively: Offset. If moving absolutely: New position (default: 0). This overrides the index argument, if given. """ - if index in ['+', '-']: + if index in ["+", "-"]: # relative moving new_idx = self._current_index() delta = 1 if count is None else count - if index == '-': + if index == "-": new_idx -= delta - elif index == '+': # pragma: no branch + elif index == "+": # pragma: no branch new_idx += delta if config.val.tabs.wrap: new_idx %= self._count() else: # absolute moving - if count is not None: + if index == "start": + new_idx = 0 + elif index == "end": + new_idx = self._count() - 1 + elif count is not None: new_idx = count - 1 elif index is not None: assert isinstance(index, int) diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py index e40b3e736..f8e1a59b1 100644 --- a/qutebrowser/browser/webengine/notification.py +++ b/qutebrowser/browser/webengine/notification.py @@ -715,6 +715,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter): # https://github.com/KDE/plasma-workspace/blob/v5.21.4/libnotificationmanager/server_p.cpp#L227-L237 # Created too many similar notifications in quick succession "org.freedesktop.Notifications.Error.ExcessNotificationGeneration", + + # From https://crashes.qutebrowser.org/view/b8c9838a - probably when + # notification daemon crashes? + "org.freedesktop.DBus.Error.Spawn.ChildSignaled", } def __init__(self, parent: QObject = None) -> None: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index ace23d14a..926ccf133 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -200,6 +200,14 @@ class WebEngineSearch(browsertab.AbstractSearch): def _empty_flags(self): return QWebEnginePage.FindFlags(0) # type: ignore[call-overload] + def _args_to_flags(self, reverse, ignore_case): + flags = self._empty_flags() + if self._is_case_sensitive(ignore_case): + flags |= QWebEnginePage.FindCaseSensitively + if reverse: + flags |= QWebEnginePage.FindBackward + return flags + def connect_signals(self): self._wrap_handler.connect_signal(self._widget.page()) @@ -246,17 +254,14 @@ class WebEngineSearch(browsertab.AbstractSearch): # Don't go to next entry on duplicate search if self.text == text and self.search_displayed: log.webview.debug("Ignoring duplicate search request" - " for {}".format(text)) + " for {}, but resetting flags".format(text)) + self._flags = self._args_to_flags(reverse, ignore_case) return self.text = text - self._flags = self._empty_flags() + self._flags = self._args_to_flags(reverse, ignore_case) self._wrap_handler.reset_match_data() self._wrap_handler.flag_wrap = wrap - if self._is_case_sensitive(ignore_case): - self._flags |= QWebEnginePage.FindCaseSensitively - if reverse: - self._flags |= QWebEnginePage.FindBackward self._find(text, self._flags, result_cb, 'search') diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index df3491ec2..7a41b995c 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -108,6 +108,16 @@ class WebKitSearch(browsertab.AbstractSearch): def _empty_flags(self): return QWebPage.FindFlags(0) # type: ignore[call-overload] + def _args_to_flags(self, reverse, ignore_case, wrap): + flags = self._empty_flags() + if self._is_case_sensitive(ignore_case): + flags |= QWebPage.FindCaseSensitively + if reverse: + flags |= QWebPage.FindBackward + if wrap: + flags |= QWebPage.FindWrapsAroundDocument + return flags + def _call_cb(self, callback, found, text, flags, caller): """Call the given callback if it's non-None. @@ -150,7 +160,8 @@ class WebKitSearch(browsertab.AbstractSearch): # Don't go to next entry on duplicate search if self.text == text and self.search_displayed: log.webview.debug("Ignoring duplicate search request" - " for {}".format(text)) + " for {}, but resetting flags".format(text)) + self._flags = self._args_to_flags(reverse, ignore_case, wrap) return # Clear old search results, this is done automatically on QtWebEngine. @@ -158,13 +169,7 @@ class WebKitSearch(browsertab.AbstractSearch): self.text = text self.search_displayed = True - self._flags = self._empty_flags() - if self._is_case_sensitive(ignore_case): - self._flags |= QWebPage.FindCaseSensitively - if reverse: - self._flags |= QWebPage.FindBackward - if wrap: - self._flags |= QWebPage.FindWrapsAroundDocument + self._flags = self._args_to_flags(reverse, ignore_case, wrap) # We actually search *twice* - once to highlight everything, then again # to get a mark so we can navigate. found = self._widget.findText(text, self._flags) diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index 120806bfe..8eaae045f 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -183,7 +183,10 @@ def screenshot( raise cmdutils.CommandError( f"File {filename} already exists (use --force to overwrite)") - qrect = None if rect is None else utils.parse_rect(rect) + try: + qrect = None if rect is None else utils.parse_rect(rect) + except ValueError as e: + raise cmdutils.CommandError(str(e)) pic = tab.grab_pixmap(qrect) if pic is None: diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 8b924f9db..cd95124db 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -679,14 +679,14 @@ content.headers.user_agent: # Vim-protip: Place your cursor below this comment and run # :r!python scripts/dev/ua_fetch.py - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, - like Gecko) Chrome/90.0.4430.93 Safari/537.36" - - Chrome 90 Win10 + like Gecko) Chrome/92.0.4515.131 Safari/537.36" + - Chrome 92 Win10 - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 - (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36" - - Chrome 90 macOS + (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36" + - Chrome 92 macOS - - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like - Gecko) Chrome/90.0.4430.93 Safari/537.36" - - Chrome 90 Linux + Gecko) Chrome/92.0.4515.131 Safari/537.36" + - Chrome 92 Linux supports_pattern: true desc: | User agent to send. @@ -1794,6 +1794,17 @@ input.media_keys: On Linux, disabling this also disables Chromium's MPRIS integration. +input.match_counts: + default: true + type: Bool + desc: >- + Interpret number prefixes as counts for bindings. + + This enables for vi-like bindings that can be prefixed with a number to + indicate a count. + Disabling it allows for emacs-like bindings where number keys are passed + through (according to `input.forward_unbound_keys`) instead. + ## keyhint keyhint.blacklist: diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 7ae45023b..c7b619b3e 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -21,12 +21,11 @@ import pkgutil import types -import sys import pathlib import importlib import argparse import dataclasses -from typing import Callable, Iterator, List, Optional, Set, Tuple +from typing import Callable, Iterator, List, Optional, Tuple from PyQt5.QtCore import pyqtSlot @@ -95,18 +94,6 @@ def load_components(*, skip_hooks: bool = False) -> None: def walk_components() -> Iterator[ExtensionInfo]: """Yield ExtensionInfo objects for all modules.""" - if hasattr(sys, 'frozen'): - yield from _walk_pyinstaller() - else: - yield from _walk_normal() - - -def _on_walk_error(name: str) -> None: - raise ImportError("Failed to import {}".format(name)) - - -def _walk_normal() -> Iterator[ExtensionInfo]: - """Walk extensions when not using PyInstaller.""" for _finder, name, ispkg in pkgutil.walk_packages( # Only packages have a __path__ attribute, # but we're sure this is one. @@ -123,23 +110,6 @@ def _walk_normal() -> Iterator[ExtensionInfo]: yield ExtensionInfo(name=name) -def _walk_pyinstaller() -> Iterator[ExtensionInfo]: - """Walk extensions when using PyInstaller. - - See https://github.com/pyinstaller/pyinstaller/issues/1905 - - Inspired by: - https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py - """ - toc: Set[str] = set() - for importer in pkgutil.iter_importers('qutebrowser'): - if hasattr(importer, 'toc'): - toc |= importer.toc # type: ignore[union-attr] - for name in toc: - if name.startswith(components.__name__ + '.'): - yield ExtensionInfo(name=name) - - def _get_init_context() -> InitContext: """Get an InitContext object.""" return InitContext(data_dir=pathlib.Path(standarddir.data()), @@ -190,3 +160,7 @@ def _on_config_changed(changed_name: str) -> None: def init() -> None: config.instance.changed.connect(_on_config_changed) + + +def _on_walk_error(name: str) -> None: + raise ImportError("Failed to import {}".format(name)) diff --git a/qutebrowser/html/settings.html b/qutebrowser/html/settings.html index 44824eeac..dfbc5c168 100644 --- a/qutebrowser/html/settings.html +++ b/qutebrowser/html/settings.html @@ -13,22 +13,112 @@ var cset = function(option, value) { {% endblock %} {% block style %} -table { border: 1px solid grey; border-collapse: collapse; } -pre { margin: 2px; } -th, td { border: 1px solid grey; padding: 0px 5px; } -th { background: lightgrey; } -th pre { color: grey; text-align: left; } -input { width: 98%; } -.setting { width: 75%; } -.value { width: 25%; text-align: center; } -.noscript, .noscript-text { color:red; } -.noscript-text { margin-bottom: 5cm; } -.option_description { margin: .5ex 0; color: grey; font-size: 80%; font-style: italic; white-space: pre-line; } +table { + border-spacing: 10px; +} + +tbody tr:nth-child(odd) { + background: #eaf4fb; +} + +pre { + margin: 2px; +} + +th { + padding: 10px; + border-radius: 5px; + background: #a6dfff; + text-align: left; + font-weight: normal; + font-size: 1.5rem; + color: #084c88; +} + +td { + padding: 5px 5px; +} + +th pre { + color: grey; + text-align: left; +} + +input { + padding: 8px; + width: 98%; + box-sizing: border-box; + border-radius: 4px; + border: 1px solid #01cdd0; + font-size: 0.9rem; + font-family: DejaVu, serif; +} + +input:focus { + outline: none; + border: 2px solid #7a589ea6; +} + +input[type="radio"] { + position: absolute; /* Positions the radio button relative to the edges of its containing element */ + -webkit-appearance: none; /* Removes its native styling */ + width: min-content; + margin: 0; + border: none; +} + +label { + cursor: pointer; + margin-bottom: 2px; + padding: 5px 10px; + border-radius: 5px; + background-color: #dddddd; + color: #666666; +} + +input[type="radio"]:checked + label { + background-color: #a6dfff; + color: #084c88; +} + +.setting { + width: 60%; +} + +.value { + width: 25%; + text-align: center; +} + +.valid-value { + text-align: center; +} + +.noscript, .noscript-text { + color: red; +} + +.noscript-text { + margin-bottom: 5cm; +} + +.option-description { + margin: .5ex 0; + color: #635d5dcf; + font-size: 80%; + font-style: italic; + white-space: pre-line; +} + +.radio-button { + position: relative; /* The absolutely positioned element inside this tag (the radio button) gets positioned relative to it. */ + display: inline-flex; + margin: 3px 1px; +} {% endblock %} {% block content %} <noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript> -<header><h1>{{ title }}</h1></header> <table> <tr> <th>Setting</th> @@ -37,18 +127,36 @@ input { width: 98%; } {% for option in configdata.DATA.values()|sort(attribute='name') if not option.no_autoconfig %} <tr> <!-- FIXME: convert to string properly --> - <td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }}) + <td class="setting">{{ option.name }} {% if option.description %} - <p class="option_description">{{ option.description|e }}</p> + <p class="option-description">{{ option.description|e }}</p> {% endif %} </td> - <td class="value"> - <input type="text" - id="input-{{ option.name }}" - onblur="cset('{{ option.name }}', this.value)" - value="{{ confget(option.name) }}"> - </input> - </td> + {% if option.typ.valid_values is not none %} + <td class="valid-value"> + {% for value in option.typ.valid_values.values %} + <div class="radio-button"> + <input type="radio" id="input-{{ option.name }}-{{ loop.index0 }}" + name="{{ option.name }}" value="{{ value }}" + onclick="cset('{{ option.name }}', this.value)" + {% if confget(option.name) == value %} + checked + {% endif %}> + <label for="input-{{ option.name }}-{{ loop.index0 }}"> + {{ value }} + </label> + </div> + {% endfor %} + </td> + {% else %} + <td class="value"> + <input type="text" + id="input-{{ option.name }}" + onblur="cset('{{ option.name }}', this.value)" + value="{{ confget(option.name) }}"> + </input> + </td> + {% endif %} </tr> {% endfor %} </table> diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 7e688dab1..4db1d5d76 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -254,6 +254,9 @@ class BaseKeyParser(QObject): def _match_count(self, sequence: keyutils.KeySequence, dry_run: bool) -> bool: """Try to match a key as count.""" + if not config.val.input.match_counts: + return False + txt = str(sequence[-1]) # To account for sequences changed above. if (txt in string.digits and self._supports_count and not (not self._count and txt == '0')): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index e081284ee..8c6ac2424 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -406,15 +406,16 @@ class TabbedBrowser(QWidget): else: yes_action() - def close_tab(self, tab, *, add_undo=True, new_undo=True): + def close_tab(self, tab, *, add_undo=True, new_undo=True, transfer=False): """Close a tab. Args: tab: The QWebView to be closed. add_undo: Whether the tab close can be undone. new_undo: Whether the undo entry should be a new item in the stack. + transfer: Whether the tab is closing because it is moving to a new window. """ - if config.val.tabs.tabs_are_windows: + if config.val.tabs.tabs_are_windows or transfer: last_close = 'close' else: last_close = config.val.tabs.last_close diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index c4ff0bb85..f27b7acfe 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -111,17 +111,23 @@ def init_faulthandler(fileobj=sys.__stderr__): Args: fobj: An opened file object to write the traceback to. """ - if fileobj is None: + try: + faulthandler.enable(fileobj) + except (RuntimeError, AttributeError): # When run with pythonw.exe, sys.__stderr__ can be None: # https://docs.python.org/3/library/sys.html#sys.__stderr__ - # If we'd enable faulthandler in that case, we just get a weird - # exception, so we don't enable faulthandler if we have no stdout. + # + # With PyInstaller, it can be a NullWriter raising AttributeError on + # fileno: https://github.com/pyinstaller/pyinstaller/issues/4481 # # Later when we have our data dir available we re-enable faulthandler # to write to a file so we can display a crash to the user at the next # start. + # + # Note that we don't have any logging initialized yet at this point, so + # this is a silent error. return - faulthandler.enable(fileobj) + if (hasattr(faulthandler, 'register') and hasattr(signal, 'SIGUSR1') and sys.stderr is not None): # If available, we also want a traceback on SIGUSR1. diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index e5ccd1b8b..c93fad09b 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -27,7 +27,7 @@ from typing import Mapping, Sequence, Dict, Optional from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess, QProcessEnvironment, QByteArray, QUrl, Qt) -from qutebrowser.utils import message, log, utils, usertypes +from qutebrowser.utils import message, log, utils, usertypes, version from qutebrowser.api import cmdutils, apitypes from qutebrowser.completion.models import miscmodels @@ -273,7 +273,9 @@ class GUIProcess(QObject): known_errors = ['No such file or directory', 'Permission denied'] if (': ' in error_string and # pragma: no branch error_string.split(': ', maxsplit=1)[1] in known_errors): - msg += f'\n(Hint: Make sure {self.cmd!r} exists and is executable)' + msg += f'\nHint: Make sure {self.cmd!r} exists and is executable' + if version.is_flatpak(): + msg += ' inside the Flatpak container' message.error(msg) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index d0819f832..c576c4a06 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -87,6 +87,11 @@ def get_argparser(): help="Set the base name of the desktop entry for this " "application. Used to set the app_id under Wayland. See " "https://doc.qt.io/qt-5/qguiapplication.html#desktopFileName-prop") + parser.add_argument('--untrusted-args', + action='store_true', + help="Mark all following arguments as untrusted, which " + "enforces that they are URLs/search terms (and not flags or " + "commands)") parser.add_argument('--json-args', help=argparse.SUPPRESS) parser.add_argument('--temp-basedir-restarted', @@ -207,7 +212,27 @@ def _unpack_json_args(args): return argparse.Namespace(**new_args) +def _validate_untrusted_args(argv): + # NOTE: Do not use f-strings here, as this should run with older Python + # versions (so that a proper error can be displayed) + try: + untrusted_idx = argv.index('--untrusted-args') + except ValueError: + return + + rest = argv[untrusted_idx + 1:] + if len(rest) > 1: + sys.exit( + "Found multiple arguments ({}) after --untrusted-args, " + "aborting.".format(' '.join(rest))) + + for arg in rest: + if arg.startswith(('-', ':')): + sys.exit("Found {} after --untrusted-args, aborting.".format(arg)) + + def main(): + _validate_untrusted_args(sys.argv) parser = get_argparser() argv = sys.argv[1:] args = parser.parse_args(argv) diff --git a/qutebrowser/utils/resources.py b/qutebrowser/utils/resources.py index ff5ec9d9a..f561d6747 100644 --- a/qutebrowser/utils/resources.py +++ b/qutebrowser/utils/resources.py @@ -82,7 +82,7 @@ def _glob( else: # zipfile.Path or importlib_resources compat object # Unfortunately, we can't tell mypy about resource_path being of type # Union[pathlib.Path, zipfile.Path] because we set "python_version = 3.6" in - # .mypy.ini, but the zipfiel stubs (correctly) only declare zipfile.Path with + # .mypy.ini, but the zipfile stubs (correctly) only declare zipfile.Path with # Python 3.8... assert glob_path.is_dir(), glob_path # type: ignore[unreachable] for subpath in glob_path.iterdir(): diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index a56769255..f42515c5c 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -669,11 +669,12 @@ def yaml_load(f: Union[str, IO[str]]) -> Any: r"of from 'collections\.abc' is deprecated.*"): try: data = yaml.load(f, Loader=YamlLoader) - except ValueError as e: - if str(e).startswith('could not convert string to float'): + except ValueError as e: # pragma: no cover + pyyaml_error = 'could not convert string to float' + if str(e).startswith(pyyaml_error): # WORKAROUND for https://github.com/yaml/pyyaml/issues/168 raise yaml.YAMLError(e) - raise # pragma: no cover + raise end = datetime.datetime.now() diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 8cd244fca..3beb6fb83 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -773,8 +773,6 @@ def _backend() -> str: if objects.backend == usertypes.Backend.QtWebKit: return 'new QtWebKit (WebKit {})'.format(qWebKitVersion()) elif objects.backend == usertypes.Backend.QtWebEngine: - webengine = usertypes.Backend.QtWebEngine - assert objects.backend == webengine, objects.backend return str(qtwebengine_versions( avoid_init='avoid-chromium-init' in objects.debug_flags)) raise utils.Unreachable(objects.backend) diff --git a/requirements.txt b/requirements.txt index a158bdde6..e088ca805 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,11 @@ adblock==0.5.0 colorama==0.4.4 dataclasses==0.6 ; python_version<"3.7" -importlib-metadata==4.8.1 ; python_version<"3.8" -importlib-resources==5.2.2 ; python_version<"3.9" -Jinja2==3.0.2 +importlib-metadata==4.8.2 ; python_version<"3.8" +importlib-resources==5.4.0 ; python_version<"3.9" +Jinja2==3.0.3 MarkupSafe==2.0.1 Pygments==2.10.0 -PyYAML==5.4.1 -typing-extensions==3.10.0.2 +PyYAML==6.0 +typing_extensions==4.0.0 ; python_version<"3.8" zipp==3.6.0 diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index a1c6646eb..4961cbdc8 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -227,7 +227,7 @@ def patch_mac_app(): # Replace some duplicate files by symlinks framework_path = os.path.join(app_path, 'Contents', 'MacOS', 'PyQt5', - 'Qt', 'lib', 'QtWebEngineCore.framework') + 'Qt5', 'lib', 'QtWebEngineCore.framework') core_lib = os.path.join(framework_path, 'Versions', '5', 'QtWebEngineCore') os.remove(core_lib) diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index a4cd81ad4..1b9759eb8 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -98,7 +98,6 @@ CHANGELOG_URLS = { 'pep8-naming': 'https://github.com/PyCQA/pep8-naming/blob/master/CHANGELOG.rst', 'pycodestyle': 'https://github.com/PyCQA/pycodestyle/blob/master/CHANGES.txt', 'pyflakes': 'https://github.com/PyCQA/pyflakes/blob/master/NEWS.rst', - 'cached-property': 'https://github.com/pydanny/cached-property/blob/master/HISTORY.md', 'cffi': 'https://github.com/python-cffi/release-doc/blob/master/doc/source/whatsnew.rst', 'astroid': 'https://github.com/PyCQA/astroid/blob/2.4/ChangeLog', 'pytest-instafail': 'https://github.com/pytest-dev/pytest-instafail/blob/master/CHANGES.rst', @@ -134,7 +133,7 @@ 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/index.html#old-versions', + 'lxml': 'https://github.com/lxml/lxml/blob/master/CHANGES.txt', 'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master', 'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst', 'pep517': 'https://github.com/pypa/pep517/blob/master/doc/changelog.rst', @@ -142,10 +141,8 @@ CHANGELOG_URLS = { 'toml': 'https://github.com/uiri/toml/releases', 'tomli': 'https://github.com/hukkin/tomli/blob/master/CHANGELOG.md', 'PyQt5': 'https://www.riverbankcomputing.com/news', - 'PyQt5-Qt': 'https://www.riverbankcomputing.com/news', 'PyQt5-Qt5': 'https://www.riverbankcomputing.com/news', 'PyQtWebEngine': 'https://www.riverbankcomputing.com/news', - 'PyQtWebEngine-Qt': 'https://www.riverbankcomputing.com/news', 'PyQtWebEngine-Qt5': 'https://www.riverbankcomputing.com/news', 'PyQt-builder': 'https://www.riverbankcomputing.com/news', 'PyQt5-sip': 'https://www.riverbankcomputing.com/news', @@ -158,11 +155,11 @@ CHANGELOG_URLS = { 'cheroot': 'https://cheroot.cherrypy.org/en/latest/history.html', 'certifi': 'https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport', 'chardet': 'https://github.com/chardet/chardet/releases', - 'charset-normalizer': 'https://github.com/Ousret/charset_normalizer/commits/master', + 'charset-normalizer': 'https://github.com/Ousret/charset_normalizer/blob/master/CHANGELOG.md', 'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst', 'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md', 'backports.entry-points-selectable': 'https://github.com/jaraco/backports.entry_points_selectable/blob/main/CHANGES.rst', - 'typing-extensions': 'https://github.com/python/typing/commits/master/typing_extensions', + 'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions', 'diff-cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG', 'pytest-icdiff': 'https://github.com/hjwp/pytest-icdiff/blob/master/HISTORY.rst', 'icdiff': 'https://github.com/jeffkaufman/icdiff/blob/master/ChangeLog', @@ -171,7 +168,7 @@ CHANGELOG_URLS = { 'check-manifest': 'https://github.com/mgedmin/check-manifest/blob/master/CHANGES.rst', 'yamllint': 'https://github.com/adrienverge/yamllint/blob/master/CHANGELOG.rst', 'pathspec': 'https://github.com/cpburnz/python-path-specification/blob/master/CHANGES.rst', - 'filelock': 'https://github.com/tox-dev/py-filelock/commits/main', + 'filelock': 'https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst', 'github3.py': 'https://github3py.readthedocs.io/en/master/release-notes/index.html', 'manhole': 'https://github.com/ionelmc/python-manhole/blob/master/CHANGELOG.rst', 'pycparser': 'https://github.com/eliben/pycparser/blob/master/CHANGES', diff --git a/tests/conftest.py b/tests/conftest.py index 40631af34..26cc04345 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -214,20 +214,74 @@ def pytest_addoption(parser): help="Delay between qutebrowser commands.") parser.addoption('--qute-profile-subprocs', action='store_true', default=False, help="Run cProfile for subprocesses.") - parser.addoption('--qute-bdd-webengine', action='store_true', - help='Use QtWebEngine for BDD tests') + parser.addoption('--qute-backend', action='store', + choices=['webkit', 'webengine'], help='Set backend for BDD tests') def pytest_configure(config): - webengine_arg = config.getoption('--qute-bdd-webengine') - webengine_env = os.environ.get('QUTE_BDD_WEBENGINE', 'false') - config.webengine = webengine_arg or webengine_env == 'true' - # Fail early if QtWebEngine is not available - if config.webengine: - import PyQt5.QtWebEngineWidgets + backend = _select_backend(config) + config.webengine = backend == 'webengine' + earlyinit.configure_pyqt() +def _select_backend(config): + """Select the backend for running tests. + + The backend is auto-selected in the following manner: + 1. Use QtWebKit if available + 2. Otherwise use QtWebEngine as a fallback + + Auto-selection is overridden by either passing a backend via + `--qute-backend=<backend>` or setting the environment variable + `QUTE_TESTS_BACKEND=<backend>`. + + Args: + config: pytest config + + Raises: + ImportError if the selected backend is not available. + + Returns: + The selected backend as a string (e.g. 'webkit'). + """ + backend_arg = config.getoption('--qute-backend') + backend_env = os.environ.get('QUTE_TESTS_BACKEND') + + backend = backend_arg or backend_env or _auto_select_backend() + + # Fail early if selected backend is not available + if backend == 'webkit': + import PyQt5.QtWebKitWidgets + elif backend == 'webengine': + import PyQt5.QtWebEngineWidgets + else: + raise utils.Unreachable(backend) + + return backend + + +def _auto_select_backend(): + try: + # Try to use QtWebKit as the default backend + import PyQt5.QtWebKitWidgets + return 'webkit' + except ImportError: + # Try to use QtWebEngine as a fallback and fail early + # if that's also not available + import PyQt5.QtWebEngineWidgets + return 'webengine' + + +def pytest_report_header(config): + if config.webengine: + backend_version = version.qtwebengine_versions(avoid_init=True) + else: + backend_version = version.qWebKitVersion() + + return f'backend: {backend_version}' + + @pytest.fixture(scope='session', autouse=True) def check_display(request): if utils.is_linux and not os.environ.get('DISPLAY', ''): diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index a4a089cea..16170d460 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -165,7 +165,7 @@ if not getattr(sys, 'frozen', False): def pytest_collection_modifyitems(config, items): - """Apply @qtwebengine_* markers; skip unittests with QUTE_BDD_WEBENGINE.""" + """Apply @qtwebengine_* markers.""" # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884 # (note this isn't actually fixed properly before Qt 5.15) header_bug_fixed = qtutils.version_check('5.15', compiled=False) diff --git a/tests/end2end/data/hints/html/README.md b/tests/end2end/data/hints/html/README.md index 2a6e97c24..5bbaecb4a 100644 --- a/tests/end2end/data/hints/html/README.md +++ b/tests/end2end/data/hints/html/README.md @@ -3,3 +3,5 @@ Tests in this directory are automatically picked up by `test_hints` in They need to contain a special `<!-- target: foo.html -->` comment which specifies where the hint in it will point to, and will then test that. + +With `<!-- target: null -->`, the page is expected to not generate any hints. diff --git a/tests/end2end/data/hints/invisible.html b/tests/end2end/data/hints/html/invisible.html index b0bfa9dd9..d382c80fa 100644 --- a/tests/end2end/data/hints/invisible.html +++ b/tests/end2end/data/hints/html/invisible.html @@ -1,3 +1,5 @@ +<!-- target: null --> + <!DOCTYPE html> <html> diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index cf35c5356..47153b741 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -241,11 +241,6 @@ Feature: Using hints # The actual check is already done above Then "No elements found." should not be logged - Scenario: Hinting invisible elements - When I open data/hints/invisible.html - And I run :hint - Then the error "No elements found." should be shown - Scenario: Clicking input with existing text When I open data/hints/input.html And I run :click-element id qute-input-existing diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 5fafd19f0..305b45690 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -71,7 +71,25 @@ Feature: Searching on a page When I run :search foo And I wait for "search found foo" in the log And I run :search foo - Then "Ignoring duplicate search request for foo" should be logged + Then "Ignoring duplicate search request for foo, but resetting flags" should be logged + + Scenario: Reset search direction on duplicate search, forward-to-back + When I run :search baz + And I wait for "search found baz" in the log + And I run :search -r baz + And I wait for "Ignoring duplicate search request for baz, but resetting flags" in the log + And I run :search-next + And I wait for "next_result found baz with flags FindBackward" in the log + Then "BAZ" should be found + + Scenario: Reset search direction on duplicate search, back-to-forward + When I run :search -r baz + And I wait for "search found baz with flags FindBackward" in the log + And I run :search baz + And I wait for "Ignoring duplicate search request for baz, but resetting flags" in the log + And I run :search-next + And I wait for "next_result found baz" in the log + Then "baz" should be found ## search.ignore_case diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index c9d983755..3715d5765 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -633,6 +633,27 @@ Feature: Tab management - data/numbers/1.txt (active) - data/numbers/3.txt + Scenario: :tab-move with absolute position + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-focus 1 + And I run :tab-move end + Then the following tabs should be open: + - data/numbers/2.txt + - data/numbers/3.txt + - data/numbers/1.txt (active) + + Scenario: :tab-move with absolute position + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-move start + Then the following tabs should be open: + - data/numbers/3.txt (active) + - data/numbers/1.txt + - data/numbers/2.txt + Scenario: Make sure :tab-move retains metadata When I open data/title.html And I open data/hello.txt in a new tab @@ -1349,6 +1370,25 @@ Feature: Tab management And I run :tab-take 0/1 Then the error "Can't take tabs when using windows as tabs" should be shown + @windows_skip + Scenario: Close the last tab of a window when taken by another window + Given I have a fresh instance + When I open data/numbers/1.txt + And I run :tab-only + And I open data/numbers/2.txt in a new window + And I set tabs.last_close to ignore + And I run :tab-take 1/1 + And I wait until data/numbers/2.txt is loaded + Then the session should look like: + windows: + - tabs: + - history: + - url: about:blank + - url: http://localhost:*/data/numbers/1.txt + - active: true + history: + - url: http://localhost:*/data/numbers/2.txt + # :tab-give @xfail_norun # Needs qutewm @@ -1406,6 +1446,24 @@ Feature: Tab management And I run :tab-give 0 Then the error "Can't give tabs when using windows as tabs" should be shown + @windows_skip + Scenario: Close the last tab of a window when given to another window + Given I have a fresh instance + When I open data/numbers/1.txt + And I run :tab-only + And I open data/numbers/2.txt in a new window + And I set tabs.last_close to ignore + And I run :tab-give 1 + And I wait until data/numbers/1.txt is loaded + Then the session should look like: + windows: + - tabs: + - active: true + history: + - url: http://localhost:*/data/numbers/2.txt + - history: + - url: http://localhost:*/data/numbers/1.txt + # Other Scenario: Using :tab-next after closing last tab (#1448) diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py index ebb2a7e33..f1cda97fe 100644 --- a/tests/end2end/test_hints_html.py +++ b/tests/end2end/test_hints_html.py @@ -40,7 +40,7 @@ def collect_tests(): @dataclasses.dataclass class ParsedFile: - target: str + target: Optional[str] qtwebengine_todo: Optional[str] @@ -107,11 +107,18 @@ def test_hints(test_name, zoom_text_only, zoom_level, find_implementation, quteproc.set_setting('zoom.text_only', str(zoom_text_only)) quteproc.set_setting('hints.find_implementation', find_implementation) quteproc.send_cmd(':zoom {}'.format(zoom_level)) + # follow hint quteproc.send_cmd(':hint all normal') - quteproc.wait_for(message='hints: a', category='hints') - quteproc.send_cmd(':hint-follow a') - quteproc.wait_for_load_finished('data/' + parsed.target) + + if parsed.target is None: + msg = quteproc.wait_for(message='No elements found.', category='message') + msg.expected = True + else: + quteproc.wait_for(message='hints: a', category='hints') + quteproc.send_cmd(':hint-follow a') + quteproc.wait_for_load_finished('data/' + parsed.target) + # reset quteproc.send_cmd(':zoom 100') if not request.config.webengine: diff --git a/tests/unit/extensions/test_loader.py b/tests/unit/extensions/test_loader.py index feb5dd347..e9b8055aa 100644 --- a/tests/unit/extensions/test_loader.py +++ b/tests/unit/extensions/test_loader.py @@ -35,16 +35,10 @@ def test_on_walk_error(): def test_walk_normal(): - names = [info.name for info in loader._walk_normal()] + names = [info.name for info in loader.walk_components()] assert 'qutebrowser.components.scrollcommands' in names -def test_walk_pyinstaller(): - # We can't test whether we get something back without being frozen by - # PyInstaller, but at least we can test that we don't crash. - list(loader._walk_pyinstaller()) - - def test_load_component(monkeypatch): monkeypatch.setattr(objects, 'commands', {}) diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 30ee36301..84068bf47 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -346,3 +346,14 @@ def test_clear_keystring_empty(qtbot, keyparser): keyparser._sequence = keyseq('') with qtbot.assert_not_emitted(keyparser.keystring_updated): keyparser.clear_keystring() + + +def test_respect_config_when_matching_counts(keyparser, config_stub): + """Don't match counts if disabled in the config.""" + config_stub.val.input.match_counts = False + + info = keyutils.KeyInfo(Qt.Key_1, Qt.NoModifier) + keyparser.handle(info.to_event()) + + assert not keyparser._sequence + assert not keyparser._count diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py index be86bf215..faf2006de 100644 --- a/tests/unit/misc/test_guiprocess.py +++ b/tests/unit/misc/test_guiprocess.py @@ -26,7 +26,7 @@ import pytest from PyQt5.QtCore import QProcess, QUrl from qutebrowser.misc import guiprocess -from qutebrowser.utils import usertypes, utils +from qutebrowser.utils import usertypes, utils, version from qutebrowser.api import cmdutils from qutebrowser.qt import sip @@ -394,8 +394,11 @@ def test_running(qtbot, proc, py_proc): proc.outcome.was_successful() -def test_failing_to_start(qtbot, proc, caplog, message_mock): +@pytest.mark.parametrize('is_flatpak', [True, False]) +def test_failing_to_start(qtbot, proc, caplog, message_mock, monkeypatch, is_flatpak): """Test the process failing to start.""" + monkeypatch.setattr(version, 'is_flatpak', lambda: is_flatpak) + with caplog.at_level(logging.ERROR, 'message'): with qtbot.wait_signal(proc.error, timeout=5000): proc.start('this_does_not_exist_either', []) @@ -405,8 +408,11 @@ def test_failing_to_start(qtbot, proc, caplog, message_mock): "Testprocess 'this_does_not_exist_either' failed to start:") if not utils.is_windows: - assert msg.text.endswith( - "(Hint: Make sure 'this_does_not_exist_either' exists and is executable)") + expected_msg = ( + "Hint: Make sure 'this_does_not_exist_either' exists and is executable") + if is_flatpak: + expected_msg += ' inside the Flatpak container' + assert msg.text.endswith(expected_msg) assert not proc.outcome.running assert proc.outcome.status is None diff --git a/tests/unit/test_qutebrowser.py b/tests/unit/test_qutebrowser.py index d9275631d..36b4065a1 100644 --- a/tests/unit/test_qutebrowser.py +++ b/tests/unit/test_qutebrowser.py @@ -22,6 +22,8 @@ (Mainly commandline flag parsing) """ +import re + import pytest from qutebrowser import qutebrowser @@ -75,3 +77,61 @@ class TestJsonArgs: # pylint: disable=no-member assert args.debug assert not args.temp_basedir + + +class TestValidateUntrustedArgs: + + @pytest.mark.parametrize('args', [ + [], + [':nop'], + [':nop', '--untrusted-args'], + [':nop', '--debug', '--untrusted-args'], + [':nop', '--untrusted-args', 'foo'], + ['--debug', '--untrusted-args', 'foo'], + ['foo', '--untrusted-args', 'bar'], + ]) + def test_valid(self, args): + qutebrowser._validate_untrusted_args(args) + + @pytest.mark.parametrize('args, message', [ + ( + ['--untrusted-args', '--debug'], + "Found --debug after --untrusted-args, aborting.", + ), + ( + ['--untrusted-args', ':nop'], + "Found :nop after --untrusted-args, aborting.", + ), + ( + ['--debug', '--untrusted-args', '--debug'], + "Found --debug after --untrusted-args, aborting.", + ), + ( + [':nop', '--untrusted-args', '--debug'], + "Found --debug after --untrusted-args, aborting.", + ), + ( + [':nop', '--untrusted-args', ':nop'], + "Found :nop after --untrusted-args, aborting.", + ), + ( + [ + ':nop', + '--untrusted-args', + ':nop', + '--untrusted-args', + 'https://www.example.org', + ], + ( + "Found multiple arguments (:nop --untrusted-args " + "https://www.example.org) after --untrusted-args, aborting." + ) + ), + ( + ['--untrusted-args', 'okay1', 'okay2'], + "Found multiple arguments (okay1 okay2) after --untrusted-args, aborting.", + ), + ]) + def test_invalid(self, args, message): + with pytest.raises(SystemExit, match=re.escape(message)): + qutebrowser._validate_untrusted_args(args) diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py index 35ccc94fe..caf52c76d 100644 --- a/tests/unit/utils/test_urlmatch.py +++ b/tests/unit/utils/test_urlmatch.py @@ -37,24 +37,30 @@ from PyQt5.QtCore import QUrl from qutebrowser.utils import urlmatch +# pylint: disable=line-too-long + @pytest.mark.parametrize('pattern, error', [ ### Chromium: kMissingSchemeSeparator ## TEST(ExtensionURLPatternTest, ParseInvalid) # ("http", "No scheme given"), - ("http:", "Invalid port: Port is empty"), - ("http:/", "Invalid port: Port is empty"), - ("about://", "Pattern without path"), - ("http:/bar", "Invalid port: Port is empty"), + pytest.param("http:", "Invalid port: Port is empty", id='scheme-no-slash'), + pytest.param("http:/", "Invalid port: Port is empty", id='scheme-single-slash'), + pytest.param("about://", "Pattern without path", id='scheme-no-path'), + pytest.param( + "http:/bar", + "Invalid port: Port is empty", + id='scheme-single-slash-path', + ), ### Chromium: kEmptyHost ## TEST(ExtensionURLPatternTest, ParseInvalid) - ("http://", "Pattern without host"), - ("http:///", "Pattern without host"), - ("http://:1234/", "Pattern without host"), - ("http://*./", "Pattern without host"), + pytest.param("http://", "Pattern without host", id='host-double-slash'), + pytest.param("http:///", "Pattern without host", id='host-triple-slash'), + pytest.param("http://:1234/", "Pattern without host", id='host-port'), + pytest.param("http://*./", "Pattern without host", id='host-pattern'), ## TEST(ExtensionURLPatternTest, IPv6Patterns) - ("http://[]:8888/*", "Pattern without host"), + pytest.param("http://[]:8888/*", "Pattern without host", id='host-ipv6'), ### Chromium: kEmptyPath ## TEST(ExtensionURLPatternTest, ParseInvalid) @@ -63,53 +69,132 @@ from qutebrowser.utils import urlmatch ### Chromium: kInvalidHost ## TEST(ExtensionURLPatternTest, ParseInvalid) - ("http://\0www/", "May not contain NUL byte"), + pytest.param("http://\0www/", "May not contain NUL byte", id='host-nul'), ## TEST(ExtensionURLPatternTest, IPv6Patterns) # No closing bracket (`]`). - ("http://[2607:f8b0:4005:805::200e/*", "Invalid IPv6 URL"), + pytest.param( + "http://[2607:f8b0:4005:805::200e/*", + "Invalid IPv6 URL", + id='host-ipv6-no-closing', + ), # Two closing brackets (`]]`). - pytest.param("http://[2607:f8b0:4005:805::200e]]/*", "Invalid IPv6 URL", marks=pytest.mark.xfail(reason="https://bugs.python.org/issue34360")), + pytest.param( + "http://[2607:f8b0:4005:805::200e]]/*", + "Invalid IPv6 URL", + marks=pytest.mark.xfail(reason="https://bugs.python.org/issue34360"), + id='host-ipv6-two-closing', + ), # Two open brackets (`[[`). - ("http://[[2607:f8b0:4005:805::200e]/*", r"""Expected '\]' to match '\[' in hostname; source was "\[2607:f8b0:4005:805::200e"; host = """""), + pytest.param( + "http://[[2607:f8b0:4005:805::200e]/*", + r"""Expected '\]' to match '\[' in hostname; source was "\[2607:f8b0:4005:805::200e"; host = """"", + id='host-ipv6-two-open', + ), # Too few colons in the last chunk. - ("http://[2607:f8b0:4005:805:200e]/*", 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e"; host = ""'), + pytest.param( + "http://[2607:f8b0:4005:805:200e]/*", + 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e"; host = ""', + id='host-ipv6-colons', + ), # Non-hex piece. - ("http://[2607:f8b0:4005:805:200e:12:bogus]/*", 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e:12:bogus"; host = ""'), + pytest.param( + "http://[2607:f8b0:4005:805:200e:12:bogus]/*", + 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e:12:bogus"; host = ""', + id='host-ipv6-non-hex', + ), ### Chromium: kInvalidHostWildcard ## TEST(ExtensionURLPatternTest, ParseInvalid) - ("http://*foo/bar", "Invalid host wildcard"), - ("http://foo.*.bar/baz", "Invalid host wildcard"), - ("http://fo.*.ba:123/baz", "Invalid host wildcard"), - ("http://foo.*/bar", "Invalid host wildcard"), + pytest.param("http://*foo/bar", "Invalid host wildcard", id='host-wildcard-no-dot'), + pytest.param( + "http://foo.*.bar/baz", + "Invalid host wildcard", + id='host-wildcard-middle', + ), + pytest.param( + "http://fo.*.ba:123/baz", + "Invalid host wildcard", + id='host-wildcard-middle-port', + ), + pytest.param("http://foo.*/bar", "Invalid host wildcard", id='host-wildcard-end'), ### Chromium: kInvalidPort ## TEST(ExtensionURLPatternTest, Ports) - ("http://foo:/", "Invalid port: Port is empty"), - ("http://*.foo:/", "Invalid port: Port is empty"), - ("http://foo:com/", "Invalid port: .* 'com'"), - ("http://foo:123456/", "Invalid port: Port out of range 0-65535"), - ("http://foo:80:80/monkey", "Invalid port: .* '80:80'"), - ("chrome://foo:1234/bar", "Ports are unsupported with chrome scheme"), + pytest.param("http://foo:/", "Invalid port: Port is empty", id='port-empty'), + pytest.param( + "http://*.foo:/", + "Invalid port: Port is empty", + id='port-empty-wildcard', + ), + pytest.param("http://foo:com/", "Invalid port: .* 'com'", id='port-alpha'), + pytest.param( + "http://foo:123456/", + "Invalid port: Port out of range 0-65535", + id='port-range', + ), + pytest.param( + "http://foo:80:80/monkey", + "Invalid port: .* '80:80'", + id='port-double', + ), + pytest.param( + "chrome://foo:1234/bar", + "Ports are unsupported with chrome scheme", + id='port-chrome', + ), # No port specified, but port separator. - ("http://[2607:f8b0:4005:805::200e]:/*", "Invalid port: Port is empty"), + pytest.param( + "http://[2607:f8b0:4005:805::200e]:/*", + "Invalid port: Port is empty", + id='port-empty-ipv6', + ), ### Additional tests - ("http://[", "Invalid IPv6 URL"), - ("http://[fc2e::bb88::edac]", 'Invalid IPv6 address; source was "fc2e::bb88::edac"; host = ""'), - ("http://[fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac"; host = ""'), - ("http://[fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac"; host = ""'), - ("http://[127.0.0.1:fc2e::bb88:edac]", r'Invalid IPv6 address; source was "127\.0\.0\.1:fc2e::bb88:edac'), - ("http://[fc2e::bb88", "Invalid IPv6 URL"), - ("http://[fc2e:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:bb88:edac"; host = ""'), - ("http://[fc2e:bb88:edac::z]", 'Invalid IPv6 address; source was "fc2e:bb88:edac::z"; host = ""'), - ("http://[fc2e:bb88:edac::2]:2a2", "Invalid port: .* '2a2'"), - ("://", "Missing scheme"), + pytest.param("http://[", "Invalid IPv6 URL", id='ipv6-single-open'), + pytest.param( + "http://[fc2e::bb88::edac]", + 'Invalid IPv6 address; source was "fc2e::bb88::edac"; host = ""', + id='ipv6-double-double', + ), + pytest.param( + "http://[fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac]", + 'Invalid IPv6 address; source was "fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac"; host = ""', + id='ipv6-long-double', + ), + pytest.param( + "http://[fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac]", + 'Invalid IPv6 address; source was "fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac"; host = ""', + id='ipv6-long', + ), + pytest.param( + "http://[127.0.0.1:fc2e::bb88:edac]", + r'Invalid IPv6 address; source was "127\.0\.0\.1:fc2e::bb88:edac', + id='ipv6-ipv4', + ), + pytest.param("http://[fc2e::bb88", "Invalid IPv6 URL", id='ipv6-trailing'), + pytest.param( + "http://[fc2e:bb88:edac]", + 'Invalid IPv6 address; source was "fc2e:bb88:edac"; host = ""', + id='ipv6-short', + ), + pytest.param( + "http://[fc2e:bb88:edac::z]", + 'Invalid IPv6 address; source was "fc2e:bb88:edac::z"; host = ""', + id='ipv6-z', + ), + pytest.param( + "http://[fc2e:bb88:edac::2]:2a2", + "Invalid port: .* '2a2'", + id='ipv6-port', + ), + pytest.param("://", "Missing scheme", id='scheme-naked'), ]) def test_invalid_patterns(pattern, error): with pytest.raises(urlmatch.ParseError, match=error): urlmatch.UrlPattern(pattern) +# pylint: enable=line-too-long + @pytest.mark.parametrize('host', ['.', ' ', ' .', '. ', '. .', '. . .', ' . ']) def test_whitespace_hosts(host): diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 6c57cb3d3..1ffbe3c09 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -484,17 +484,20 @@ class TestGitStrSubprocess: @needs_git def test_real_git(self, git_repo): """Test with a real git repository.""" - branch_name = subprocess.run( - ['git', 'config', 'init.defaultBranch'], - check=False, - stdout=subprocess.PIPE, - encoding='utf-8', - ).stdout.strip() - if not branch_name: - branch_name = 'master' + def _get_git_setting(name, default): + return subprocess.run( + ['git', 'config', '--default', default, name], + check=True, + stdout=subprocess.PIPE, + encoding='utf-8', + ).stdout.strip() ret = version._git_str_subprocess(str(git_repo)) - assert ret == f'6e4b65a on {branch_name} (1970-01-01 01:00:00 +0100)' + branch_name = _get_git_setting('init.defaultBranch', 'master') + abbrev_length = int(_get_git_setting('core.abbrev', '7')) + expected_sha = '6e4b65a529c0ab78fb370c1527d5809f7436b8f3'[:abbrev_length] + + assert ret == f'{expected_sha} on {branch_name} (1970-01-01 01:00:00 +0100)' def test_missing_dir(self, tmp_path): """Test with a directory which doesn't exist.""" @@ -13,7 +13,6 @@ minversion = 3.15 setenv = PYTEST_QT_API=pyqt5 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 DBUS_SESSION_BUS_ADDRESS basepython = @@ -42,7 +41,6 @@ commands = basepython = {env:PYTHON:python3} setenv = PYTEST_QT_API=pyqt5 - QUTE_BDD_WEBENGINE=true pip_pre = true deps = -r{toxinidir}/misc/requirements/requirements-tests-bleeding.txt commands_pre = pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt5 PyQtWebEngine @@ -160,7 +158,7 @@ passenv = APPDATA HOME PYINSTALLER_DEBUG deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt - -r{toxinidir}/misc/requirements/requirements-pyqt-pyinstaller.txt + -r{toxinidir}/misc/requirements/requirements-pyqt.txt commands = {envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec |