diff options
127 files changed, 1756 insertions, 1843 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c292d7986..5978f1f97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: linters: if: "!contains(github.event.head_commit.message, '[ci skip]')" timeout-minutes: 10 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: @@ -90,7 +90,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up problem matchers run: "python scripts/dev/ci/problemmatchers.py py38 ${{ runner.temp }}" - - run: tox -e py38 + - run: tox -e py tests: if: "!contains(github.event.head_commit.message, '[ci skip]')" @@ -112,6 +112,10 @@ jobs: - testenv: py38-pyqt514 os: ubuntu-20.04 python: 3.8 + ### PyQt 5.15.0 (Python 3.9) + - testenv: py39-pyqt5150 + os: ubuntu-20.04 + python: 3.9 ### PyQt 5.15 (Python 3.9, with coverage) - testenv: py39-pyqt515-cov os: ubuntu-20.04 @@ -121,6 +125,11 @@ jobs: os: macos-10.15 python: 3.7 args: "tests/unit" # Only run unit tests on macOS + ### macOS Big Sur + - testenv: py37-pyqt515 + os: macos-11.0 + python: 3.7 + args: "tests/unit" # Only run unit tests on macOS ### Windows: PyQt 5.15 (Python 3.7 to match PyInstaller env) - testenv: py37-pyqt515 os: windows-2019 @@ -164,7 +173,7 @@ jobs: codeql: if: "!contains(github.event.head_commit.message, '[ci skip]')" timeout-minutes: 30 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout repository uses: actions/checkout@v2 @@ -186,7 +195,7 @@ jobs: irc: timeout-minutes: 2 continue-on-error: true - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: [linters, tests, tests-docker, codeql] if: "always() && github.repository_owner == 'qutebrowser'" steps: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..06707eb3f --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,62 @@ +name: Rebuild Docker CI images + +on: + workflow_dispatch: + schedule: + - cron: "23 5 * * *" # daily at 5:23 + +jobs: + docker: + if: "github.repository == 'qutebrowser/qutebrowser'" + runs-on: ubuntu-20.04 + strategy: + matrix: + image: + - archlinux-webkit + - archlinux-webengine + - archlinux-webengine-unstable + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.x' + - run: pip install jinja2 + - name: Generate Dockerfile + run: python3 generate.py ${{ matrix.image }} + working-directory: scripts/dev/ci/docker/ + - uses: docker/setup-buildx-action@v1 + - uses: docker/login-action@v1 + with: + username: qutebrowser + password: ${{ secrets.DOCKER_TOKEN }} + - uses: docker/build-push-action@v2 + with: + file: scripts/dev/ci/docker/Dockerfile + context: . + tags: "qutebrowser/ci:${{ matrix.image }}" + push: ${{ github.ref == 'refs/heads/master' }} + + irc: + timeout-minutes: 2 + continue-on-error: true + runs-on: ubuntu-20.04 + needs: [docker] + if: "always() && github.repository == 'qutebrowser/qutebrowser'" + steps: + - name: Send success IRC notification + uses: Gottox/irc-message-action@v1.1 + if: "needs.docker.result == 'success'" + with: + server: chat.freenode.net + channel: '#qutebrowser-dev' + nickname: qutebrowser-bot + message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})" + - name: Send non-success IRC notification + uses: Gottox/irc-message-action@v1.1 + if: "needs.docker.result != 'success'" + with: + server: chat.freenode.net + channel: '#qutebrowser-dev' + nickname: qutebrowser-bot + message: "[${{ github.workflow }}] \u00034FAIL:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})\n + linters: ${{ needs.linters.result }}, tests: ${{ needs.tests.result }}, tests-docker: ${{ needs.tests-docker.result }}, codeql: ${{ needs.codeql.result }}" diff --git a/.github/workflows/recompile-requirements.yml b/.github/workflows/recompile-requirements.yml index c41f67810..c939aa81d 100644 --- a/.github/workflows/recompile-requirements.yml +++ b/.github/workflows/recompile-requirements.yml @@ -31,7 +31,7 @@ jobs: run: "python3 scripts/dev/recompile_requirements.py ${{ github.events.input.environments }}" id: requirements - name: Create pull request - uses: peter-evans/create-pull-request@v2 + uses: peter-evans/create-pull-request@v3 with: committer: qutebrowser bot <bot@qutebrowser.org> author: qutebrowser bot <bot@qutebrowser.org> diff --git a/.gitignore b/.gitignore index 50c67dee4..31c4ca3b7 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ Sessionx.vim /.pytest_cache /.testmondata /.hypothesis +/.benchmarks .mypy_cache /prof /venv @@ -36,10 +36,6 @@ ignore_missing_imports = True # https://bitbucket.org/birkenfeld/pygments-main/issues/1485/type-hints ignore_missing_imports = True -[mypy-cssutils] -# Pretty much inactive currently -ignore_missing_imports = True - [mypy-pypeg2] # Pretty much inactive currently ignore_missing_imports = True @@ -74,7 +74,7 @@ valid-metaclass-classmethod-first-arg=cls [TYPECHECK] ignored-modules=PyQt5,PyQt5.QtWebKit -ignored-classes=DummyBox +ignored-classes=DummyBox,__cause__ [IMPORTS] known-third-party=sip @@ -9,6 +9,7 @@ ignore: | rules: document-start: disable line-length: + max: 88 ignore: | /.github/*.yml /.github/workflows/*.yml diff --git a/README.asciidoc b/README.asciidoc index a3cf8b270..5437036be 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -134,8 +134,6 @@ The following software and libraries are required to run qutebrowser: The following libraries are optional: -* http://cthedot.de/cssutils/[cssutils] (for an improved `:download --mhtml` - with QtWebKit). * On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log output. * http://asciidoc.org/[asciidoc] to generate the documentation for the `:help` diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 03d7a3e66..85383256c 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -35,6 +35,13 @@ Major changes at the time of writing, it's recommended to https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc#installing-qutebrowser-with-virtualenv[install qutebrowser in a virtualenv] with a newer version of Qt/PyQt. +- Windows 7 is not supported anymore by the Windows binaries. +- The (formerly optional) `cssutils` dependency is now removed. It was only + needed for improved behavior in corner cases when using `:download --mhtml` + with the (non-default) QtWebKit backend, and as such it's unlikely anyone is + still relying on it. The `cssutils` project is also dead upstream, with its + repository being gone after Bitbucket + https://bitbucket.org/blog/sunsetting-mercurial-support-in-bitbucket[removed Mercurial support]. Removed ~~~~~~~ @@ -42,6 +49,27 @@ Removed - The `--enable-webengine-inspector` flag (which was only needed for Qt 5.10 and below) is now dropped. With Qt 5.11 and newer, the inspector/devtools are enabled unconditionally. +- Support for moving qutebrowser data from versions before v1.0.0 has been + removed. +- The `--old` flag for `:config-diff` has been removed. It used to show + customized options for the old pre-v1.0 config files (in order to aid + migration to v1.0). +- The `:inspector` command which was deprecated in v1.13.0 (in favor of + `:devtools`) is now removed. + +Added +~~~~~ + +- When QtWebEngine has been updated but PyQtWebEngine hasn't yet, the dark mode + settings might stop working. As a (currently undocumented) escape hatch, this + version adds a `QUTE_DARKMODE_VARIANT=qt_515_2` environment variable which can + be set to get the correct behavior in (transitive) situations like this. +- New `--desktop-file-name` commandline argument, which can be used to customize + the desktop filename passed to Qt (which is used to set the `app_id` on + Wayland). +- New userscripts: + - `kodi` to play videos in Kodi + - `qr` to generate a QR code of the current URL Changed ~~~~~~~ @@ -49,6 +77,7 @@ Changed - `config.py` files now are required to have either `config.load_autoconfig(False)` (don't load `autoconfig.yml`) or `config.load_autoconfig()` (do load `autoconfig.yml`) in them. +- (TODO) Windows and macOS releases now ship Python 3.9 rather than 3.7. - The `colors.webpage.darkmode.*` settings are now also supported with older Qt versions (Qt 5.12 and 5.13) rather than just with Qt 5.14 and above. - For regexes in the config (`hints.{prev,next}_regexes`), certain patterns @@ -60,42 +89,119 @@ Changed pre-selected in the prompt shown by qutebrowser. - URLs such as `::1/foo` are now handled as a search term or local file rather than IPv6. Use `[::1]/foo` to force parsing as IPv6 instead. +- The `mkvenv.py` script now runs a "smoke test" after setting up the virtual + environment to ensure it's working as expected. If necessary, the test can be + skipped via a new `--skip-smoke-test` flag. +- Both qutebrowser userscripts and Greasemonkey scripts are now additionally + picked up from qutebrowser's config directory (the `userscripts` and + `greasemonkey` subdirectories of e.g. `~/.config/qutebrowser/`) rather than only + the data directory (the same subdirectories of e.g. + `~/.local/share/qutebrowser/`). Fixed ~~~~~ -- The `open_url_instance.sh` userscript now complains when `socat` is not - installed, rather than silencing the error. - With interpolated color settings (`colors.tabs.indicator.*` and `colors.downloads.*`), the alpha channel is now handled correctly. +- The `format_json` userscript now uses `env` in its shebang, making it work + correctly on systems where `bash` isn't located in `/bin`. v1.14.1 (unreleased) -------------------- +Added +~~~~~ + +- With v1.14.0, qutebrowser configures the main window to be transparent, so + that it's possible to configure a translucent tab- or statusbar. However, that + change introduced various issues, such as performance degradation on some + systems or breaking dmenu window embedding with its `-w` option. To avoid those + issues for people who are not using transparency, the default behavior is + reverted to versions before v1.14.0 in this release. A new `window.transparent` + setting can be set to `true` to restore the behavior of v1.14.0. + Changed ~~~~~~~ -- (TODO) Windows and macOS releases now ship Qt 5.15.2, which is based on - Chromium 83.0.4103.122 with security fixes up to 86.0.4240.111. This includes +- Windows and macOS releases now ship Qt 5.15.2, which is based on + Chromium 83.0.4103.122 with security fixes up to 86.0.4240.183. This includes CVE-2020-15999 in the bundled freetype library, which is known to be exploited in the wild. It also includes various other bugfixes/features compared to Qt 5.15.0 included in qutebrowser v1.14.0, such as: * Correct handling of AltGr on Windows * Fix for `content.cookies.accept` not working properly - * Proper support for screen sharing + * Fixes for screen sharing (some websites are still broken until an upcoming Qt + 5.15.3) * Support for FIDO U2F / WebAuth * Fix for the unwanted creation of directories such as `databases-incognito` in the home directory * Proper autocompletion in the devtools console * Proper signalisation of a tab's audible status (`[A]`) + * Fix for a hang when opening the context menu on macOS Big Sur (11.0) * Hardware accelerated graphics on macOS Fixed ~~~~~ -- Fix for a crash introduced in v1.14.0 when closing qutebrowser after opening a - download with PDF.js. -- (TODO) Fix for various functionality breaking in private windows with v1.14.0, +- Setting the `content.headers.referer` setting to `same-domain` (the default) + was supposed to truncate referers to only the host with QtWebEngine. + Unfortunately, this functionality broke in Qt 5.14. It works properly again + with this release, including a test so this won't happen again. +- With QtWebEngine 5.15, setting the `content.headers.referer` setting to + `never` did still send referers. This is now fixed as well. +- In v1.14.0, a regression was introduced, causing a crash when qutebrowser was + closed after opening a download with PDF.js. This is now fixed. +- With Qt 5.12, the `Object.fromEntries` JavaScript API is unavailable (it was + introduced in Chromium 73, while Qt 5.12 is based on 69). This caused + https://www.vr.fi/en and possibly other websites to break when accessed with Qt + 5.12. A suitable polyfill is now included with qutebrowser if + `content.site_specific_quirks` is enabled (which is the default). +- While XDG startup notifications (e.g. launch feedback via the bouncy cursor + in KDE Plasma) were supported ever since Qt 5.1, qutebrowser's desktop file + accidentally declared that it wasn't supported. This is now fixed. +- The `dmenu_qutebrowser` and `qutedmenu` userscripts now correctly read the + qutebrowser sqlite history which has been in use since v1.0.0. +- With Python 3.8+ and vertical tabs, a deprecation warning for an implicit int + conversion was shown. This is now fixed. +- Ever since Qt 5.11, fetching more completion data when that data is loaded + lazily (such as with history) and the last visible item is selected was broken. + The exact reason is currently unknown, but this release adds a tenative fix. +- When PgUp/PgDown were used to go beyond the last visible item, the above issue + caused a crash, which is now also fixed. +- As a workaround for an overzealous Microsoft Defender false-positive detecting + a "trojan" in the (unprocessed) adblock list, `:adblock-update` now doesn't + cache the HTTP response anymore. +- With the QtWebKit backend and `content.headers` set to `same-domain` (the + default), origins with the same domain but different schemes or ports were + treated as the same domain. They now are correctly treated as different domains. +- When a URL path uses percent escapes (such as + `https://example.com/embedded%2Fpath`), using `:navigate up` would treat the + `%2F` as a path separator and replace any remaining percent escapes by their + unescaped equivalents. Those are now handled correctly. +- On macOS 11.0 (Big Sur), the default monospace font name caused a parsing error, thus + resulting in broken styling for the completion, hints, and other UI components. + They now look properly again. +- Due to a Qt bug, installing Qt/PyQt from prebuilt binaries on systems with a + very old `libxcb-utils` version (notably, Debian Stable, but not Ubuntu since + 16.04 LTS) results in a setup which fails to start. This also affects the + `mkvenv.py` script, which now includes a workaround for this case. +- The `open_url_instance.sh` userscript now complains when `socat` is not + installed, rather than silencing the error. +- The example AppArmor profile in `misc/` was outdated and written for the + older QtWebKit backend. It is now updated to serve as an useful starting + point with QtWebEngine. +- When running `:devtools` on Fedora without the needed (optional) dependency + installed, it was suggested to install `qt5-webengine-devtools`, which does + not, in fact, exist. It's now correctly suggested to install + `qt5-qtwebengine-devtools` instead. +- With Qt 5.15.2, lines/borders coming from the `readability-js` userscript + were invisible. This is now fixed by changing the border color to grey (with all + Qt versions). +- Due to changes in the underlying Chromium, the + `colors.webpage.prefers_color_scheme_dark` setting broke with Qt 5.15.2. It now + works properly again. +- Minor performance improvements. +- Fix for various functionality breaking in private windows with v1.14.0, after the last private window is closed. This includes: * Ad blocking * Downloads diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index d14cea86a..39df56faa 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -324,8 +324,14 @@ There is a total of four possible approaches to get dark websites: The setting requires a restart and QtWebEngine with at least Qt 5.14. - The `colors.webpage.darkmode.*` settings enable the dark mode of the underlying Chromium. Those setting require a restart and QtWebEngine with at least Qt 5.14. It's - unfortunately not possible (due to limitations in Chromium and/or QtWebEngine) to + unfortunately not possible (due to limitations + https://bugs.chromium.org/p/chromium/issues/detail?id=952419[in Chromium] and/or + https://bugreports.qt.io/browse/QTBUG-84484[QtWebEngine]) to change them dynamically or to specify a list of excluded websites. + There is some remaining hope to + https://github.com/qutebrowser/qutebrowser/issues/5542[allow for this] + using HTML/CSS features, but so far nobody has been able to get things to + work (even with Chromium) - help welcome! - The `content.user_stylesheets` setting allows specifying a custom CSS such as https://github.com/alphapapa/solarized-everything-css/[Solarized Everything]. Despite the name, the repository also offers themes other than just Solarized. This approach diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index b9e525075..f31283e9c 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -334,14 +334,8 @@ Remove a key from a dict. [[config-diff]] === config-diff -Syntax: +:config-diff [*--old*]+ - Show all customized options. -==== optional arguments -* +*-o*+, +*--old*+: Show difference for the pre-v1.0 files (qutebrowser.conf/keys.conf). - - [[config-edit]] === config-edit Syntax: +:config-edit [*--no-source*]+ @@ -605,7 +599,7 @@ Syntax: +:greasemonkey-reload [*--force*]+ Re-read Greasemonkey scripts from disk. -The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data directory (see `:version`). +The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data or config directories (see `:version`). ==== optional arguments * +*-f*+, +*--force*+: For any scripts that have required dependencies, re-download them. @@ -1316,7 +1310,8 @@ Note that the command is *not* run in a shell, so things like `$VAR` or `> outpu * +*-v*+, +*--verbose*+: Show notifications when the command started/exited. * +*-o*+, +*--output*+: Show the output in a new tab. * +*-m*+, +*--output-messages*+: Show the output as messages. -* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser. +* +*-d*+, +*--detach*+: Detach the command from qutebrowser so that it continues running when qutebrowser quits. + ==== count Given to userscripts as $QUTE_COUNT. diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index cd72ae6ce..89866ccce 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -9,9 +9,7 @@ qutebrowser's config files -------------------------- qutebrowser releases before v1.0.0 had a `qutebrowser.conf` and `keys.conf` -file. Those are not used anymore since that release - see -<<migrating,"Migrating older configurations">> for information on how to -migrate to the new config. +file. Those are not used anymore since v1.0.0. When using `:set` and `:bind`, changes are saved to an `autoconfig.yml` file automatically. If you don't want to have a config file which is curated by @@ -439,38 +437,3 @@ Various emacs/conkeror-like keybinding configs exist: It's also mostly possible to get rid of modal keybindings by setting `input.insert_mode.auto_enter` to `false`, and `input.forward_unbound_keys` to `all`. - -[[migrating]] -Migrating older configurations ------------------------------- - -qutebrowser does no automatic migration for the new configuration. However, -there's a special link:qute://configdiff/old[configdiff] page -(`qute://configdiff/old`) in qutebrowser, which will show you the changes you -did in your old configuration, compared to the old defaults. - -Other changes in default settings: - -- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history - if no text was entered yet. - With v1.0.x, they always navigate through command history instead of selecting - completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion - instead. - You can get back the old behavior by doing: -+ ----- -:bind -m command <Up> completion-item-focus prev -:bind -m command <Down> completion-item-focus next ----- -+ -or always navigate through command history with -+ ----- -:bind -m command <Up> command-history-prev -:bind -m command <Down> command-history-next ----- - -- The default for `completion.web_history.max_items` is now set to `-1`, showing - an unlimited number of items in the completion for `:open` as the new - sqlite-based completion is much faster. If the `:open` completion is too slow - on your machine, set an appropriate limit again. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index ddd5df44c..309f1ab1d 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -327,6 +327,7 @@ |<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`. |<<window.hide_decoration,window.hide_decoration>>|Hide the window decoration. |<<window.title_format,window.title_format>>|Format to use for the window title. The same placeholders like for +|<<window.transparent,window.transparent>>|Set the main window background to transparent. |<<zoom.default,zoom.default>>|Default zoom level. |<<zoom.levels,zoom.levels>>|Available zoom levels. |<<zoom.mouse_divider,zoom.mouse_divider>>|Number of zoom increments to divide the mouse wheel movements to. @@ -4206,6 +4207,22 @@ Type: <<types,FormatString>> Default: +pass:[{perc}{current_title}{title_sep}qutebrowser]+ +[[window.transparent]] +=== window.transparent +Set the main window background to transparent. + +This allows having a transparent tab- or statusbar (might require a compositor such +as picom). However, it breaks some functionality such as dmenu embedding via its +`-w` option. On some systems, it was additionally reported that main window +transparency negatively affects performance. + +Note this setting only affects windows opened after setting it. + + +Type: <<types,Bool>> + +Default: +pass:[false]+ + [[zoom.default]] === zoom.default Default zoom level. diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 777eddc65..83e7986bc 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -62,6 +62,9 @@ show it. *--backend* '{webkit,webengine}':: Which backend to use. +*--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 + === debug arguments *-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}':: Override the configured console loglevel diff --git a/doc/userscripts.asciidoc b/doc/userscripts.asciidoc index 2dc34402d..9bbc68ce0 100644 --- a/doc/userscripts.asciidoc +++ b/doc/userscripts.asciidoc @@ -18,7 +18,7 @@ mpv, a simple key binding to something like `:spawn mpv {url}` should suffice. Also note userscripts need to have the executable bit set (`chmod +x`) for qutebrowser to run them. -To call a userscript, it needs to be stored in your data directory under +To call a userscript, it needs to be stored in your config or data directory under `userscripts` (for example: `~/.local/share/qutebrowser/userscripts/myscript`), or just use an absolute path. diff --git a/misc/org.qutebrowser.qutebrowser.desktop b/misc/org.qutebrowser.qutebrowser.desktop index a1deb319f..cf3ee0422 100644 --- a/misc/org.qutebrowser.qutebrowser.desktop +++ b/misc/org.qutebrowser.qutebrowser.desktop @@ -46,7 +46,7 @@ Type=Application Categories=Network;WebBrowser; Exec=qutebrowser %u Terminal=false -StartupNotify=false +StartupNotify=true MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute; Keywords=Browser Actions=new-window;preferences; diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index 8b8f6ba1a..e9376f0b1 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -2,8 +2,7 @@ build==0.1.0 check-manifest==0.45 -packaging==20.4 +packaging==20.7 pep517==0.9.1 pyparsing==2.4.7 -six==1.15.0 toml==0.10.2 diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index ed83ef302..578ab2b64 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -2,25 +2,24 @@ bump2version==1.0.1 certifi==2020.11.8 -cffi==1.14.3 +cffi==1.14.4 chardet==3.0.4 colorama==0.4.4 cryptography==3.2.1 -cssutils==1.0.2 github3.py==1.3.0 hunter==3.3.1 idna==2.10 jwcrypto==0.8 manhole==1.6.0 -packaging==20.4 +packaging==20.7 pycparser==2.20 Pympler==0.9 pyparsing==2.4.7 -PyQt-builder==1.5.0 +PyQt-builder==1.6.0 python-dateutil==2.8.1 -requests==2.24.0 -sip==5.4.0 +requests==2.25.0 +sip==5.5.0 six==1.15.0 toml==0.10.2 uritemplate==3.0.1 -# urllib3==1.25.11 +# urllib3==1.26.2 diff --git a/misc/requirements/requirements-dev.txt-raw b/misc/requirements/requirements-dev.txt-raw index e7758f167..fd840bab1 100644 --- a/misc/requirements/requirements-dev.txt-raw +++ b/misc/requirements/requirements-dev.txt-raw @@ -1,5 +1,4 @@ hunter -cssutils pympler github3.py bump2version diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 8bad8cb2d..3427c0c69 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -2,11 +2,11 @@ attrs==20.3.0 flake8==3.8.4 -flake8-bugbear==20.1.4 +flake8-bugbear==20.11.1 flake8-builtins==1.5.3 flake8-comprehensions==3.3.0 flake8-copyright==0.2.2 -flake8-debugger==3.2.1 +flake8-debugger==4.0.0 flake8-deprecated==1.3 flake8-docstrings==1.5.0 flake8-future-import==0.4.6 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 8509c5bea..a046a0b5e 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,10 +1,10 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py diff-cover==4.0.1 -inflect==4.1.0 +inflect==5.0.2 Jinja2==2.11.2 jinja2-pluralize==0.3.0 -lxml==4.6.1 +lxml==4.6.2 MarkupSafe==1.1.1 mypy==0.790 mypy-extensions==0.4.3 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index a48e80fde..b1a3e98ee 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py altgraph==0.17 -pyinstaller==4.0 +pyinstaller==4.1 pyinstaller-hooks-contrib==2020.10 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index d82495cb9..e3856a40a 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -2,7 +2,7 @@ astroid==2.3.3 # rq.filter: < 2.4 certifi==2020.11.8 -cffi==1.14.3 +cffi==1.14.4 chardet==3.0.4 cryptography==3.2.1 github3.py==1.3.0 @@ -15,9 +15,9 @@ pycparser==2.20 pylint==2.4.4 # rq.filter: < 2.5 python-dateutil==2.8.1 ./scripts/dev/pylint_checkers -requests==2.24.0 +requests==2.25.0 six==1.15.0 typed-ast==1.4.1 ; python_version<"3.8" uritemplate==3.0.1 -# urllib3==1.25.11 +# urllib3==1.26.2 wrapt==1.11.2 diff --git a/misc/requirements/requirements-pyqt-5.15.0.txt b/misc/requirements/requirements-pyqt-5.15.0.txt new file mode 100644 index 000000000..b9ee53f65 --- /dev/null +++ b/misc/requirements/requirements-pyqt-5.15.0.txt @@ -0,0 +1,5 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +PyQt5==5.15.0 # rq.filter: == 5.15.0 +PyQt5-sip==12.8.1 +PyQtWebEngine==5.15.0 # rq.filter: == 5.15.0 diff --git a/misc/requirements/requirements-pyqt-5.15.0.txt-raw b/misc/requirements/requirements-pyqt-5.15.0.txt-raw new file mode 100644 index 000000000..12d6adb7d --- /dev/null +++ b/misc/requirements/requirements-pyqt-5.15.0.txt-raw @@ -0,0 +1,4 @@ +#@ filter: PyQt5 == 5.15.0 +#@ filter: PyQtWebEngine == 5.15.0 +PyQt5 == 5.15.0 +PyQtWebEngine == 5.15.0 diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt index 21745c814..e791bb323 100644 --- a/misc/requirements/requirements-pyqt-5.15.txt +++ b/misc/requirements/requirements-pyqt-5.15.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.15.1 # rq.filter: < 6 +PyQt5==5.15.2 # rq.filter: < 6 PyQt5-sip==12.8.1 -PyQtWebEngine==5.15.1 # rq.filter: < 6 +PyQtWebEngine==5.15.2 # rq.filter: < 6 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 148e8d8bb..ec6cfd810 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.15.1 +PyQt5==5.15.2 PyQt5-sip==12.8.1 -PyQtWebEngine==5.15.0 +PyQtWebEngine==5.15.2 diff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw index 83ebc7671..9c6afbf16 100644 --- a/misc/requirements/requirements-pyqt.txt-raw +++ b/misc/requirements/requirements-pyqt.txt-raw @@ -1,2 +1,2 @@ PyQt5 -PyQtWebEngine!=5.15.1 +PyQtWebEngine diff --git a/misc/requirements/requirements-qutebrowser.txt-raw b/misc/requirements/requirements-qutebrowser.txt-raw index c66c65beb..4678b9ce5 100644 --- a/misc/requirements/requirements-qutebrowser.txt-raw +++ b/misc/requirements/requirements-qutebrowser.txt-raw @@ -3,5 +3,4 @@ Pygments pyPEG2 PyYAML colorama -cssutils attrs diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 83184aa09..164311235 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py alabaster==0.7.12 -Babel==2.8.0 +Babel==2.9.0 certifi==2020.11.8 chardet==3.0.4 docutils==0.16 @@ -9,18 +9,17 @@ idna==2.10 imagesize==1.2.0 Jinja2==2.11.2 MarkupSafe==1.1.1 -packaging==20.4 +packaging==20.7 Pygments==2.7.2 pyparsing==2.4.7 pytz==2020.4 -requests==2.24.0 -six==1.15.0 +requests==2.25.0 snowballstemmer==2.0.0 -Sphinx==3.3.0 +Sphinx==3.3.1 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.4 -urllib3==1.25.11 +urllib3==1.26.2 diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt index 14b6eec04..9efbeca40 100644 --- a/misc/requirements/requirements-tests-git.txt +++ b/misc/requirements/requirements-tests-git.txt @@ -27,7 +27,6 @@ git+https://github.com/pallets/werkzeug.git ## qutebrowser dependencies git+https://github.com/tartley/colorama.git -hg+https://bitbucket.org/cthedot/cssutils git+https://github.com/pallets/jinja.git git+https://github.com/pallets/markupsafe.git hg+http://bitbucket.org/birkenfeld/pygments-main diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 21b290737..bd77427d4 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -5,7 +5,7 @@ attrs==20.3.0 beautifulsoup4==4.9.3 certifi==2020.11.8 chardet==3.0.4 -cheroot==8.4.5 +cheroot==8.4.7 click==7.1.2 # colorama==0.4.4 coverage==5.3 @@ -15,7 +15,7 @@ filelock==3.0.12 Flask==1.1.2 glob2==0.7 hunter==3.3.1 -hypothesis==5.41.2 +hypothesis==5.41.4 icdiff==1.9.1 idna==2.10 iniconfig==1.1.1 @@ -26,7 +26,7 @@ Mako==1.1.3 manhole==1.6.0 # MarkupSafe==1.1.1 more-itertools==8.6.0 -packaging==20.4 +packaging==20.7 parse==1.18.0 parse-type==0.5.2 pluggy==0.13.1 @@ -50,13 +50,13 @@ pytest-rerunfailures==9.1.1 pytest-xdist==2.1.0 pytest-xvfb==2.0.0 PyVirtualDisplay==1.3.2 -requests==2.24.0 +requests==2.25.0 requests-file==1.5.1 six==1.15.0 sortedcontainers==2.3.0 soupsieve==2.0.1 termcolor==1.1.0 -tldextract==3.0.2 +tldextract==3.1.0 toml==0.10.2 urllib3==1.25.11 vulture==2.1 diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw index fd346d475..0c9e3928f 100644 --- a/misc/requirements/requirements-tests.txt-raw +++ b/misc/requirements/requirements-tests.txt-raw @@ -1,5 +1,6 @@ beautifulsoup4 -cheroot +# https://github.com/cherrypy/cheroot/issues/341 +cheroot!=8.4.8 coverage Flask hypothesis @@ -33,5 +34,7 @@ pytest-clarity # Needed to test misc/userscripts/qute-lastpass tldextract +# https://github.com/urllib3/urllib3/issues/2071 +urllib3!=1.26.0,!=1.26.1,!=1.26.2 #@ ignore: Jinja2, MarkupSafe, colorama diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 9241f07a6..86b3997f4 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -3,13 +3,11 @@ appdirs==1.4.4 distlib==0.3.1 filelock==3.0.12 -packaging==20.4 +packaging==20.7 pluggy==0.13.1 py==1.9.0 pyparsing==2.4.7 six==1.15.0 toml==0.10.2 tox==3.20.1 -tox-pip-version==0.0.7 -tox-venv==0.4.0 -virtualenv==20.1.0 +virtualenv==20.2.1 diff --git a/misc/requirements/requirements-tox.txt-raw b/misc/requirements/requirements-tox.txt-raw index fab438034..053148f84 100644 --- a/misc/requirements/requirements-tox.txt-raw +++ b/misc/requirements/requirements-tox.txt-raw @@ -1,3 +1 @@ tox -tox-venv -tox-pip-version diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md index a17f7164c..669bfa664 100644 --- a/misc/userscripts/README.md +++ b/misc/userscripts/README.md @@ -24,7 +24,7 @@ The following userscripts are included in the current directory. - [qutedmenu](./qutedmenu): Handle open -s && open -t with bemenu. - [readability](./readability): Executes python-readability on current page and opens the summary as new tab. -- [readability-js](./readability-js): Processes the current page with the readability +- [readability-js](./readability-js): Processes the current page with the readability library used in Firefox Reader View and opens the summary as new tab. - [ripbang](./ripbang): Adds DuckDuckGo bang as searchengine. - [rss](./rss): Keeps track of URLs in RSS feeds and opens new ones. @@ -32,6 +32,9 @@ The following userscripts are included in the current directory. - [tor_identity](./tor_identity): Change your tor identity. - [view_in_mpv](./view_in_mpv): Views the current web page in mpv using sensible mpv-flags. +- [qr](./qr): Show a QR code for the current webpage via + [qrencode](https://fukuchi.org/works/qrencode/). +- [kodi](./kodi): Play videos in Kodi. [castnow]: https://github.com/xat/castnow [youtube-dl]: https://rg3.github.io/youtube-dl/ @@ -67,6 +70,8 @@ The following userscripts can be found on their own repositories. and retrieve they when you want. - [doi](https://github.com/cadadr/configuration/blob/master/qutebrowser/userscripts/doi): Opens DOIs on Sci-Hub. +- [1password](https://github.com/tomoakley/dotfiles/blob/master/qutebrowser/userscripts/1password): + Integration with 1password on macOS. [Zotero]: https://www.zotero.org/ [Pocket]: https://getpocket.com/ diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index 84be1b619..57bdb805c 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -38,9 +38,10 @@ # (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.) # -[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com' -url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser) +[ -z "$QUTE_URL" ] && QUTE_URL='https://duckduckgo.com' + +url=$(printf "%s\n%s" "$QUTE_URL" "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')" | cat "$QUTE_CONFIG_DIR/quickmarks" - | dmenu -l 15 -p qutebrowser) url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url") [ -z "${url// }" ] && exit diff --git a/misc/userscripts/format_json b/misc/userscripts/format_json index 541408c70..8a83c25fa 100755 --- a/misc/userscripts/format_json +++ b/misc/userscripts/format_json @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail # # Behavior: diff --git a/misc/userscripts/kodi b/misc/userscripts/kodi new file mode 100755 index 000000000..63fcc81fe --- /dev/null +++ b/misc/userscripts/kodi @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# +# Behavior: +# A qutebrowser userscript that plays Twitch, YouTube or Vimeo videos in Kodi via its +# API. +# +# Requirements: +# awk +# bash +# curl +# +# Kodi setup: +# Settings -> Services -> Control +# enable 'Allow remote control via HTTP' +# set Username and Password +# enable 'Allow remote control from applications on this system' +# Optional yet recommended, setup SSL within Kodi over via a proxy webserver +# +# userscript setup: +# create ~/.config/qutebrowser/kodi_rc with host and authentication information like: +# +# HOST="http://127.0.0.1:8080" +# or +# HOST="https://kodi.example.com" +# +# AUTH="user:password" +# or +# AUTH="bas64authenticationinformation" +# +# The base64 authentication is the output of +# `echo -ne "user:password" |base64 --wrap 0` +# reminder base64 is not encryption +# +# For vim users you might want to add '# vim: set nospell filetype=bash' to the +# kodi_rc file. +# +# qutebrowser setup: +# in ~/.config/qutebrowser/config.py add something like +# +# to send video link via hints: +# config.bind('X', 'hint links userscript kodi') +# to send current URL: +# config.bind('X', 'spawn --userscript kodi') +# +# troubleshooting: +# Errors detected within this userscript with have an exit of 231. All other exit +# codes will come from curl or awk. To test that the kodi_rc file is set up +# correctly, run the following command. It will display a 'It works!' notification within Kodi. +# +# source ~/.config/qutebrowser/kodi_rc ; curl --request POST "$HOST"/jsonrpc --header "Authorization: Basic $AUTH" --header "Content-Type: application/json" --data '{"id":1,"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"It works!","message":"both HOST and AUTH are correct"}}' +# +# In case you miss the notification in Kodi the successful response is: +# +# {"id":1,"jsonrpc":"2.0","result":"OK"} +# +# Note, curl will display errors for some problems, but not all. + +if [[ -z "$QUTE_FIFO" ]] ; then + echo "This script is designed to run as a qutebrowser userscript, not as a standalone script." + exit 231 +fi + +# configuration loading adapted from the password_fill userscript +QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/} +KODI_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/kodi_rc} +if [[ -f "$KODI_CONFIG" ]] ; then + # shellcheck source=/dev/null + source "$KODI_CONFIG" + if [[ -z "$HOST" || -z "$AUTH" ]] ; then + echo "message-error 'HOST and/or AUTH not set in $KODI_CONFIG'" > "$QUTE_FIFO" + exit 231 + fi +else + echo "message-error '$KODI_CONFIG not found'" > "$QUTE_FIFO" + exit 231 +fi + +# get real URL from twitter links +if [[ "$QUTE_URL" =~ ^https:\/\/t\.co ]] ; then + QUTE_URL=$(curl -o /dev/null --silent --head --write-out '%{redirect_url}' "$QUTE_URL" ) +fi + +# regex from https://github.com/dirkjanm/firefox-send-to-xbmc/blob/master/webextension/main.js +if [[ "$QUTE_URL" =~ ^.*twitch.tv\/([a-zA-Z0-9_]+)$ ]] ; then + NAME="${BASH_REMATCH[1]}" + JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.twitch/?mode=play&channel_name='$NAME'"}},"id":"2"}' + +elif [[ "$QUTE_URL" =~ ^.*twitch.tv\/videos\/([0-9]+)$ ]] ; then + NAME="${BASH_REMATCH[1]}" + JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.twitch/?mode=play&video_id='$NAME'"}},"id":"2"}' + +elif [[ "$QUTE_URL" =~ ^.*vimeo.com\/([0-9]+) ]] ; then + NAME="${BASH_REMATCH[1]}" + JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.vimeo/play/?video_id='$NAME'"}},"id":"2"}' + +elif [[ "$QUTE_URL" =~ ^.*youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=([^#\&\?]*).* ]] ; then + NAME="${BASH_REMATCH[1]}" + JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.youtube/play/?video_id='$NAME'"}},"id":"2"}' +fi + +if [[ "$JSON" ]] ; then + curl \ + --request POST "$HOST"/jsonrpc \ + --header "Authorization: Basic $AUTH" \ + --header "Content-Type: application/json" \ + --data "$JSON" \ + --silent > /dev/null +else + URL=$(echo "$QUTE_URL" |awk -F/ '{print $3}') + echo "message-warning 'kodi userscript does not support this $URL'" > "$QUTE_FIFO" +fi diff --git a/misc/userscripts/qr b/misc/userscripts/qr new file mode 100755 index 000000000..84215249b --- /dev/null +++ b/misc/userscripts/qr @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +pngfile=$(mktemp --suffix=.png) +trap 'rm -f "$pngfile"' EXIT + +qrencode -t PNG -o "$pngfile" -s 10 "$QUTE_URL" +echo ":open -t file:///$pngfile" >> "$QUTE_FIFO" +sleep 1 # give qutebrowser time to open the file before it gets removed diff --git a/misc/userscripts/qutedmenu b/misc/userscripts/qutedmenu index cc5a44413..bdd0d9b27 100755 --- a/misc/userscripts/qutedmenu +++ b/misc/userscripts/qutedmenu @@ -6,8 +6,9 @@ # If you would like to set a custom colorscheme/font use these dirs. # https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors -readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config} + +readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config} readonly optsfile=$confdir/dmenu/bemenucolors create_menu() { @@ -22,15 +23,13 @@ create_menu() { done < "$QUTE_CONFIG_DIR"/bookmarks/urls # Finally history - while read -r _ url; do - printf -- '%s\n' "$url" - done < "$QUTE_DATA_DIR"/history + printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')" } get_selection() { opts+=(-p qutebrowser) - #create_menu | dmenu -l 10 "${opts[@]}" - create_menu | bemenu -l 10 "${opts[@]}" + create_menu | dmenu -l 10 "${opts[@]}" + #create_menu | bemenu -l 10 "${opts[@]}" } # Main diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js index e189e5ee4..310d1c081 100755 --- a/misc/userscripts/readability-js +++ b/misc/userscripts/readability-js @@ -57,7 +57,7 @@ const HEADER = ` table, th, td { - border: 1px solid currentColor; + border: 1px solid grey; border-collapse: collapse; padding: 6px; vertical-align: top; @@ -77,7 +77,7 @@ const HEADER = ` background-color: #dddddd; } blockquote { - border-inline-start: 2px solid #333333 !important; + border-inline-start: 2px solid grey !important; padding: 0; padding-inline-start: 16px; margin-inline-start: 24px; diff --git a/qutebrowser/api/downloads.py b/qutebrowser/api/downloads.py index 5e5d1916a..55656c5b5 100644 --- a/qutebrowser/api/downloads.py +++ b/qutebrowser/api/downloads.py @@ -75,4 +75,6 @@ def download_temp(url: QUrl) -> TempDownload: fobj.name = 'temporary: ' + url.host() target = downloads.FileObjDownloadTarget(fobj) download_manager = objreg.get('qtnetwork-download-manager') - return download_manager.get(url, target=target, auto_remove=True) + # cache=False is set as a WORKAROUND for MS Defender thinking we're a trojan + # downloader when caching the hostblock list... + return download_manager.get(url, target=target, auto_remove=True, cache=False) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index fc514a634..76d52470a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -96,7 +96,8 @@ def run(args): q_app = Application(args) q_app.setOrganizationName("qutebrowser") q_app.setApplicationName("qutebrowser") - q_app.setDesktopFileName("org.qutebrowser.qutebrowser") + # Default DesktopFileName is org.qutebrowser.qutebrowser, set in `get_argparser()` + q_app.setDesktopFileName(args.desktop_file_name) q_app.setApplicationVersion(qutebrowser.__version__) if args.version: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 34c078d89..18777e250 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1057,7 +1057,8 @@ class CommandDispatcher: verbose: Show notifications when the command started/exited. output: Show the output in a new tab. output_messages: Show the output as messages. - detach: Whether the command should be detached from qutebrowser. + detach: Detach the command from qutebrowser so that it continues + running when qutebrowser quits. cmdline: The commandline to execute. count: Given to userscripts as $QUTE_COUNT. """ diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 31a9d7f29..96220897c 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -560,8 +560,8 @@ class AbstractDownloadItem(QObject): elif self.stats.percentage() is None: return start else: - return utils.interpolate_color(start, stop, - self.stats.percentage(), system) + return qtutils.interpolate_color( + start, stop, self.stats.percentage(), system) def _do_cancel(self): """Actual cancel implementation.""" diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py index 0305bd589..df8b2b0c2 100644 --- a/qutebrowser/browser/greasemonkey.py +++ b/qutebrowser/browser/greasemonkey.py @@ -41,9 +41,12 @@ from qutebrowser.misc import objects gm_manager = cast('GreasemonkeyManager', None) -def _scripts_dir(): +def _scripts_dirs(): """Get the directory of the scripts.""" - return os.path.join(standarddir.data(), 'greasemonkey') + return [ + os.path.join(standarddir.data(), 'greasemonkey'), + os.path.join(standarddir.config(), 'greasemonkey'), + ] class GreasemonkeyScript: @@ -277,18 +280,19 @@ class GreasemonkeyManager(QObject): self._run_end = [] self._run_idle = [] - scripts_dir = os.path.abspath(_scripts_dir()) - log.greasemonkey.debug("Reading scripts from: {}".format(scripts_dir)) - for script_filename in glob.glob(os.path.join(scripts_dir, '*.js')): - if not os.path.isfile(script_filename): - continue - script_path = os.path.join(scripts_dir, script_filename) - with open(script_path, encoding='utf-8-sig') as script_file: - script = GreasemonkeyScript.parse(script_file.read(), - script_filename) - if not script.name: - script.name = script_filename - self.add_script(script, force) + for scripts_dir in _scripts_dirs(): + scripts_dir = os.path.abspath(scripts_dir) + log.greasemonkey.debug("Reading scripts from: {}".format(scripts_dir)) + for script_filename in glob.glob(os.path.join(scripts_dir, '*.js')): + if not os.path.isfile(script_filename): + continue + script_path = os.path.join(scripts_dir, script_filename) + with open(script_path, encoding='utf-8-sig') as script_file: + script = GreasemonkeyScript.parse(script_file.read(), + script_filename) + if not script.name: + script.name = script_filename + self.add_script(script, force) self.scripts_reloaded.emit() def add_script(self, script, force=False): @@ -325,7 +329,7 @@ class GreasemonkeyManager(QObject): log.greasemonkey.debug("Loaded script: {}".format(script.name)) def _required_url_to_file_path(self, url): - requires_dir = os.path.join(_scripts_dir(), 'requires') + requires_dir = os.path.join(_scripts_dirs()[0], 'requires') if not os.path.exists(requires_dir): os.mkdir(requires_dir) return os.path.join(requires_dir, utils.sanitize_filename(url)) @@ -426,7 +430,7 @@ def greasemonkey_reload(force=False): """Re-read Greasemonkey scripts from disk. The scripts are read from a 'greasemonkey' subdirectory in - qutebrowser's data directory (see `:version`). + qutebrowser's data or config directories (see `:version`). Args: force: For any scripts that have required dependencies, @@ -440,7 +444,8 @@ def init(): global gm_manager gm_manager = GreasemonkeyManager() - try: - os.mkdir(_scripts_dir()) - except FileExistsError: - pass + for scripts_dir in _scripts_dirs(): + try: + os.mkdir(scripts_dir) + except FileExistsError: + pass diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index b852ab29e..bace6fa6a 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -134,13 +134,13 @@ def path_up(url, count): """ urlutils.ensure_valid(url) url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery) - path = url.path() + path = url.path(QUrl.FullyEncoded) if not path or path == '/': raise Error("Can't go up!") for _i in range(0, min(count, path.count('/'))): path = posixpath.join(path, posixpath.pardir) path = posixpath.normpath(path) - url.setPath(path) + url.setPath(path, QUrl.StrictMode) return url diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index c5bfc07e6..0bf165965 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -419,11 +419,12 @@ class DownloadManager(downloads.AbstractDownloadManager): private=config.val.content.private_browsing, parent=self) @pyqtSlot('QUrl') - def get(self, url, **kwargs): + def get(self, url, cache=True, **kwargs): """Start a download with a link URL. Args: url: The URL to get, as QUrl + cache: If set to False, don't cache the response. **kwargs: passed to get_request(). Return: @@ -437,6 +438,9 @@ class DownloadManager(downloads.AbstractDownloadManager): user_agent = websettings.user_agent(url) req.setHeader(QNetworkRequest.UserAgentHeader, user_agent) + if not cache: + req.setAttribute(QNetworkRequest.CacheSaveControlAttribute, False) + return self.get_request(req, **kwargs) def get_mhtml(self, tab, target): diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 3652b3aed..cb4a9ba61 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -38,7 +38,7 @@ from PyQt5.QtCore import QUrlQuery, QUrl import qutebrowser from qutebrowser.browser import pdfjs, downloads, history -from qutebrowser.config import config, configdata, configexc, configdiff +from qutebrowser.config import config, configdata, configexc from qutebrowser.utils import (version, utils, jinja, log, message, docutils, objreg, standarddir) from qutebrowser.qt import sip @@ -481,18 +481,10 @@ def qute_back(url: QUrl) -> _HandlerRet: @add_handler('configdiff') -def qute_configdiff(url: QUrl) -> _HandlerRet: +def qute_configdiff(_url: QUrl) -> _HandlerRet: """Handler for qute://configdiff.""" - if url.path() == '/old': - try: - return 'text/html', configdiff.get_diff() - except OSError as e: - error = (b'Failed to read old config: ' + - str(e.strerror).encode('utf-8')) - return 'text/plain', error - else: - data = config.instance.dump_userconfig().encode('utf-8') - return 'text/plain', data + data = config.instance.dump_userconfig().encode('utf-8') + return 'text/plain', data @add_handler('pastebin-version') diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 193a2a0e0..9234e82d8 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -82,7 +82,7 @@ def javascript_confirm(url, js_msg, abort_on): raise CallSuper msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()), - js_msg) + html.escape(js_msg)) urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) ans = message.ask('Javascript confirm', msg, mode=usertypes.PromptMode.yesno, @@ -99,7 +99,7 @@ def javascript_prompt(url, js_msg, default, abort_on): return (False, "") msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()), - js_msg) + html.escape(js_msg)) urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) answer = message.ask('Javascript prompt', msg, mode=usertypes.PromptMode.text, @@ -122,7 +122,7 @@ def javascript_alert(url, js_msg, abort_on): return msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()), - js_msg) + html.escape(js_msg)) urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert, abort_on=abort_on, url=urlstr) diff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py index 5795364b0..d067edea3 100644 --- a/qutebrowser/browser/webengine/darkmode.py +++ b/qutebrowser/browser/webengine/darkmode.py @@ -73,6 +73,7 @@ Prefix changed to "forceDarkMode". - As with Qt 5.15.0 / .1, but with "forceDarkMode" as prefix. """ +import os import enum from typing import Any, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union @@ -90,7 +91,6 @@ class Variant(enum.Enum): """A dark mode variant.""" - unavailable = enum.auto() qt_511_to_513 = enum.auto() qt_514 = enum.auto() qt_515_0 = enum.auto() @@ -159,8 +159,6 @@ _QT_514_SETTINGS = [ # workaround warning below if the setting wasn't explicitly customized. _DARK_MODE_DEFINITIONS: Mapping[Variant, _DarkModeDefinitionType] = { - Variant.unavailable: ([], set()), - Variant.qt_515_2: ([ # 'darkMode' renamed to 'forceDarkMode' ('enabled', 'forceDarkModeEnabled', _BOOLS), @@ -235,6 +233,13 @@ _DARK_MODE_DEFINITIONS: Mapping[Variant, _DarkModeDefinitionType] = { def _variant() -> Variant: """Get the dark mode variant based on the underlying Qt version.""" + env_var = os.environ.get('QUTE_DARKMODE_VARIANT') + if env_var is not None: + try: + return Variant[env_var] + except KeyError: + log.init.warning(f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}") + if PYQT_WEBENGINE_VERSION is not None: # Available with Qt >= 5.13 if PYQT_WEBENGINE_VERSION >= 0x050f02: @@ -259,6 +264,16 @@ def _variant() -> Variant: def settings() -> Iterator[Tuple[str, str]]: """Get necessary blink settings to configure dark mode for QtWebEngine.""" + if (qtutils.version_check('5.15.2', compiled=False) and + config.val.colors.webpage.prefers_color_scheme_dark): + # With older Qt versions, this is passed in qtargs.py as --force-dark-mode + # instead. + # + # With Chromium 85 (> Qt 5.15.2), the enumeration has changed in Blink and this + # will need to be set to '0' instead: + # https://chromium-review.googlesource.com/c/chromium/src/+/2232922 + yield "preferredColorScheme", "1" + if not config.val.colors.webpage.darkmode.enabled: return diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 95e01588b..b27509552 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import QUrl, QByteArray from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo) -from qutebrowser.config import websettings +from qutebrowser.config import websettings, config from qutebrowser.browser import shared from qutebrowser.utils import utils, log, debug, qtutils from qutebrowser.extensions import interceptors @@ -204,5 +204,11 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): for header, value in shared.custom_headers(url=url): info.setHttpHeader(header, value) + # Note this is ignored before Qt 5.12.4 and 5.13.1 due to + # https://bugreports.qt.io/browse/QTBUG-60203 - there, we set the + # commandline-flag in qtargs.py instead. + if config.cache['content.headers.referer'] == 'never': + info.setHttpHeader(b'Referer', b'') + user_agent = websettings.user_agent(url) info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 8ba176885..98a6bf05d 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1149,6 +1149,9 @@ class _WebEngineScripts(QObject): quirks.append(('globalthis_quirk', QWebEngineScript.DocumentCreation, QWebEngineScript.MainWorld)) + quirks.append(('object_fromentries_quirk', + QWebEngineScript.DocumentCreation, + QWebEngineScript.MainWorld)) for filename, injection_point, world in quirks: script = QWebEngineScript() diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index 6e2442575..794e1b73b 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -62,7 +62,7 @@ _CSS_URL_PATTERNS = [re.compile(x) for x in [ ]] -def _get_css_imports_regex(data): +def _get_css_imports(data): """Return all assets that are referenced in the given CSS document. The returned URLs are relative to the stylesheet's URL. @@ -79,55 +79,6 @@ def _get_css_imports_regex(data): return urls -def _get_css_imports_cssutils(data, inline=False): - """Return all assets that are referenced in the given CSS document. - - The returned URLs are relative to the stylesheet's URL. - - Args: - data: The content of the stylesheet to scan as string. - inline: True if the argument is an inline HTML style attribute. - """ - try: - import cssutils - except ImportError: - return None - - # We don't care about invalid CSS data, this will only litter the log - # output with CSS errors - parser = cssutils.CSSParser(loglevel=100, - fetcher=lambda url: (None, ""), validate=False) - if not inline: - sheet = parser.parseString(data) - return list(cssutils.getUrls(sheet)) - else: - urls = [] - declaration = parser.parseStyle(data) - # prop = background, color, margin, ... - for prop in declaration: - # value = red, 10px, url(foobar), ... - for value in prop.propertyValue: - if isinstance(value, cssutils.css.URIValue): - if value.uri: - urls.append(value.uri) - return urls - - -def _get_css_imports(data, inline=False): - """Return all assets that are referenced in the given CSS document. - - The returned URLs are relative to the stylesheet's URL. - - Args: - data: The content of the stylesheet to scan as string. - inline: True if the argument is an inline HTML style attribute. - """ - imports = _get_css_imports_cssutils(data, inline) - if imports is None: - imports = _get_css_imports_regex(data) - return imports - - def _check_rel(element): """Return true if the element's rel attribute fits our criteria. @@ -328,7 +279,7 @@ class _Downloader: for element in web_frame.findAllElements('[style]'): element = webkitelem.WebKitElement(element, tab=self.tab) style = element['style'] - for element_url in _get_css_imports(style, inline=True): + for element_url in _get_css_imports(style): self._fetch_url(web_url.resolved(QUrl(element_url))) # Shortcut if no assets need to be downloaded, otherwise the file would diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index ce25d7d28..6d2c2f147 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -395,6 +395,7 @@ def _lookup_path(cmd): directories = [ os.path.join(standarddir.data(), "userscripts"), os.path.join(standarddir.data(system=True), "userscripts"), + os.path.join(standarddir.config(), "userscripts"), ] for directory in directories: cmd_path = os.path.join(directory, cmd) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index a8e58b8a2..4e1290f82 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -47,6 +47,7 @@ class _Highlighter(QSyntaxHighlighter): self._expression = QRegularExpression( pat, QRegularExpression.CaseInsensitiveOption ) + qtutils.ensure_valid(self._expression) def highlightBlock(self, text): """Override highlightBlock for custom highlighting.""" diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 1f5304b61..86de688a0 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -331,7 +331,8 @@ class CompletionView(QTreeView): QItemSelectionModel.Rows) # if the last item is focused, try to fetch more - if idx.row() == self.model().rowCount(idx.parent()) - 1: + next_idx = self.indexBelow(idx) + if not self.visualRect(next_idx).isValid(): self.expandAll() count = self.model().count() diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py index f0cc21da0..d4193b6d8 100644 --- a/qutebrowser/completion/models/listcategory.py +++ b/qutebrowser/completion/models/listcategory.py @@ -22,7 +22,7 @@ import re from typing import Iterable, Tuple -from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp +from PyQt5.QtCore import QSortFilterProxyModel, QRegularExpression from PyQt5.QtGui import QStandardItem, QStandardItemModel from PyQt5.QtWidgets import QWidget @@ -63,8 +63,9 @@ class ListCategory(QSortFilterProxyModel): val = re.sub(r' +', r' ', val) # See #1919 val = re.escape(val) val = val.replace(r'\ ', '.*') - rx = QRegExp(val, Qt.CaseInsensitive) - self.setFilterRegExp(rx) + rx = QRegularExpression(val, QRegularExpression.CaseInsensitiveOption) + qtutils.ensure_valid(rx) + self.setFilterRegularExpression(rx) self.invalidate() sortcol = 0 self.sort(sortcol) diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index 19ad126c1..f553fce3b 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -343,10 +343,3 @@ def devtools_focus(tab: apitypes.Tab) -> None: tab.data.splitter.cycle_focus() except apitypes.InspectorError as e: raise cmdutils.CommandError(e) - - -@cmdutils.register(deprecated='Use :devtools instead') -@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def inspector(tab: apitypes.Tab) -> None: - """Toggle the web inspector.""" - devtools(tab) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index a0739af4d..0f57b0a03 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -259,17 +259,9 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('win_id', value=cmdutils.Value.win_id) - def config_diff(self, win_id: int, old: bool = False) -> None: - """Show all customized options. - - Args: - old: Show difference for the pre-v1.0 files - (qutebrowser.conf/keys.conf). - """ + def config_diff(self, win_id: int) -> None: + """Show all customized options.""" url = QUrl('qute://configdiff') - if old: - url.setPath('/old') - tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.load_url(url, newtab=False) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index da64c0f6a..645342767 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2074,6 +2074,19 @@ window.title_format: Format to use for the window title. The same placeholders like for `tabs.title.format` are defined. +window.transparent: + type: Bool + default: false + desc: | + Set the main window background to transparent. + + This allows having a transparent tab- or statusbar (might require a compositor such + as picom). However, it breaks some functionality such as dmenu embedding via its + `-w` option. On some systems, it was additionally reported that main window + transparency negatively affects performance. + + Note this setting only affects windows opened after setting it. + ## zoom zoom.default: diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py deleted file mode 100644 index 42215c093..000000000 --- a/qutebrowser/config/configdiff.py +++ /dev/null @@ -1,761 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2017-2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. - -"""Code to show a diff of the legacy config format.""" - -import difflib -import os.path -from typing import MutableSequence - -import pygments -import pygments.lexers -import pygments.formatters - -from qutebrowser.utils import standarddir - - -OLD_CONF = """ -[general] -ignore-case = smart -startpage = https://start.duckduckgo.com -yank-ignored-url-parameters = ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content -default-open-dispatcher = -default-page = ${startpage} -auto-search = naive -auto-save-config = true -auto-save-interval = 15000 -editor = gvim -f "{}" -editor-encoding = utf-8 -private-browsing = false -developer-extras = false -print-element-backgrounds = true -xss-auditing = false -default-encoding = iso-8859-1 -new-instance-open-target = tab -new-instance-open-target.window = last-focused -log-javascript-console = debug -save-session = false -session-default-name = -url-incdec-segments = path,query -[ui] -history-session-interval = 30 -zoom-levels = 25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500% -default-zoom = 100% -downloads-position = top -status-position = bottom -message-timeout = 2000 -message-unfocused = false -confirm-quit = never -zoom-text-only = false -frame-flattening = false -user-stylesheet = -hide-scrollbar = true -smooth-scrolling = false -remove-finished-downloads = -1 -hide-statusbar = false -statusbar-padding = 1,1,0,0 -window-title-format = {perc}{title}{title_sep}qutebrowser -modal-js-dialog = false -hide-wayland-decoration = false -keyhint-blacklist = -keyhint-delay = 500 -prompt-radius = 8 -prompt-filebrowser = true -[network] -do-not-track = true -accept-language = en-US,en -referer-header = same-domain -user-agent = -proxy = system -proxy-dns-requests = true -ssl-strict = ask -dns-prefetch = true -custom-headers = -netrc-file = -[completion] -show = always -download-path-suggestion = path -timestamp-format = %Y-%m-%d -height = 50% -cmd-history-max-items = 100 -web-history-max-items = -1 -quick-complete = true -shrink = false -scrollbar-width = 12 -scrollbar-padding = 2 -[input] -timeout = 500 -partial-timeout = 5000 -insert-mode-on-plugins = false -auto-leave-insert-mode = true -auto-insert-mode = false -forward-unbound-keys = auto -spatial-navigation = false -links-included-in-focus-chain = true -rocker-gestures = false -mouse-zoom-divider = 512 -[tabs] -background-tabs = false -select-on-remove = next -new-tab-position = next -new-tab-position-explicit = last -last-close = ignore -show = always -show-switching-delay = 800 -wrap = true -movable = true -close-mouse-button = middle -position = top -show-favicons = true -favicon-scale = 1.0 -width = 20% -pinned-width = 43 -indicator-width = 3 -tabs-are-windows = false -title-format = {index}: {title} -title-format-pinned = {index} -title-alignment = left -mousewheel-tab-switching = true -padding = 0,0,5,5 -indicator-padding = 2,2,0,4 -[storage] -download-directory = -prompt-download-directory = true -remember-download-directory = true -maximum-pages-in-cache = 0 -offline-web-application-cache = true -local-storage = true -cache-size = -[content] -allow-images = true -allow-javascript = true -allow-plugins = false -webgl = true -hyperlink-auditing = false -geolocation = ask -notifications = ask -media-capture = ask -javascript-can-open-windows-automatically = false -javascript-can-close-windows = false -javascript-can-access-clipboard = false -ignore-javascript-prompt = false -ignore-javascript-alert = false -local-content-can-access-remote-urls = false -local-content-can-access-file-urls = true -cookies-accept = no-3rdparty -cookies-store = true -host-block-lists = https://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext -host-blocking-enabled = true -host-blocking-whitelist = piwik.org -enable-pdfjs = false -[hints] -border = 1px solid #E3BE23 -mode = letter -chars = asdfghjkl -min-chars = 1 -scatter = true -uppercase = false -dictionary = /usr/share/dict/words -auto-follow = unique-match -auto-follow-timeout = 0 -next-regexes = \\bnext\\b,\\bmore\\b,\\bnewer\\b,\\b[>\u2192\u226b]\\b,\\b(>>|\xbb)\\b,\\bcontinue\\b -prev-regexes = \\bprev(ious)?\\b,\\bback\\b,\\bolder\\b,\\b[<\u2190\u226a]\\b,\\b(<<|\xab)\\b -find-implementation = python -hide-unmatched-rapid-hints = true -[searchengines] -DEFAULT = https://duckduckgo.com/?q={} -[aliases] -[colors] -completion.fg = white -completion.bg = #333333 -completion.alternate-bg = #444444 -completion.category.fg = white -completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050) -completion.category.border.top = black -completion.category.border.bottom = ${completion.category.border.top} -completion.item.selected.fg = black -completion.item.selected.bg = #e8c000 -completion.item.selected.border.top = #bbbb00 -completion.item.selected.border.bottom = ${completion.item.selected.border.top} -completion.match.fg = #ff4444 -completion.scrollbar.fg = ${completion.fg} -completion.scrollbar.bg = ${completion.bg} -statusbar.fg = white -statusbar.bg = black -statusbar.fg.private = ${statusbar.fg} -statusbar.bg.private = #666666 -statusbar.fg.insert = ${statusbar.fg} -statusbar.bg.insert = darkgreen -statusbar.fg.command = ${statusbar.fg} -statusbar.bg.command = ${statusbar.bg} -statusbar.fg.command.private = ${statusbar.fg.private} -statusbar.bg.command.private = ${statusbar.bg.private} -statusbar.fg.caret = ${statusbar.fg} -statusbar.bg.caret = purple -statusbar.fg.caret-selection = ${statusbar.fg} -statusbar.bg.caret-selection = #a12dff -statusbar.progress.bg = white -statusbar.url.fg = ${statusbar.fg} -statusbar.url.fg.success = white -statusbar.url.fg.success.https = lime -statusbar.url.fg.error = orange -statusbar.url.fg.warn = yellow -statusbar.url.fg.hover = aqua -tabs.fg.odd = white -tabs.bg.odd = grey -tabs.fg.even = white -tabs.bg.even = darkgrey -tabs.fg.selected.odd = white -tabs.bg.selected.odd = black -tabs.fg.selected.even = ${tabs.fg.selected.odd} -tabs.bg.selected.even = ${tabs.bg.selected.odd} -tabs.bg.bar = #555555 -tabs.indicator.start = #0000aa -tabs.indicator.stop = #00aa00 -tabs.indicator.error = #ff0000 -tabs.indicator.system = rgb -hints.fg = black -hints.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8)) -hints.fg.match = green -downloads.bg.bar = black -downloads.fg.start = white -downloads.bg.start = #0000aa -downloads.fg.stop = ${downloads.fg.start} -downloads.bg.stop = #00aa00 -downloads.fg.system = rgb -downloads.bg.system = rgb -downloads.fg.error = white -downloads.bg.error = red -webpage.bg = white -keyhint.fg = #FFFFFF -keyhint.fg.suffix = #FFFF00 -keyhint.bg = rgba(0, 0, 0, 80%) -messages.fg.error = white -messages.bg.error = red -messages.border.error = #bb0000 -messages.fg.warning = white -messages.bg.warning = darkorange -messages.border.warning = #d47300 -messages.fg.info = white -messages.bg.info = black -messages.border.info = #333333 -prompts.fg = white -prompts.bg = darkblue -prompts.selected.bg = #308cc6 -[fonts] -_monospace = xos4 Terminus, Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal -completion = 8pt ${_monospace} -completion.category = bold ${completion} -tabbar = 8pt ${_monospace} -statusbar = 8pt ${_monospace} -downloads = 8pt ${_monospace} -hints = bold 13px ${_monospace} -debug-console = 8pt ${_monospace} -web-family-standard = -web-family-fixed = -web-family-serif = -web-family-sans-serif = -web-family-cursive = -web-family-fantasy = -web-size-minimum = 0 -web-size-minimum-logical = 6 -web-size-default = 16 -web-size-default-fixed = 13 -keyhint = 8pt ${_monospace} -messages.error = 8pt ${_monospace} -messages.warning = 8pt ${_monospace} -messages.info = 8pt ${_monospace} -prompts = 8pt sans-serif -""" - -OLD_KEYS_CONF = """ -[!normal] -leave-mode - <escape> - <ctrl-[> -[normal] -clear-keychain ;; search ;; fullscreen --leave - <escape> - <ctrl-[> -set-cmd-text -s :open - o -set-cmd-text :open {url:pretty} - go -set-cmd-text -s :open -t - O -set-cmd-text :open -t -i {url:pretty} - gO -set-cmd-text -s :open -b - xo -set-cmd-text :open -b -i {url:pretty} - xO -set-cmd-text -s :open -w - wo -set-cmd-text :open -w {url:pretty} - wO -set-cmd-text / - / -set-cmd-text ? - ? -set-cmd-text : - : -open -t - ga - <ctrl-t> -open -w - <ctrl-n> -tab-close - d - <ctrl-w> -tab-close -o - D -tab-only - co -tab-focus - T -tab-move - gm -tab-move - - gl -tab-move + - gr -tab-next - J - <ctrl-pgdown> -tab-prev - K - <ctrl-pgup> -tab-clone - gC -reload - r - <f5> -reload -f - R - <ctrl-f5> -back - H - <back> -back -t - th -back -w - wh -forward - L - <forward> -forward -t - tl -forward -w - wl -fullscreen - <f11> -hint - f -hint all tab - F -hint all window - wf -hint all tab-bg - ;b -hint all tab-fg - ;f -hint all hover - ;h -hint images - ;i -hint images tab - ;I -hint links fill :open {hint-url} - ;o -hint links fill :open -t -i {hint-url} - ;O -hint links yank - ;y -hint links yank-primary - ;Y -hint --rapid links tab-bg - ;r -hint --rapid links window - ;R -hint links download - ;d -hint inputs - ;t -scroll left - h -scroll down - j -scroll up - k -scroll right - l -undo - u - <ctrl-shift-t> -scroll-perc 0 - gg -scroll-perc - G -search-next - n -search-prev - N -enter-mode insert - i -enter-mode caret - v -enter-mode set_mark - ` -enter-mode jump_mark - ' -yank - yy -yank -s - yY -yank title - yt -yank title -s - yT -yank domain - yd -yank domain -s - yD -yank pretty-url - yp -yank pretty-url -s - yP -open -- {clipboard} - pp -open -- {primary} - pP -open -t -- {clipboard} - Pp -open -t -- {primary} - PP -open -w -- {clipboard} - wp -open -w -- {primary} - wP -quickmark-save - m -set-cmd-text -s :quickmark-load - b -set-cmd-text -s :quickmark-load -t - B -set-cmd-text -s :quickmark-load -w - wb -bookmark-add - M -set-cmd-text -s :bookmark-load - gb -set-cmd-text -s :bookmark-load -t - gB -set-cmd-text -s :bookmark-load -w - wB -save - sf -set-cmd-text -s :set - ss -set-cmd-text -s :set -t - sl -set-cmd-text -s :bind - sk -zoom-out - - -zoom-in - + -zoom - = -navigate prev - [[ -navigate next - ]] -navigate prev -t - {{ -navigate next -t - }} -navigate up - gu -navigate up -t - gU -navigate increment - <ctrl-a> -navigate decrement - <ctrl-x> -inspector - wi -download - gd -download-cancel - ad -download-clear - cd -view-source - gf -set-cmd-text -s :buffer - gt -tab-focus last - <ctrl-tab> - <ctrl-6> - <ctrl-^> -enter-mode passthrough - <ctrl-v> -quit - <ctrl-q> - ZQ -wq - ZZ -scroll-page 0 1 - <ctrl-f> -scroll-page 0 -1 - <ctrl-b> -scroll-page 0 0.5 - <ctrl-d> -scroll-page 0 -0.5 - <ctrl-u> -tab-focus 1 - <alt-1> - g0 - g^ -tab-focus 2 - <alt-2> -tab-focus 3 - <alt-3> -tab-focus 4 - <alt-4> -tab-focus 5 - <alt-5> -tab-focus 6 - <alt-6> -tab-focus 7 - <alt-7> -tab-focus 8 - <alt-8> -tab-focus -1 - <alt-9> - g$ -home - <ctrl-h> -stop - <ctrl-s> -print - <ctrl-alt-p> -open qute://settings - Ss -follow-selected - <return> - <ctrl-m> - <ctrl-j> - <shift-return> - <enter> - <shift-enter> -follow-selected -t - <ctrl-return> - <ctrl-enter> -repeat-command - . -tab-pin - <ctrl-p> -record-macro - q -run-macro - @ -[insert] -open-editor - <ctrl-e> -insert-text {primary} - <shift-ins> -[hint] -follow-hint - <return> - <ctrl-m> - <ctrl-j> - <shift-return> - <enter> - <shift-enter> -hint --rapid links tab-bg - <ctrl-r> -hint links - <ctrl-f> -hint all tab-bg - <ctrl-b> -[passthrough] -[command] -command-history-prev - <ctrl-p> -command-history-next - <ctrl-n> -completion-item-focus prev - <shift-tab> - <up> -completion-item-focus next - <tab> - <down> -completion-item-focus next-category - <ctrl-tab> -completion-item-focus prev-category - <ctrl-shift-tab> -completion-item-del - <ctrl-d> -command-accept - <return> - <ctrl-m> - <ctrl-j> - <shift-return> - <enter> - <shift-enter> -[prompt] -prompt-accept - <return> - <ctrl-m> - <ctrl-j> - <shift-return> - <enter> - <shift-enter> -prompt-accept yes - y -prompt-accept no - n -prompt-open-download - <ctrl-x> -prompt-item-focus prev - <shift-tab> - <up> -prompt-item-focus next - <tab> - <down> -[command,prompt] -rl-backward-char - <ctrl-b> -rl-forward-char - <ctrl-f> -rl-backward-word - <alt-b> -rl-forward-word - <alt-f> -rl-beginning-of-line - <ctrl-a> -rl-end-of-line - <ctrl-e> -rl-unix-line-discard - <ctrl-u> -rl-kill-line - <ctrl-k> -rl-kill-word - <alt-d> -rl-unix-word-rubout - <ctrl-w> -rl-backward-kill-word - <alt-backspace> -rl-yank - <ctrl-y> -rl-delete-char - <ctrl-?> -rl-backward-delete-char - <ctrl-h> -[caret] -toggle-selection - v - <space> -drop-selection - <ctrl-space> -enter-mode normal - c -move-to-next-line - j -move-to-prev-line - k -move-to-next-char - l -move-to-prev-char - h -move-to-end-of-word - e -move-to-next-word - w -move-to-prev-word - b -move-to-start-of-next-block - ] -move-to-start-of-prev-block - [ -move-to-end-of-next-block - } -move-to-end-of-prev-block - { -move-to-start-of-line - 0 -move-to-end-of-line - $ -move-to-start-of-document - gg -move-to-end-of-document - G -yank selection -s - Y -yank selection - y - <return> - <ctrl-m> - <ctrl-j> - <shift-return> - <enter> - <shift-enter> -scroll left - H -scroll down - J -scroll up - K -scroll right - L -""" - - -def get_diff() -> str: - """Get a HTML diff for the old config files.""" - old_conf_lines: MutableSequence[str] = [] - old_key_lines: MutableSequence[str] = [] - - for filename, dest in [('qutebrowser.conf', old_conf_lines), - ('keys.conf', old_key_lines)]: - path = os.path.join(standarddir.config(), filename) - - with open(path, 'r', encoding='utf-8') as f: - for line in f: - if not line.strip() or line.startswith('#'): - continue - dest.append(line.rstrip()) - - conf_delta = difflib.unified_diff(OLD_CONF.lstrip().splitlines(), - old_conf_lines) - key_delta = difflib.unified_diff(OLD_KEYS_CONF.lstrip().splitlines(), - old_key_lines) - - conf_diff = '\n'.join(conf_delta) - key_diff = '\n'.join(key_delta) - - # pylint: disable=no-member - # WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/ - lexer = pygments.lexers.DiffLexer() - formatter = pygments.formatters.HtmlFormatter( - full=True, linenos='table', - title='Diffing pre-1.0 default config with pre-1.0 modified config') - # pylint: enable=no-member - return pygments.highlight(conf_diff + key_diff, lexer, formatter) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 9185ee6ef..6328c3140 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -56,8 +56,8 @@ from typing import (Any, Callable, Dict as DictType, Iterable, Iterator, import attr import yaml from PyQt5.QtCore import QUrl, Qt -from PyQt5.QtGui import QColor, QFontDatabase -from PyQt5.QtWidgets import QTabWidget, QTabBar, QApplication +from PyQt5.QtGui import QColor +from PyQt5.QtWidgets import QTabWidget, QTabBar from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.misc import objects, debugcachestats @@ -96,9 +96,15 @@ class ValidValues: generate_docs: Whether to show the values in the docs. """ - def __init__(self, - *values: Union[str, DictType[str, str], Tuple[str, str]], - generate_docs: bool = True) -> None: + def __init__( + self, + *values: Union[ + str, + DictType[str, Optional[str]], + Tuple[str, Optional[str]], + ], + generate_docs: bool = True, + ) -> None: if not values: raise ValueError("ValidValues with no values makes no sense!") self.descriptions: DictType[str, str] = {} @@ -107,17 +113,18 @@ class ValidValues: for value in values: if isinstance(value, str): # Value without description - self.values.append(value) + val = value + desc = None elif isinstance(value, dict): # List of dicts from configdata.yml assert len(value) == 1, value - value, desc = list(value.items())[0] - self.values.append(value) - self.descriptions[value] = desc + val, desc = list(value.items())[0] else: - # (value, description) tuple - self.values.append(value[0]) - self.descriptions[value[0]] = value[1] + val, desc = value + + self.values.append(val) + if desc is not None: + self.descriptions[val] = desc def __contains__(self, val: str) -> bool: return val in self.values @@ -308,17 +315,10 @@ class BaseType: """ if self.valid_values is None: return None - else: - out = [] - for val in self.valid_values: - try: - desc = self.valid_values.descriptions[val] - except KeyError: - # Some values are self-explaining and don't need a - # description. - desc = "" - out.append((val, desc)) - return out + return [ + (val, self.valid_values.descriptions.get(val, "")) + for val in self.valid_values + ] def __repr__(self) -> str: return utils.get_repr(self, none_ok=self.none_ok) @@ -329,14 +329,15 @@ class MappingType(BaseType): """Base class for any setting which has a mapping to the given values. Attributes: - MAPPING: The mapping to use. + MAPPING: A mapping from config values to (translated_value, docs) tuples. """ - MAPPING: DictType[str, Any] = {} + MAPPING: DictType[str, Tuple[Any, Optional[str]]] = {} - def __init__(self, none_ok: bool = False, valid_values: ValidValues = None) -> None: + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) - self.valid_values = valid_values + self.valid_values = ValidValues( + *[(key, doc) for (key, (_val, doc)) in self.MAPPING.items()]) def to_py(self, value: Any) -> Any: self._basic_py_validation(value, str) @@ -345,7 +346,8 @@ class MappingType(BaseType): elif not value: return None self._validate_valid_values(value.lower()) - return self.MAPPING[value.lower()] + mapped, _doc = self.MAPPING[value.lower()] + return mapped def __repr__(self) -> str: return utils.get_repr(self, none_ok=self.none_ok, @@ -998,20 +1000,11 @@ class ColorSystem(MappingType): """The color system to use for color interpolation.""" - def __init__(self, none_ok: bool = False) -> None: - super().__init__( - none_ok, - valid_values=ValidValues( - ('rgb', "Interpolate in the RGB color system."), - ('hsv', "Interpolate in the HSV color system."), - ('hsl', "Interpolate in the HSL color system."), - ('none', "Don't show a gradient."))) - MAPPING = { - 'rgb': QColor.Rgb, - 'hsv': QColor.Hsv, - 'hsl': QColor.Hsl, - 'none': None, + 'rgb': (QColor.Rgb, "Interpolate in the RGB color system."), + 'hsv': (QColor.Hsv, "Interpolate in the HSV color system."), + 'hsl': (QColor.Hsl, "Interpolate in the HSL color system."), + 'none': (None, "Don't show a gradient."), } @@ -1019,19 +1012,13 @@ class IgnoreCase(MappingType): """Whether to search case insensitively.""" - def __init__(self, none_ok: bool = False) -> None: - super().__init__( - none_ok, - valid_values=ValidValues( - ('always', "Search case-insensitively."), - ('never', "Search case-sensitively."), - ('smart', ("Search case-sensitively if there are capital " - "characters.")))) - MAPPING = { - 'always': usertypes.IgnoreCase.always, - 'never': usertypes.IgnoreCase.never, - 'smart': usertypes.IgnoreCase.smart, + 'always': (usertypes.IgnoreCase.always, "Search case-insensitively."), + 'never': (usertypes.IgnoreCase.never, "Search case-sensitively."), + 'smart': ( + usertypes.IgnoreCase.smart, + "Search case-sensitively if there are capital characters." + ), } @@ -1172,50 +1159,11 @@ class FontBase(BaseType): If the given family value (fonts.default_family in the config) is unset, a system-specific default monospace font is used. - - Note that (at least) three ways of getting the default monospace font - exist: - - 1) f = QFont() - f.setStyleHint(QFont.Monospace) - print(f.defaultFamily()) - - 2) f = QFont() - f.setStyleHint(QFont.TypeWriter) - print(f.defaultFamily()) - - 3) f = QFontDatabase.systemFont(QFontDatabase.FixedFont) - print(f.family()) - - They yield different results depending on the OS: - - QFont.Monospace | QFont.TypeWriter | QFontDatabase - ------------------------------------------------------ - Windows: Courier New | Courier New | Courier New - Linux: DejaVu Sans Mono | DejaVu Sans Mono | monospace - macOS: Menlo | American Typewriter | Monaco - - Test script: https://p.cmpl.cc/d4dfe573 - - On Linux, it seems like both actually resolve to the same font. - - On macOS, "American Typewriter" looks like it indeed tries to imitate a - typewriter, so it's not really a suitable UI font. - - Looking at those Wikipedia articles: - - https://en.wikipedia.org/wiki/Monaco_(typeface) - https://en.wikipedia.org/wiki/Menlo_(typeface) - - the "right" choice isn't really obvious. Thus, let's go for the - QFontDatabase approach here, since it's by far the simplest one. """ if default_family: families = configutils.FontFamilies(default_family) else: - assert QApplication.instance() is not None - font = QFontDatabase.systemFont(QFontDatabase.FixedFont) - families = configutils.FontFamilies([font.family()]) + families = configutils.FontFamilies.from_system_default() cls.default_family = families.to_str(quote=True) cls.default_size = default_size @@ -1753,33 +1701,23 @@ class Position(MappingType): """The position of the tab bar.""" MAPPING = { - 'top': QTabWidget.North, - 'bottom': QTabWidget.South, - 'left': QTabWidget.West, - 'right': QTabWidget.East, + 'top': (QTabWidget.North, None), + 'bottom': (QTabWidget.South, None), + 'left': (QTabWidget.West, None), + 'right': (QTabWidget.East, None), } - def __init__(self, none_ok: bool = False) -> None: - super().__init__( - none_ok, - valid_values=ValidValues('top', 'bottom', 'left', 'right')) - class TextAlignment(MappingType): """Alignment of text.""" MAPPING = { - 'left': Qt.AlignLeft, - 'right': Qt.AlignRight, - 'center': Qt.AlignCenter, + 'left': (Qt.AlignLeft, None), + 'right': (Qt.AlignRight, None), + 'center': (Qt.AlignCenter, None), } - def __init__(self, none_ok: bool = False) -> None: - super().__init__( - none_ok, - valid_values=ValidValues('left', 'right', 'center')) - class VerticalPosition(String): @@ -1828,21 +1766,22 @@ class SelectOnRemove(MappingType): """Which tab to select when the focused tab is removed.""" MAPPING = { - 'prev': QTabBar.SelectLeftTab, - 'next': QTabBar.SelectRightTab, - 'last-used': QTabBar.SelectPreviousTab, + 'prev': ( + QTabBar.SelectLeftTab, + ("Select the tab which came before the closed one " + "(left in horizontal, above in vertical)."), + ), + 'next': ( + QTabBar.SelectRightTab, + ("Select the tab which came after the closed one " + "(right in horizontal, below in vertical)."), + ), + 'last-used': ( + QTabBar.SelectPreviousTab, + "Select the previously selected tab.", + ), } - def __init__(self, none_ok: bool = False) -> None: - super().__init__( - none_ok, - valid_values=ValidValues( - ('prev', "Select the tab which came before the closed one " - "(left in horizontal, above in vertical)."), - ('next', "Select the tab which came after the closed one " - "(right in horizontal, below in vertical)."), - ('last-used', "Select the previously selected tab."))) - class ConfirmQuit(FlagList): diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index 7c2d4ee8c..e7a60a7eb 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -29,6 +29,8 @@ from typing import ( MutableMapping) from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QFontDatabase +from PyQt5.QtWidgets import QApplication from qutebrowser.utils import utils, urlmatch, usertypes, qtutils from qutebrowser.config import configexc @@ -280,6 +282,9 @@ class FontFamilies: def __iter__(self) -> Iterator[str]: yield from self._families + def __len__(self) -> int: + return len(self._families) + def __repr__(self) -> str: return utils.get_repr(self, families=self._families, constructor=True) @@ -288,7 +293,7 @@ class FontFamilies: def _quoted_families(self) -> Iterator[str]: for f in self._families: - needs_quoting = any(c in f for c in ', ') + needs_quoting = any(c in f for c in '., ') yield '"{}"'.format(f) if needs_quoting else f def to_str(self, *, quote: bool = True) -> str: @@ -296,6 +301,57 @@ class FontFamilies: return ', '.join(families) @classmethod + def from_system_default( + cls, + font_type: QFontDatabase.SystemFont = QFontDatabase.FixedFont, + ) -> 'FontFamilies': + """Get a FontFamilies object for the default system font. + + By default, the monospace font is returned, though via the "font_type" argument, + other types can be requested as well. + + Note that (at least) three ways of getting the default monospace font + exist: + + 1) f = QFont() + f.setStyleHint(QFont.Monospace) + print(f.defaultFamily()) + + 2) f = QFont() + f.setStyleHint(QFont.TypeWriter) + print(f.defaultFamily()) + + 3) f = QFontDatabase.systemFont(QFontDatabase.FixedFont) + print(f.family()) + + They yield different results depending on the OS: + + QFont.Monospace | QFont.TypeWriter | QFontDatabase + ------------------------------------------------------ + Windows: Courier New | Courier New | Courier New + Linux: DejaVu Sans Mono | DejaVu Sans Mono | monospace + macOS: Menlo | American Typewriter | Monaco + + Test script: https://p.cmpl.cc/d4dfe573 + + On Linux, it seems like both actually resolve to the same font. + + On macOS, "American Typewriter" looks like it indeed tries to imitate a + typewriter, so it's not really a suitable UI font. + + Looking at those Wikipedia articles: + + https://en.wikipedia.org/wiki/Monaco_(typeface) + https://en.wikipedia.org/wiki/Menlo_(typeface) + + the "right" choice isn't really obvious. Thus, let's go for the + QFontDatabase approach here, since it's by far the simplest one. + """ + assert QApplication.instance() is not None + font = QFontDatabase.systemFont(font_type) + return cls([font.family()]) + + @classmethod def from_str(cls, family_str: str) -> 'FontFamilies': """Parse a CSS-like string of font families.""" families = [] diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py index 790fbaca3..2136f7e7f 100644 --- a/qutebrowser/config/qtargs.py +++ b/qutebrowser/config/qtargs.py @@ -109,6 +109,16 @@ def _qtwebengine_enabled_features(feature_flags: Sequence[str]) -> Iterator[str] if config.val.scrolling.bar == 'overlay': yield 'OverlayScrollbar' + if (qtutils.version_check('5.14', compiled=False) and + config.val.content.headers.referer == 'same-domain'): + # Handling of reduced-referrer-granularity in Chromium 76+ + # https://chromium-review.googlesource.com/c/chromium/src/+/1572699 + # + # Note that this is removed entirely (and apparently the default) starting with + # Chromium 89 (Qt 5.15.x or 6.x): + # https://chromium-review.googlesource.com/c/chromium/src/+/2545444 + yield 'ReducedReferrerGranularity' + def _qtwebengine_args( namespace: argparse.Namespace, @@ -145,8 +155,7 @@ def _qtwebengine_args( from qutebrowser.browser.webengine import darkmode blink_settings = list(darkmode.settings()) if blink_settings: - yield '--blink-settings=' + ','.join('{}={}'.format(k, v) - for k, v in blink_settings) + yield '--blink-settings=' + ','.join(f'{k}={v}' for k, v in blink_settings) enabled_features = list(_qtwebengine_enabled_features(feature_flags)) if enabled_features: @@ -191,17 +200,33 @@ def _qtwebengine_settings_args() -> Iterator[str]: }, 'content.headers.referer': { 'always': None, - 'never': '--no-referrers', - 'same-domain': '--reduced-referrer-granularity', } } - if qtutils.version_check('5.14'): + if (qtutils.version_check('5.14', compiled=False) and + not qtutils.version_check('5.15.2', compiled=False)): + # In Qt 5.14 to 5.15.1, `--force-dark-mode` is used to set the + # preferred colorscheme. In Qt 5.15.2, this is handled by a + # blink-setting instead. settings['colors.webpage.prefers_color_scheme_dark'] = { True: '--force-dark-mode', False: None, } + referrer_setting = settings['content.headers.referer'] + if qtutils.version_check('5.14', compiled=False): + # Starting with Qt 5.14, this is handled via --enable-features + referrer_setting['same-domain'] = None + else: + referrer_setting['same-domain'] = '--reduced-referrer-granularity' + + can_override_referer = ( + qtutils.version_check('5.12.4', compiled=False) and + not qtutils.version_check('5.13.0', compiled=False, exact=True) + ) + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60203 + referrer_setting['never'] = None if can_override_referer else '--no-referrers' + for setting, args in sorted(settings.items()): arg = args[config.instance.get(setting)] if arg is not None: diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml index cbb8e17c0..939500aa3 100644 --- a/qutebrowser/javascript/.eslintrc.yaml +++ b/qutebrowser/javascript/.eslintrc.yaml @@ -12,9 +12,7 @@ env: browser: true - -parserOptions: - ecmaVersion: 6 + es6: true extends: "eslint:all" @@ -31,7 +29,7 @@ rules: init-declarations: "off" no-plusplus: "off" no-extra-parens: "off" - id-length: ["error", {"exceptions": ["i", "k", "x", "y"]}] + id-length: ["error", {"exceptions": ["i", "k", "v", "x", "y"]}] object-shorthand: "off" max-statements: ["error", {"max": 40}] quotes: ["error", "double", {"avoidEscape": true}] @@ -43,7 +41,7 @@ rules: func-names: "off" sort-keys: "off" no-warning-comments: "off" - max-len: ["error", {"ignoreUrls": true}] + max-len: ["error", {"ignoreUrls": true, "code": 88}] capitalized-comments: "off" prefer-destructuring: "off" line-comment-position: "off" diff --git a/qutebrowser/javascript/object_fromentries_quirk.user.js b/qutebrowser/javascript/object_fromentries_quirk.user.js new file mode 100644 index 000000000..6f6ad8b31 --- /dev/null +++ b/qutebrowser/javascript/object_fromentries_quirk.user.js @@ -0,0 +1,46 @@ +// Based on: https://gitlab.com/moongoal/js-polyfill-object.fromentries/-/tree/master + +/* + Copyright 2018 Alfredo Mungo <alfredo.mungo@protonmail.ch> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +*/ + +"use strict"; + +if (!Object.fromEntries) { + Object.defineProperty(Object, "fromEntries", { + value(entries) { + if (!entries || !entries[Symbol.iterator]) { + throw new Error( + "Object.fromEntries() requires a single iterable argument"); + } + + const obj = {}; + + Object.keys(entries).forEach((key) => { + const [k, v] = entries[key]; + obj[k] = v; + }); + + return obj; + }, + }); +} + diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index b8228545a..6273b3382 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -203,8 +203,10 @@ class MainWindow(QWidget): from qutebrowser.mainwindow.statusbar import bar self.setAttribute(Qt.WA_DeleteOnClose) - self.setAttribute(Qt.WA_TranslucentBackground) - self.palette().setColor(QPalette.Window, Qt.transparent) + if config.val.window.transparent: + self.setAttribute(Qt.WA_TranslucentBackground) + self.palette().setColor(QPalette.Window, Qt.transparent) + self._overlays: MutableSequence[_OverlayInfoType] = [] self.win_id = next(win_id_gen) self.registry = objreg.ObjectRegistry() diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 9bb8d34ce..c67e5fa0e 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -863,7 +863,7 @@ class TabbedBrowser(QWidget): start = config.cache['colors.tabs.indicator.start'] stop = config.cache['colors.tabs.indicator.stop'] system = config.cache['colors.tabs.indicator.system'] - color = utils.interpolate_color(start, stop, perc, system) + color = qtutils.interpolate_color(start, stop, perc, system) self.widget.set_tab_indicator_color(idx, color) self.widget.update_tab_title(idx) if idx == self.widget.currentIndex(): @@ -880,7 +880,7 @@ class TabbedBrowser(QWidget): start = config.cache['colors.tabs.indicator.start'] stop = config.cache['colors.tabs.indicator.stop'] system = config.cache['colors.tabs.indicator.system'] - color = utils.interpolate_color(start, stop, 100, system) + color = qtutils.interpolate_color(start, stop, 100, system) else: color = config.cache['colors.tabs.indicator.error'] self.widget.set_tab_indicator_color(idx, color) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index b40c59bd5..f853f8fd9 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -639,7 +639,7 @@ class TabBar(QTabBar): main_window = objreg.get('main-window', scope='window', window=self._win_id) perc = int(confwidth.rstrip('%')) - width = main_window.width() * perc / 100 + width = main_window.width() * perc // 100 else: width = int(confwidth) size = QSize(width, height) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 2bdb790e7..52cb8ad0c 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -30,7 +30,6 @@ import datetime import enum from typing import List, Tuple -import pkg_resources from PyQt5.QtCore import pyqtSlot, Qt, QSize from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QCheckBox, @@ -361,8 +360,8 @@ class _CrashDialog(QDialog): Args: newest: The newest version as a string. """ - new_version = pkg_resources.parse_version(newest) - cur_version = pkg_resources.parse_version(qutebrowser.__version__) + new_version = utils.parse_version(newest) + cur_version = utils.parse_version(qutebrowser.__version__) lines = ['The report has been sent successfully. Thanks!'] if new_version > cur_version: lines.append("<b>Note:</b> The newest available version is v{}, " diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 92920c72c..d1c57760e 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -170,13 +170,16 @@ def qt_version(qversion=None, qt_version_str=None): def check_qt_version(): """Check if the Qt version is recent enough.""" - from PyQt5.QtCore import (qVersion, QT_VERSION, PYQT_VERSION, - PYQT_VERSION_STR) - from pkg_resources import parse_version - parsed_qversion = parse_version(qVersion()) - - if (QT_VERSION < 0x050C00 or PYQT_VERSION < 0x050C00 or - parsed_qversion < parse_version('5.12.0')): + from PyQt5.QtCore import QT_VERSION, PYQT_VERSION, PYQT_VERSION_STR + try: + from PyQt5.QtCore import QVersionNumber, QLibraryInfo + qt_ver = QLibraryInfo.version().normalized() + recent_qt_runtime = qt_ver >= QVersionNumber(5, 12) # type: ignore[operator] + except (ImportError, AttributeError): + # QVersionNumber was added in Qt 5.6, QLibraryInfo.version() in 5.8 + recent_qt_runtime = False + + if QT_VERSION < 0x050C00 or PYQT_VERSION < 0x050C00 or not recent_qt_runtime: text = ("Fatal error: Qt >= 5.12.0 and PyQt >= 5.12.0 are required, " "but Qt {} / PyQt {} is installed.".format(qt_version(), PYQT_VERSION_STR)) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index bca1df020..7f36d4807 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -85,6 +85,11 @@ def get_argparser(): parser.add_argument('--json-args', help=argparse.SUPPRESS) parser.add_argument('--temp-basedir-restarted', help=argparse.SUPPRESS) + parser.add_argument('--desktop-file-name', + default="org.qutebrowser.qutebrowser", + 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") debug = parser.add_argument_group('debug arguments') debug.add_argument('-l', '--loglevel', dest='loglevel', diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 275da7c4c..cd6ea2b32 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -31,9 +31,8 @@ Module attributes: import io import operator import contextlib -from typing import TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, cast +from typing import TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, Tuple, cast -import pkg_resources from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR, PYQT_VERSION_STR, QObject, QUrl) @@ -48,7 +47,7 @@ if TYPE_CHECKING: from PyQt5.QtWebEngineWidgets import QWebEngineHistory from qutebrowser.misc import objects -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, utils MAXVALS = { @@ -100,15 +99,15 @@ def version_check(version: str, if compiled and exact: raise ValueError("Can't use compiled=True with exact=True!") - parsed = pkg_resources.parse_version(version) + parsed = utils.parse_version(version) op = operator.eq if exact else operator.ge - result = op(pkg_resources.parse_version(qVersion()), parsed) + result = op(utils.parse_version(qVersion()), parsed) if compiled and result: # qVersion() ==/>= parsed, now check if QT_VERSION_STR ==/>= parsed. - result = op(pkg_resources.parse_version(QT_VERSION_STR), parsed) + result = op(utils.parse_version(QT_VERSION_STR), parsed) if compiled and result: # Finally, check PYQT_VERSION_STR as well. - result = op(pkg_resources.parse_version(PYQT_VERSION_STR), parsed) + result = op(utils.parse_version(PYQT_VERSION_STR), parsed) return result @@ -118,8 +117,8 @@ MAX_WORLD_ID = 256 def is_new_qtwebkit() -> bool: """Check if the given version is a new QtWebKit.""" assert qWebKitVersion is not None - return (pkg_resources.parse_version(qWebKitVersion()) > - pkg_resources.parse_version('538.1')) + return (utils.parse_version(qWebKitVersion()) > + utils.parse_version('538.1')) def is_single_process() -> bool: @@ -157,19 +156,15 @@ def check_overflow(arg: int, ctype: str, fatal: bool = True) -> int: return arg -if TYPE_CHECKING: - # Protocol was added in Python 3.8 - from typing import Protocol - - class Validatable(Protocol): +class Validatable(utils.Protocol): - """An object with an isValid() method (e.g. QUrl).""" + """An object with an isValid() method (e.g. QUrl).""" - def isValid(self) -> bool: - ... + def isValid(self) -> bool: + ... -def ensure_valid(obj: 'Validatable') -> None: +def ensure_valid(obj: Validatable) -> None: """Ensure a Qt object with an .isValid() method is valid.""" if not obj.isValid(): raise QtValueError(obj) @@ -440,7 +435,7 @@ class QtValueError(ValueError): """Exception which gets raised by ensure_valid.""" - def __init__(self, obj: 'Validatable') -> None: + def __init__(self, obj: Validatable) -> None: try: self.reason = obj.errorString() # type: ignore[attr-defined] except AttributeError: @@ -474,3 +469,78 @@ class EventLoop(QEventLoop): status = super().exec_(flags) self._executing = False return status + + +def _get_color_percentage(x1: int, y1: int, z1: int, a1: int, + x2: int, y2: int, z2: int, a2: int, + percent: int) -> Tuple[int, int, int, int]: + """Get a color which is percent% interpolated between start and end. + + Args: + x1, y1, z1, a1 : Start color components (R, G, B, A / H, S, V, A / H, S, L, A) + x2, y2, z2, a2 : End color components (R, G, B, A / H, S, V, A / H, S, L, A) + percent: Percentage to interpolate, 0-100. + 0: Start color will be returned. + 100: End color will be returned. + + Return: + A (x, y, z, alpha) tuple with the interpolated color components. + """ + if not 0 <= percent <= 100: + raise ValueError("percent needs to be between 0 and 100!") + x = round(x1 + (x2 - x1) * percent / 100) + y = round(y1 + (y2 - y1) * percent / 100) + z = round(z1 + (z2 - z1) * percent / 100) + a = round(a1 + (a2 - a1) * percent / 100) + return (x, y, z, a) + + +def interpolate_color( + start: QColor, + end: QColor, + percent: int, + colorspace: Optional[QColor.Spec] = QColor.Rgb +) -> QColor: + """Get an interpolated color value. + + Args: + start: The start color. + end: The end color. + percent: Which value to get (0 - 100) + colorspace: The desired interpolation color system, + QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum) + If None, start is used except when percent is 100. + + Return: + The interpolated QColor, with the same spec as the given start color. + """ + ensure_valid(start) + ensure_valid(end) + + if colorspace is None: + if percent == 100: + return QColor(*end.getRgb()) + else: + return QColor(*start.getRgb()) + + out = QColor() + if colorspace == QColor.Rgb: + r1, g1, b1, a1 = start.getRgb() + r2, g2, b2, a2 = end.getRgb() + components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent) + out.setRgb(*components) + elif colorspace == QColor.Hsv: + h1, s1, v1, a1 = start.getHsv() + h2, s2, v2, a2 = end.getHsv() + components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent) + out.setHsv(*components) + elif colorspace == QColor.Hsl: + h1, s1, l1, a1 = start.getHsl() + h2, s2, l2, a2 = end.getHsl() + components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent) + out.setHsl(*components) + else: + raise ValueError("Invalid colorspace!") + out = out.convertTo(start.spec()) + ensure_valid(out) + return out diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index fe9461b2d..ff4afc83c 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -22,7 +22,6 @@ import os import os.path import sys -import shutil import contextlib import enum import argparse @@ -31,7 +30,7 @@ from typing import Iterator, Optional from PyQt5.QtCore import QStandardPaths from PyQt5.QtWidgets import QApplication -from qutebrowser.utils import log, debug, message, utils +from qutebrowser.utils import log, debug, utils # The cached locations _locations = {} @@ -129,11 +128,11 @@ def config_py() -> str: def _init_data(args: Optional[argparse.Namespace]) -> None: """Initialize the location for data.""" - typ = QStandardPaths.DataLocation + typ = QStandardPaths.AppDataLocation path = _from_args(typ, args) if path is None: if utils.is_windows: - app_data_path = _writable_location(QStandardPaths.AppDataLocation) + app_data_path = _writable_location(typ) # same location as config path = os.path.join(app_data_path, 'data') elif sys.platform.startswith('haiku'): # HaikuOS returns an empty value for AppDataLocation @@ -174,7 +173,7 @@ def _init_cache(args: Optional[argparse.Namespace]) -> None: if path is None: if utils.is_windows: # Local, not Roaming! - data_path = _writable_location(QStandardPaths.DataLocation) + data_path = _writable_location(QStandardPaths.AppLocalDataLocation) path = os.path.join(data_path, 'cache') else: path = _writable_location(typ) @@ -251,7 +250,7 @@ def _writable_location(typ: QStandardPaths.StandardLocation) -> str: # Types we are sure we handle correctly below. assert typ in [ - QStandardPaths.ConfigLocation, QStandardPaths.DataLocation, + QStandardPaths.ConfigLocation, QStandardPaths.AppLocalDataLocation, QStandardPaths.CacheLocation, QStandardPaths.DownloadLocation, QStandardPaths.RuntimeLocation, QStandardPaths.TempLocation, QStandardPaths.AppDataLocation], typ_str @@ -287,7 +286,8 @@ def _from_args( """ basedir_suffix = { QStandardPaths.ConfigLocation: 'config', - QStandardPaths.DataLocation: 'data', + QStandardPaths.AppDataLocation: 'data', + QStandardPaths.AppLocalDataLocation: 'data', QStandardPaths.CacheLocation: 'cache', QStandardPaths.DownloadLocation: 'download', QStandardPaths.RuntimeLocation: 'runtime', @@ -339,44 +339,6 @@ def init(args: Optional[argparse.Namespace]) -> None: _init_dirs(args) _init_cachedir_tag() - if args is not None and getattr(args, 'basedir', None) is None: - if utils.is_mac: # pragma: no cover - _move_macos() - elif utils.is_windows: # pragma: no cover - _move_windows() - - -def _move_macos() -> None: - """Move most config files to new location on macOS.""" - old_config = config(auto=True) # ~/Library/Preferences/qutebrowser - new_config = config() # ~/.qutebrowser - for f in os.listdir(old_config): - if f not in ['qsettings', 'autoconfig.yml']: - _move_data(os.path.join(old_config, f), - os.path.join(new_config, f)) - - -def _move_windows() -> None: - """Move the whole qutebrowser directory from Local to Roaming AppData.""" - # %APPDATA%\Local\qutebrowser - old_appdata_dir = _writable_location(QStandardPaths.DataLocation) - # %APPDATA%\Roaming\qutebrowser - new_appdata_dir = _writable_location(QStandardPaths.AppDataLocation) - - # data subfolder - old_data = os.path.join(old_appdata_dir, 'data') - new_data = os.path.join(new_appdata_dir, 'data') - ok = _move_data(old_data, new_data) - if not ok: # pragma: no cover - return - - # config files - new_config_dir = os.path.join(new_appdata_dir, 'config') - _create(new_config_dir) - for f in os.listdir(old_appdata_dir): - if f != 'cache': - _move_data(os.path.join(old_appdata_dir, f), - os.path.join(new_config_dir, f)) def _init_cachedir_tag() -> None: @@ -396,33 +358,3 @@ def _init_cachedir_tag() -> None: "cachedir/\n") except OSError: log.init.exception("Failed to create CACHEDIR.TAG") - - -def _move_data(old: str, new: str) -> bool: - """Migrate data from an old to a new directory. - - If the old directory does not exist, the migration is skipped. - If the new directory already exists, an error is shown. - - Return: True if moving succeeded, False otherwise. - """ - if not os.path.exists(old): - return False - - log.init.debug("Migrating data from {} to {}".format(old, new)) - - if os.path.exists(new): - if not os.path.isdir(new) or os.listdir(new): - message.error("Failed to move data from {} as {} is non-empty!" - .format(old, new)) - return False - os.rmdir(new) - - try: - shutil.move(old, new) - except OSError as e: - message.error("Failed to move data from {} to {}: {}".format( - old, new, e)) - return False - - return True diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 977bd7cc6..41d20e734 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -467,12 +467,19 @@ def same_domain(url1: QUrl, url2: QUrl) -> bool: For example example.com and www.example.com are considered the same. but example.co.uk and test.co.uk are not. + If the URL's schemes or ports are different, they are always treated as not equal. + Return: True if the domains are the same, False otherwise. """ ensure_valid(url1) ensure_valid(url2) + if url1.scheme() != url2.scheme(): + return False + if url1.port() != url2.port(): + return False + suffix1 = url1.topLevelDomain() suffix2 = url2.topLevelDomain() if not suffix1: diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 9ecae9e92..893dae877 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -21,7 +21,7 @@ import operator import enum -from typing import TYPE_CHECKING, Any, Optional, Sequence, TypeVar, Union +from typing import Any, Optional, Sequence, TypeVar, Union import attr from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer @@ -30,19 +30,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.utils import log, qtutils, utils -if TYPE_CHECKING: - # Protocol was added in Python 3.8 - from typing import Protocol - - class SupportsLessThan(Protocol): - - """Protocol for the _T TypeVar below.""" - - def __lt__(self, other: Any) -> bool: - ... - - -_T = TypeVar('_T', bound='SupportsLessThan') +_T = TypeVar('_T', bound=utils.SupportsLessThan) class Unset: diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 7c2bf843d..31ff5bf50 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -36,10 +36,11 @@ import glob import mimetypes import ctypes import ctypes.util -from typing import Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union +from typing import (Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union, + TYPE_CHECKING, cast) -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QColor, QClipboard, QDesktopServices +from PyQt5.QtCore import QUrl, QVersionNumber +from PyQt5.QtGui import QClipboard, QDesktopServices from PyQt5.QtWidgets import QApplication import pkg_resources import yaml @@ -53,7 +54,7 @@ except ImportError: # pragma: no cover YAML_C_EXT = False import qutebrowser -from qutebrowser.utils import qtutils, log +from qutebrowser.utils import log fake_clipboard = None @@ -66,6 +67,34 @@ is_windows = sys.platform.startswith('win') is_posix = os.name == 'posix' +try: + # Protocol was added in Python 3.8 + from typing import Protocol +except ImportError: # pragma: no cover + if not TYPE_CHECKING: + class Protocol: + + """Empty stub at runtime.""" + + +class SupportsLessThan(Protocol): + + """Protocol for a "comparable" object.""" + + def __lt__(self, other: Any) -> bool: + ... + + +if TYPE_CHECKING: + class VersionNumber(SupportsLessThan, QVersionNumber): + + """WORKAROUND for incorrect PyQt stubs.""" +else: + class VersionNumber: + + """We can't inherit from Protocol and QVersionNumber at runtime.""" + + class Unreachable(Exception): """Raised when there was unreachable code.""" @@ -210,79 +239,10 @@ def resource_filename(filename: str) -> str: return pkg_resources.resource_filename(qutebrowser.__name__, filename) -def _get_color_percentage(x1: int, y1: int, z1: int, a1: int, - x2: int, y2: int, z2: int, a2: int, - percent: int) -> Tuple[int, int, int, int]: - """Get a color which is percent% interpolated between start and end. - - Args: - x1, y1, z1, a1 : Start color components (R, G, B, A / H, S, V, A / H, S, L, A) - x2, y2, z2, a2 : End color components (R, G, B, A / H, S, V, A / H, S, L, A) - percent: Percentage to interpolate, 0-100. - 0: Start color will be returned. - 100: End color will be returned. - - Return: - A (x, y, z, alpha) tuple with the interpolated color components. - """ - if not 0 <= percent <= 100: - raise ValueError("percent needs to be between 0 and 100!") - x = round(x1 + (x2 - x1) * percent / 100) - y = round(y1 + (y2 - y1) * percent / 100) - z = round(z1 + (z2 - z1) * percent / 100) - a = round(a1 + (a2 - a1) * percent / 100) - return (x, y, z, a) - - -def interpolate_color( - start: QColor, - end: QColor, - percent: int, - colorspace: Optional[QColor.Spec] = QColor.Rgb -) -> QColor: - """Get an interpolated color value. - - Args: - start: The start color. - end: The end color. - percent: Which value to get (0 - 100) - colorspace: The desired interpolation color system, - QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum) - If None, start is used except when percent is 100. - - Return: - The interpolated QColor, with the same spec as the given start color. - """ - qtutils.ensure_valid(start) - qtutils.ensure_valid(end) - - if colorspace is None: - if percent == 100: - return QColor(*end.getRgb()) - else: - return QColor(*start.getRgb()) - - out = QColor() - if colorspace == QColor.Rgb: - r1, g1, b1, a1 = start.getRgb() - r2, g2, b2, a2 = end.getRgb() - components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent) - out.setRgb(*components) - elif colorspace == QColor.Hsv: - h1, s1, v1, a1 = start.getHsv() - h2, s2, v2, a2 = end.getHsv() - components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent) - out.setHsv(*components) - elif colorspace == QColor.Hsl: - h1, s1, l1, a1 = start.getHsl() - h2, s2, l2, a2 = end.getHsl() - components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent) - out.setHsl(*components) - else: - raise ValueError("Invalid colorspace!") - out = out.convertTo(start.spec()) - qtutils.ensure_valid(out) - return out +def parse_version(version: str) -> VersionNumber: + """Parse a version string.""" + v_q, _suffix = QVersionNumber.fromString(version) + return cast(VersionNumber, v_q.normalized()) def format_seconds(total_seconds: int) -> str: diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index fa855d771..64efe4c4f 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -34,7 +34,6 @@ import functools from typing import Mapping, Optional, Sequence, Tuple, cast import attr -import pkg_resources from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo from PyQt5.QtNetwork import QSslSocket from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile, @@ -84,7 +83,7 @@ class DistributionInfo: id: Optional[str] = attr.ib() parsed: 'Distribution' = attr.ib() - version: Optional[Tuple[str, ...]] = attr.ib() + version: Optional[utils.VersionNumber] = attr.ib() pretty: str = attr.ib() @@ -139,8 +138,8 @@ def distribution() -> Optional[DistributionInfo]: assert pretty is not None if 'VERSION_ID' in info: - dist_version: Optional[Tuple[str, ...]] = pkg_resources.parse_version( - info['VERSION_ID']) + version_id = info['VERSION_ID'] + dist_version: Optional[utils.VersionNumber] = utils.parse_version(version_id) else: dist_version = None @@ -263,7 +262,6 @@ def _module_versions() -> Sequence[str]: ('jinja2', ['__version__']), ('pygments', ['__version__']), ('yaml', ['__version__']), - ('cssutils', ['__version__']), ('attr', ['__version__']), ('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']), @@ -402,7 +400,7 @@ def _chromium_version() -> str: 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25) 5.15.2: Updated to 83.0.4103.122 (~2020-06-24) - Security fixes up to 86.0.4240.111 (2020-10-20) + Security fixes up to 86.0.4240.183 (2020-11-02) Also see: diff --git a/requirements.txt b/requirements.txt index ce4d80400..fee8906ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ attrs==20.3.0 colorama==0.4.4 -cssutils==1.0.2 Jinja2==2.11.2 MarkupSafe==1.1.1 Pygments==2.7.2 diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 8030d61a1..6044a1e18 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -252,7 +252,7 @@ def _get_windows_python_path(x64): return fallback -def build_windows(): +def build_windows(*, skip_packaging): """Build windows executables/setups.""" utils.print_title("Updating 3rdparty content") update_3rdparty.run(nsis=True, ace=False, pdfjs=True, fancy_dmg=False) @@ -289,6 +289,14 @@ def build_windows(): utils.print_title("Running 64bit smoke test") smoke_test(os.path.join(out_64, 'qutebrowser.exe')) + if not skip_packaging: + artifacts += _package_windows(out_32, out_64) + + return artifacts + + +def _package_windows(out_32, out_64): + """Build installers/zips for Windows.""" utils.print_title("Building installers") subprocess.run(['makensis.exe', '/DVERSION={}'.format(qutebrowser.__version__), @@ -301,7 +309,7 @@ def build_windows(): name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__) name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__) - artifacts += [ + artifacts = [ (os.path.join('dist', name_32), 'application/vnd.microsoft.portable-executable', 'Windows 32bit installer'), @@ -465,7 +473,9 @@ def main(): "If not given, the current Python interpreter is used.", nargs='?') parser.add_argument('--upload', action='store_true', required=False, - help="Toggle to upload the release to GitHub") + help="Toggle to upload the release to GitHub.") + parser.add_argument('--skip-packaging', action='store_true', required=False, + help="Skip Windows installer/zip generation.") args = parser.parse_args() utils.change_cwd() @@ -487,7 +497,7 @@ def main(): run_asciidoc2html(args) if os.name == 'nt': - artifacts = build_windows() + artifacts = build_windows(skip_packaging=args.skip_packaging) elif sys.platform == 'darwin': artifacts = build_mac() else: diff --git a/scripts/dev/ci/docker/Dockerfile.j2 b/scripts/dev/ci/docker/Dockerfile.j2 new file mode 100644 index 000000000..1835f0a2f --- /dev/null +++ b/scripts/dev/ci/docker/Dockerfile.j2 @@ -0,0 +1,27 @@ +FROM thecompiler/archlinux +MAINTAINER Florian Bruhin <me@the-compiler.org> + +{% if unstable %} +RUN sed -i '/^# after the header/a[kde-unstable]\nInclude = /etc/pacman.d/mirrorlist\n\n[testing]\nInclude = /etc/pacman.d/mirrorlist' /etc/pacman.conf +{% endif %} +RUN pacman -Suyy --noconfirm \ + git \ + python-tox \ + python-distlib \ + qt5-base \ + qt5-declarative \ + {% if webengine %}qt5-webengine python-pyqtwebengine{% else %}qt5-webkit{% endif %} \ + python-pyqt5 \ + xorg-xinit \ + xorg-server-xvfb \ + ttf-bitstream-vera \ + gcc \ + libyaml \ + xorg-xdpyinfo + +USER user +WORKDIR /home/user + +CMD git clone /outside qutebrowser.git && \ + cd qutebrowser.git && \ + tox -e py diff --git a/scripts/dev/ci/docker/README.md b/scripts/dev/ci/docker/README.md new file mode 100644 index 000000000..eb2b8db91 --- /dev/null +++ b/scripts/dev/ci/docker/README.md @@ -0,0 +1,9 @@ +This directory contains a Dockerfile template for containers used to test +qutebrowser on CI. + +The `generate.py` script uses that template to generate various image +configuration. + +The images are rebuilt via Github Actions in this directory, and qutebrowser +then downloads them during the CI run. Note that means that it'll take a while +until builds will use the newer image if you make a change to this directory. diff --git a/scripts/dev/ci/docker/generate.py b/scripts/dev/ci/docker/generate.py new file mode 100644 index 000000000..7d09fdb20 --- /dev/null +++ b/scripts/dev/ci/docker/generate.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2019-2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +"""Generate Dockerfiles for qutebrowser's CI.""" + +import sys + +import jinja2 + + +def main(): + with open('Dockerfile.j2') as f: + template = jinja2.Template(f.read()) + + image = sys.argv[1] + config = { + 'archlinux-webkit': {'webengine': False, 'unstable': False}, + 'archlinux-webengine': {'webengine': True, 'unstable': False}, + 'archlinux-webengine-unstable': {'webengine': True, 'unstable': True}, + }[image] + + with open('Dockerfile', 'w') as f: + f.write(template.render(**config)) + f.write('\n') + + +if __name__ == '__main__': + main() diff --git a/scripts/dev/ci/problemmatchers.py b/scripts/dev/ci/problemmatchers.py index 3e804af05..cc423f922 100644 --- a/scripts/dev/ci/problemmatchers.py +++ b/scripts/dev/ci/problemmatchers.py @@ -188,7 +188,7 @@ MATCHERS = { "severity": "error", "pattern": [ { - "regexp": r'^([^:]+):(\d+): (Found .*)', + "regexp": r'^([^:]+):(\d+): \033\[34m(Found .*)\033\[0m', "file": 1, "line": 2, "message": 3, diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 14373f94f..ad446412c 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -145,8 +145,8 @@ def _check_spelling_file(path, fobj, patterns): for pattern, explanation in patterns: if pattern.search(line): ok = False - print(f'{path}:{num}: Found "{pattern.pattern}" - ', end='') - utils.print_col(explanation, 'blue') + print(f'{path}:{num}: ', end='') + utils.print_col(f'Found "{pattern.pattern}" - {explanation}', 'blue') return ok @@ -185,7 +185,7 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: "'type: ignore[error-code]' instead."), ), ( - re.compile(r'# type: (?!ignore\[)'), + re.compile(r'# type: (?!ignore(\[|$))'), "Don't use type comments, use type annotations instead.", ), ( @@ -274,12 +274,35 @@ def check_userscripts_descriptions(_args: argparse.Namespace = None) -> bool: return ok +def check_userscript_shebangs(_args: argparse.Namespace) -> bool: + """Check that we're using /usr/bin/env in shebangs.""" + ok = True + folder = pathlib.Path('misc/userscripts') + + for sub in folder.iterdir(): + if sub.is_dir() or sub.name == 'README.md': + continue + + with sub.open('r', encoding='utf-8') as f: + shebang = f.readline() + assert shebang.startswith('#!'), shebang + binary = shebang.split()[0][2:] + + if binary not in ['/bin/sh', '/usr/bin/env']: + bin_name = pathlib.Path(binary).name + print(f"In {sub}, use #!/usr/bin/env {bin_name} instead of #!{binary}") + ok = False + + return ok + + def main() -> int: checkers = { 'git': check_git, 'vcs': check_vcs_conflict, 'spelling': check_spelling, - 'userscripts': check_userscripts_descriptions, + 'userscript-descriptions': check_userscripts_descriptions, + 'userscript-shebangs': check_userscript_shebangs, 'changelog-urls': check_changelog_urls, } diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index 00d95fe02..87740c5bb 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -73,7 +73,7 @@ CHANGELOG_URLS = { 'pytest-bdd': 'https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst', 'snowballstemmer': 'https://github.com/snowballstem/snowball/blob/master/NEWS', 'virtualenv': 'https://virtualenv.pypa.io/en/latest/changelog.html', - 'packaging': 'https://pypi.org/project/packaging/', + 'packaging': 'https://packaging.pypa.io/en/latest/changelog.html', 'build': 'https://github.com/pypa/build/commits/master', 'attrs': 'http://www.attrs.org/en/stable/changelog.html', 'Jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst', @@ -82,7 +82,7 @@ CHANGELOG_URLS = { 'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/', 'flake8-debugger': 'https://github.com/JBKahn/flake8-debugger/', 'flake8-builtins': 'https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst', - 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear', + 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear#change-log', 'flake8-tidy-imports': 'https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst', 'flake8-tuple': 'https://github.com/ar4s/flake8_tuple/blob/master/HISTORY.rst', 'flake8-comprehensions': 'https://github.com/adamchainz/flake8-comprehensions/blob/master/HISTORY.rst', @@ -105,7 +105,7 @@ CHANGELOG_URLS = { 'more-itertools': 'https://github.com/erikrose/more-itertools/blob/master/docs/versions.rst', 'pydocstyle': 'http://www.pydocstyle.org/en/latest/release_notes.html', 'Sphinx': 'https://www.sphinx-doc.org/en/master/changes.html', - 'Babel': 'http://babel.pocoo.org/en/latest/changelog.html', + 'Babel': 'https://github.com/python-babel/babel/blob/master/CHANGES', 'alabaster': 'https://alabaster.readthedocs.io/en/latest/changelog.html', 'imagesize': 'https://github.com/shibukawa/imagesize_py/commits/master', 'pytz': 'https://mm.icann.org/pipermail/tz-announce/', @@ -130,9 +130,8 @@ CHANGELOG_URLS = { 'six': 'https://github.com/benjaminp/six/blob/master/CHANGES', 'altgraph': 'https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst', 'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst', - 'lxml': 'https://lxml.de/4.6/changes-4.6.0.html', + 'lxml': 'https://lxml.de/index.html#old-versions', 'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master', - 'tox-pip-version': 'https://github.com/pglass/tox-pip-version/commits/master', 'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst', 'pep517': 'https://github.com/pypa/pep517/blob/master/doc/changelog.rst', 'cryptography': 'https://cryptography.io/en/latest/changelog.html', @@ -164,14 +163,12 @@ CHANGELOG_URLS = { '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/benediktschmitt/py-filelock/commits/master', - 'cssutils': 'https://pythonhosted.org/cssutils/CHANGELOG.html', 'github3.py': 'https://github3py.readthedocs.io/en/master/release-notes/index.html', 'manhole': 'https://github3py.readthedocs.io/en/master/release-notes/index.html', 'pycparser': 'https://github.com/eliben/pycparser/blob/master/CHANGES', 'python-dateutil': 'https://dateutil.readthedocs.io/en/stable/changelog.html', 'appdirs': 'https://github.com/ActiveState/appdirs/blob/master/CHANGES.rst', 'pluggy': 'https://github.com/pytest-dev/pluggy/blob/master/CHANGELOG.rst', - 'tox-venv': 'https://github.com/tox-dev/tox-venv/blob/master/CHANGELOG.rst', 'inflect': 'https://github.com/jazzband/inflect/blob/master/CHANGES.rst', 'jinja2-pluralize': 'https://github.com/audreyfeldroy/jinja2_pluralize/blob/master/HISTORY.rst', 'mypy-extensions': 'https://github.com/python/mypy_extensions/commits/master', @@ -360,6 +357,7 @@ def _get_changed_files(): def parse_versioned_line(line): """Parse a requirements.txt line into name/version.""" if '==' in line: + line = line.rsplit('#', maxsplit=1)[0] # Strip comments name, version = line.split('==') if ';' in version: # pip environment markers version = version.split(';')[0].strip() diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py index f582e23b0..897088539 100644 --- a/scripts/mkvenv.py +++ b/scripts/mkvenv.py @@ -24,12 +24,13 @@ import argparse import pathlib import sys +import re import os import os.path import shutil -import venv +import venv as pyvenv import subprocess -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Dict, Union sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) from scripts import utils, link_pyqt @@ -38,7 +39,22 @@ from scripts import utils, link_pyqt REPO_ROOT = pathlib.Path(__file__).parent.parent -def parse_args() -> argparse.Namespace: +class Error(Exception): + + """Exception for errors in this script.""" + + def __init__(self, msg, code=1): + super().__init__(msg) + self.code = code + + +def print_command(*cmd: Union[str, pathlib.Path], venv: bool) -> None: + """Print a command being run.""" + prefix = 'venv$ ' if venv else '$ ' + utils.print_col(prefix + ' '.join([str(e) for e in cmd]), 'blue') + + +def parse_args(argv: List[str] = None) -> argparse.Namespace: """Parse commandline arguments.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--keep', @@ -71,10 +87,13 @@ def parse_args() -> argparse.Namespace: parser.add_argument('--skip-docs', action='store_true', help="Skip doc generation.") + parser.add_argument('--skip-smoke-test', + action='store_true', + help="Skip Qt smoke test.") parser.add_argument('--tox-error', action='store_true', help=argparse.SUPPRESS) - return parser.parse_args() + return parser.parse_args(argv) def pyqt_versions() -> List[str]: @@ -93,22 +112,40 @@ def pyqt_versions() -> List[str]: return versions + ['auto'] -def run_venv(venv_dir: pathlib.Path, executable, *args: str) -> None: +def run_venv( + venv_dir: pathlib.Path, + executable, + *args: str, + capture_output=False, + capture_error=False, + env=None, +) -> subprocess.CompletedProcess: """Run the given command inside the virtualenv.""" subdir = 'Scripts' if os.name == 'nt' else 'bin' + if env is None: + proc_env = None + else: + proc_env = os.environ.copy() + proc_env.update(env) + try: - subprocess.run([str(venv_dir / subdir / executable)] + - [str(arg) for arg in args], check=True) + return subprocess.run( + [str(venv_dir / subdir / executable)] + [str(arg) for arg in args], + check=True, + universal_newlines=capture_output or capture_error, + stdout=subprocess.PIPE if capture_output else None, + stderr=subprocess.PIPE if capture_error else None, + env=proc_env, + ) except subprocess.CalledProcessError as e: - utils.print_error("Subprocess failed, exiting") - sys.exit(e.returncode) + raise Error("Subprocess failed, exiting") from e def pip_install(venv_dir: pathlib.Path, *args: str) -> None: """Run a pip install command inside the virtualenv.""" arg_str = ' '.join(str(arg) for arg in args) - utils.print_col('venv$ pip install {}'.format(arg_str), 'blue') + print_command('pip install', arg_str, venv=True) run_venv(venv_dir, 'python', '-m', 'pip', 'install', *args) @@ -125,27 +162,25 @@ def delete_old_venv(venv_dir: pathlib.Path) -> None: ] if not any(m.exists() for m in markers): - utils.print_error('{} does not look like a virtualenv, ' - 'cowardly refusing to remove it.'.format(venv_dir)) - sys.exit(1) + raise Error('{} does not look like a virtualenv, cowardly refusing to ' + 'remove it.'.format(venv_dir)) - utils.print_col('$ rm -r {}'.format(venv_dir), 'blue') + print_command('rm -r', venv_dir, venv=False) shutil.rmtree(str(venv_dir)) def create_venv(venv_dir: pathlib.Path, use_virtualenv: bool = False) -> None: """Create a new virtualenv.""" if use_virtualenv: - utils.print_col('$ python3 -m virtualenv {}'.format(venv_dir), 'blue') + print_command('python3 -m virtualenv', venv_dir, venv=False) try: subprocess.run([sys.executable, '-m', 'virtualenv', venv_dir], check=True) except subprocess.CalledProcessError as e: - utils.print_error("virtualenv failed, exiting") - sys.exit(e.returncode) + raise Error("virtualenv failed, exiting", e.returncode) else: - utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue') - venv.create(str(venv_dir), with_pip=True) + print_command('python3 -m venv', venv_dir, venv=False) + pyvenv.create(str(venv_dir), with_pip=True) def upgrade_seed_pkgs(venv_dir: pathlib.Path) -> None: @@ -202,6 +237,129 @@ def install_pyqt_wheels(venv_dir: pathlib.Path, pip_install(venv_dir, *wheels) +def apply_xcb_util_workaround( + venv_dir: pathlib.Path, + pyqt_type: str, + pyqt_version: str, +) -> None: + """If needed (Debian Stable), symlink libxcb-util.so.0 -> .1. + + WORKAROUND for https://bugreports.qt.io/browse/QTBUG-88688 + """ + utils.print_title("Running xcb-util workaround") + + if not sys.platform.startswith('linux'): + print("Workaround not needed: Not on Linux.") + return + if pyqt_type != 'binary': + print("Workaround not needed: Not installing from PyQt binaries.") + return + if pyqt_version not in ['auto', '5.15']: + print("Workaround not needed: Not installing Qt 5.15.") + return + + libs = _find_libs() + abi_type = 'libc6,x86-64' # the only one PyQt wheels are available for + + if ('libxcb-util.so.1', abi_type) in libs: + print("Workaround not needed: libxcb-util.so.1 found.") + return + + try: + libxcb_util_libs = libs['libxcb-util.so.0', abi_type] + except KeyError: + utils.print_error('Workaround failed: libxcb-util.so.0 not found.') + return + + if len(libxcb_util_libs) > 1: + utils.print_error( + f'Workaround failed: Multiple matching libxcb-util found: ' + f'{libxcb_util_libs}') + return + + libxcb_util_path = pathlib.Path(libxcb_util_libs[0]) + + code = [ + 'from PyQt5.QtCore import QLibraryInfo', + 'print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))', + ] + proc = run_venv(venv_dir, 'python', '-c', '; '.join(code), capture_output=True) + venv_lib_path = pathlib.Path(proc.stdout.strip()) + + link_path = venv_lib_path / libxcb_util_path.with_suffix('.1').name + + # This gives us a nicer path to print, and also conveniently makes sure we + # didn't accidentally end up with a path outside the venv. + rel_link_path = venv_dir / link_path.relative_to(venv_dir.resolve()) + print_command('ln -s', libxcb_util_path, rel_link_path, venv=False) + + link_path.symlink_to(libxcb_util_path) + + +def _find_libs() -> Dict[Tuple[str, str], List[str]]: + """Find all system-wide .so libraries.""" + all_libs: Dict[Tuple[str, str], List[str]] = {} + + if pathlib.Path("/sbin/ldconfig").exists(): + # /sbin might not be in PATH on e.g. Debian + ldconfig_bin = "/sbin/ldconfig" + else: + ldconfig_bin = "ldconfig" + ldconfig_proc = subprocess.run( + [ldconfig_bin, '-p'], + check=True, + stdout=subprocess.PIPE, + encoding=sys.getfilesystemencoding(), + ) + + pattern = re.compile(r'(?P<name>\S+) \((?P<abi_type>[^)]+)\) => (?P<path>.*)') + for line in ldconfig_proc.stdout.splitlines(): + match = pattern.fullmatch(line.strip()) + if match is None: + if 'libs found in cache' not in line: + utils.print_col(f'Failed to match ldconfig output: {line}', 'yellow') + continue + + key = match.group('name'), match.group('abi_type') + path = match.group('path') + + libs = all_libs.setdefault(key, []) + libs.append(path) + + return all_libs + + +def run_qt_smoke_test(venv_dir: pathlib.Path) -> None: + """Make sure the Qt installation works.""" + utils.print_title("Running Qt smoke test") + code = [ + 'import sys', + 'from PyQt5.QtWidgets import QApplication', + 'from PyQt5.QtCore import qVersion, QT_VERSION_STR, PYQT_VERSION_STR', + 'print(f"Python: {sys.version}")', + 'print(f"qVersion: {qVersion()}")', + 'print(f"QT_VERSION_STR: {QT_VERSION_STR}")', + 'print(f"PYQT_VERSION_STR: {PYQT_VERSION_STR}")', + 'QApplication([])', + 'print("Qt seems to work properly!")', + 'print()', + ] + try: + run_venv( + venv_dir, + 'python', '-c', '; '.join(code), + env={'QT_DEBUG_PLUGINS': '1'}, + capture_error=True + ) + except Error as e: + proc_e = e.__cause__ + assert isinstance(proc_e, subprocess.CalledProcessError), proc_e + print(proc_e.stderr) + raise Error( + f"Smoke test failed with status {proc_e.returncode}. " + "You might find additional information in the debug output above.") + + def install_requirements(venv_dir: pathlib.Path) -> None: """Install qutebrowser's requirement.txt.""" utils.print_title("Installing other qutebrowser dependencies") @@ -233,27 +391,24 @@ def regenerate_docs(venv_dir: pathlib.Path, a2h_args = [] script_path = pathlib.Path(__file__).parent / 'asciidoc2html.py' - utils.print_col('venv$ python3 scripts/asciidoc2html.py {}' - .format(' '.join(a2h_args)), 'blue') + print_command('python3 scripts/asciidoc2html.py', *a2h_args, venv=True) run_venv(venv_dir, 'python', str(script_path), *a2h_args) -def main() -> None: +def run(args) -> None: """Install qutebrowser in a virtualenv..""" - args = parse_args() venv_dir = pathlib.Path(args.venv_dir) wheels_dir = pathlib.Path(args.pyqt_wheels_dir) utils.change_cwd() if (args.pyqt_version != 'auto' and args.pyqt_type not in ['binary', 'source']): - utils.print_error('The --pyqt-version option is only available when ' - 'installing PyQt from binary or source') - sys.exit(1) - elif args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels': - utils.print_error('The --pyqt-wheels-dir option is only available ' - 'when installing PyQt from wheels') - sys.exit(1) + raise Error('The --pyqt-version option is only available when installing PyQt ' + 'from binary or source') + + if args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels': + raise Error('The --pyqt-wheels-dir option is only available when installing ' + 'PyQt from wheels') if not args.keep: utils.print_title("Creating virtual environment") @@ -275,6 +430,10 @@ def main() -> None: else: raise AssertionError + apply_xcb_util_workaround(venv_dir, args.pyqt_type, args.pyqt_version) + if args.pyqt_type != 'skip' and not args.skip_smoke_test: + run_qt_smoke_test(venv_dir) + install_requirements(venv_dir) install_qutebrowser(venv_dir) if args.dev: @@ -284,5 +443,14 @@ def main() -> None: regenerate_docs(venv_dir, args.asciidoc) +def main(): + args = parse_args() + try: + run(args) + except Error as e: + utils.print_error(str(e)) + sys.exit(e.code) + + if __name__ == '__main__': main() diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index 11066fb92..17b457521 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -35,7 +35,7 @@ from PyQt5.QtCore import PYQT_VERSION, QCoreApplication pytest.register_assert_rewrite('end2end.fixtures') -from end2end.fixtures.webserver import server, server_per_test, ssl_server +from end2end.fixtures.webserver import server, server_per_test, server2, ssl_server from end2end.fixtures.quteprocess import (quteproc_process, quteproc, quteproc_new) from end2end.fixtures.testprocess import pytest_runtest_makereport diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index 5dd6ef79d..aa9009f7c 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -41,10 +41,10 @@ Feature: Keyboard input @no_xvfb @posix @qtwebengine_skip Scenario: :fake-key sending key to the website with other window focused When I open data/keyinput/log.html - And I run :inspector + And I run :devtools And I wait for "Focus object changed: <PyQt5.QtWebKitWidgets.QWebView object at *>" in the log And I run :fake-key x - And I run :inspector + And I run :devtools And I wait for "Focus object changed: <qutebrowser.browser.webkit.webview.WebView *>" in the log Then the error "No focused webview!" should be shown diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index c2733fce7..06dc0b805 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -173,18 +173,18 @@ Feature: Various utility commands. @no_xvfb @posix @qtwebengine_skip Scenario: Inspector smoke test - When I run :inspector + When I run :devtools And I wait for "Focus object changed: <PyQt5.QtWebKitWidgets.QWebView object at *>" in the log - And I run :inspector + And I run :devtools And I wait for "Focus object changed: *" in the log Then no crash should happen # Different code path as an inspector got created now @no_xvfb @posix @qtwebengine_skip Scenario: Inspector smoke test 2 - When I run :inspector + When I run :devtools And I wait for "Focus object changed: <PyQt5.QtWebKitWidgets.QWebView object at *>" in the log - And I run :inspector + And I run :devtools And I wait for "Focus object changed: *" in the log Then no crash should happen diff --git a/tests/end2end/features/test_downloads_bdd.py b/tests/end2end/features/test_downloads_bdd.py index 951ebcfea..1f27d2794 100644 --- a/tests/end2end/features/test_downloads_bdd.py +++ b/tests/end2end/features/test_downloads_bdd.py @@ -96,12 +96,6 @@ def wait_for_download_prompt(tmpdir, quteproc, path): "(reason: question asked)") -@bdd.when("I download an SSL page") -def download_ssl_page(quteproc, ssl_server): - quteproc.send_cmd(':download https://localhost:{}/' - .format(ssl_server.port)) - - @bdd.then(bdd.parsers.parse("The downloaded file {filename} should not exist")) def download_should_not_exist(filename, tmpdir): path = tmpdir / 'downloads' / filename diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py index 85778c3e0..51352c539 100644 --- a/tests/end2end/fixtures/testprocess.py +++ b/tests/end2end/fixtures/testprocess.py @@ -101,7 +101,7 @@ def pytest_runtest_makereport(item, call): return quteproc_log = getattr(item, '_quteproc_log', None) - server_log = getattr(item, '_server_log', None) + server_logs = getattr(item, '_server_logs', []) if not hasattr(report.longrepr, 'addsection'): # In some conditions (on macOS and Windows it seems), report.longrepr @@ -114,11 +114,11 @@ def pytest_runtest_makereport(item, call): verbose = item.config.getoption('--verbose') if quteproc_log is not None: - report.longrepr.addsection("qutebrowser output", - _render_log(quteproc_log, verbose=verbose)) - if server_log is not None: - report.longrepr.addsection("server output", - _render_log(server_log, verbose=verbose)) + report.longrepr.addsection( + "qutebrowser output", _render_log(quteproc_log, verbose=verbose)) + for name, content in server_logs: + report.longrepr.addsection( + f"{name} output", _render_log(content, verbose=verbose)) class Process(QObject): diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 9f4383b35..d40739724 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -192,21 +192,41 @@ def server(qapp, request): @pytest.fixture(autouse=True) def server_per_test(server, request): """Fixture to clean server request list after each test.""" - request.node._server_log = server.captured_log + if not hasattr(request.node, '_server_logs'): + request.node._server_logs = [] + request.node._server_logs.append(('server', server.captured_log)) + server.before_test() yield server.after_test() @pytest.fixture +def server2(qapp, request): + """Fixture for a second server object for cross-origin tests.""" + server = WebserverProcess(request, 'webserver_sub') + + if not hasattr(request.node, '_server_logs'): + request.node._server_logs = [] + request.node._server_logs.append(('secondary server', server.captured_log)) + + server.start() + yield server + server.terminate() + + +@pytest.fixture def ssl_server(request, qapp): """Fixture for a webserver with a self-signed SSL certificate. - This needs to be explicitly used in a test, and overwrites the server log - used in that test. + This needs to be explicitly used in a test. """ server = WebserverProcess(request, 'webserver_sub_ssl') - request.node._server_log = server.captured_log + + if not hasattr(request.node, '_server_logs'): + request.node._server_logs = [] + request.node._server_logs.append(('SSL server', server.captured_log)) + server.start() yield server server.after_test() diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py index cf8ffd006..e38d64bb8 100644 --- a/tests/end2end/fixtures/webserver_sub.py +++ b/tests/end2end/fixtures/webserver_sub.py @@ -29,8 +29,8 @@ parameters or headers with the same name properly. import sys import json import time -import os import threading +import pathlib from http import HTTPStatus import cheroot.wsgi @@ -40,6 +40,9 @@ app = flask.Flask(__name__) _redirect_later_event = None +END2END_DIR = pathlib.Path(__file__).resolve().parents[1] + + @app.route('/') def root(): """Show simple text.""" @@ -54,15 +57,8 @@ def send_data(path): If a directory is requested, its index.html is sent. """ - if hasattr(sys, 'frozen'): - basedir = os.path.realpath(os.path.dirname(sys.executable)) - data_dir = os.path.join(basedir, 'end2end', 'data') - else: - basedir = os.path.join(os.path.realpath(os.path.dirname(__file__)), - '..') - data_dir = os.path.join(basedir, 'data') - print(basedir) - if os.path.isdir(os.path.join(data_dir, path)): + data_dir = END2END_DIR / 'data' + if (data_dir / path).is_dir(): path += '/index.html' return flask.send_from_directory(data_dir, path) @@ -248,6 +244,12 @@ def view_headers(): return flask.jsonify(headers=dict(flask.request.headers)) +@app.route('/headers-link/<int:port>') +def headers_link(port): + """Get a (possibly cross-origin) link to /headers.""" + return flask.render_template('headers-link.html', port=port) + + @app.route('/response-headers') def response_headers(): """Return a set of response headers from the query string.""" @@ -273,11 +275,9 @@ def view_user_agent(): @app.route('/favicon.ico') def favicon(): - basedir = os.path.join(os.path.realpath(os.path.dirname(__file__)), - '..', '..', '..') - return flask.send_from_directory(os.path.join(basedir, 'icons'), - 'qutebrowser.ico', - mimetype='image/vnd.microsoft.icon') + icon_dir = END2END_DIR.parents[1] / 'icons' + return flask.send_from_directory( + icon_dir, 'qutebrowser.ico', mimetype='image/vnd.microsoft.icon') @app.after_request @@ -321,9 +321,9 @@ class WSGIServer(cheroot.wsgi.Server): def main(): - if hasattr(sys, 'frozen'): - basedir = os.path.realpath(os.path.dirname(sys.executable)) - app.template_folder = os.path.join(basedir, 'end2end', 'templates') + app.template_folder = END2END_DIR / 'templates' + assert app.template_folder.is_dir(), app.template_folder + port = int(sys.argv[1]) server = WSGIServer(('127.0.0.1', port), app) server.start() diff --git a/tests/end2end/templates/headers-link.html b/tests/end2end/templates/headers-link.html new file mode 100644 index 000000000..fece530b1 --- /dev/null +++ b/tests/end2end/templates/headers-link.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Link to header page</title> + </head> + <body> + <a href="http://localhost:{{ port }}/headers" id="link">headers</a> + </body> +</html> diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index abffc9350..74805cec2 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -23,6 +23,7 @@ import subprocess import sys import logging import re +import json import pytest from PyQt5.QtCore import QProcess @@ -383,3 +384,50 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() + + +@pytest.mark.parametrize('value, expected', [ + ('always', 'http://localhost:(port2)/headers-link/(port)'), + ('never', None), + ('same-domain', 'http://localhost:(port2)/'), # None with QtWebKit +]) +def test_referrer(quteproc_new, server, server2, request, value, expected): + """Check referrer settings.""" + args = _base_args(request.config) + [ + '--temp-basedir', + '-s', 'content.headers.referer', value, + ] + quteproc_new.start(args) + + quteproc_new.open_path(f'headers-link/{server.port}', port=server2.port) + quteproc_new.send_cmd(':click-element id link') + quteproc_new.wait_for_load_finished('headers') + + content = quteproc_new.get_content() + data = json.loads(content) + print(data) + headers = data['headers'] + + if not request.config.webengine and value == 'same-domain': + # With QtWebKit and same-domain, we don't send a referer at all. + expected = None + + if expected is not None: + for key, val in [('(port)', server.port), ('(port2)', server2.port)]: + expected = expected.replace(key, str(val)) + + assert headers.get('Referer') == expected + + +@pytest.mark.qtwebkit_skip +@utils.qt514 +def test_preferred_colorscheme(request, quteproc_new): + """Make sure the the preferred colorscheme is set.""" + args = _base_args(request.config) + [ + '--temp-basedir', + '-s', 'colors.webpage.prefers_color_scheme_dark', 'true', + ] + quteproc_new.start(args) + + quteproc_new.send_cmd(':jseval matchMedia("(prefers-color-scheme: dark)").matches') + quteproc_new.wait_for(message='True') diff --git a/tests/end2end/test_mkvenv.py b/tests/end2end/test_mkvenv.py new file mode 100644 index 000000000..430be0279 --- /dev/null +++ b/tests/end2end/test_mkvenv.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + + +from scripts import mkvenv + + +def test_smoke(tmp_path): + """Simple smoke test of mkvenv.py.""" + args = mkvenv.parse_args(['--venv-dir', str(tmp_path / 'venv'), '--skip-docs']) + mkvenv.run(args) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 3c0623dfb..6f80099bb 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -166,7 +166,7 @@ def fake_web_tab(stubs, tab_registry, mode_manager, qapp): @pytest.fixture -def greasemonkey_manager(monkeypatch, data_tmpdir): +def greasemonkey_manager(monkeypatch, data_tmpdir, config_tmpdir): gm_manager = greasemonkey.GreasemonkeyManager() monkeypatch.setattr(greasemonkey, 'gm_manager', gm_manager) diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index c70b858f5..c1990de0d 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -464,7 +464,8 @@ class TestCompletionMetaInfo: def test_contains_keyerror(self, metainfo): with pytest.raises(KeyError): - 'does_not_exist' in metainfo # pylint: disable=pointless-statement + # pylint: disable=pointless-statement + 'does_not_exist' in metainfo # noqa: B015 def test_getitem_keyerror(self, metainfo): with pytest.raises(KeyError): diff --git a/tests/unit/browser/test_navigate.py b/tests/unit/browser/test_navigate.py index 5fe0acbf6..5a93a517c 100644 --- a/tests/unit/browser/test_navigate.py +++ b/tests/unit/browser/test_navigate.py @@ -187,6 +187,8 @@ class TestUp: ('/one/two/three', 1, '/one/two'), ('/one/two/three?foo=bar', 1, '/one/two'), ('/one/two/three', 2, '/one'), + ('/one/two%2Fthree', 1, '/one'), + ('/one/two%2Fthree/four', 1, '/one/two%2Fthree'), ]) def test_up(self, url_suffix, count, expected_suffix): url_base = 'https://example.com' diff --git a/tests/unit/browser/webengine/test_darkmode.py b/tests/unit/browser/webengine/test_darkmode.py index 05cd7737a..cd84526c3 100644 --- a/tests/unit/browser/webengine/test_darkmode.py +++ b/tests/unit/browser/webengine/test_darkmode.py @@ -32,6 +32,26 @@ def patch_backend(monkeypatch): monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) +@pytest.mark.parametrize('qversion, enabled, expected', [ + # Disabled or nothing set + ("5.14", False, []), + ("5.15.0", False, []), + ("5.15.1", False, []), + ("5.15.2", False, []), + + # Enabled in configuration + ("5.14", True, []), + ("5.15.0", True, []), + ("5.15.1", True, []), + ("5.15.2", True, [("preferredColorScheme", "1")]), +]) +@utils.qt514 +def test_colorscheme(config_stub, monkeypatch, qversion, enabled, expected): + monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion) + config_stub.val.colors.webpage.prefers_color_scheme_dark = enabled + assert list(darkmode.settings()) == expected + + @pytest.mark.parametrize('settings, expected', [ # Disabled ({}, []), @@ -169,6 +189,22 @@ def test_variant(monkeypatch, qversion, webengine_version, expected): assert darkmode._variant() == expected +@pytest.mark.parametrize('value, is_valid, expected', [ + ('invalid_value', False, darkmode.Variant.qt_515_0), + ('qt_515_2', True, darkmode.Variant.qt_515_2), +]) +def test_variant_override(monkeypatch, caplog, value, is_valid, expected): + monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: None) + monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', 0x050f00) + monkeypatch.setenv('QUTE_DARKMODE_VARIANT', value) + + with caplog.at_level(logging.WARNING): + assert darkmode._variant() == expected + + log_msg = 'Ignoring invalid QUTE_DARKMODE_VARIANT=invalid_value' + assert (log_msg in caplog.messages) != is_valid + + def test_broken_smart_images_policy(config_stub, monkeypatch, caplog): config_stub.val.colors.webpage.darkmode.enabled = True config_stub.val.colors.webpage.darkmode.policy.images = 'smart' diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py index 0aa3943e7..e1c9d04f8 100644 --- a/tests/unit/browser/webkit/network/test_networkreply.py +++ b/tests/unit/browser/webkit/network/test_networkreply.py @@ -52,9 +52,8 @@ class TestFixedDataNetworkReply: b'Hello World! This is a test.']) def test_data(self, qtbot, req, data): reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo') - with qtbot.waitSignal(reply.metaDataChanged), \ - qtbot.waitSignal(reply.readyRead), \ - qtbot.waitSignal(reply.finished): + with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead, + reply.finished], order='strict'): pass assert reply.bytesAvailable() == len(data) @@ -79,7 +78,7 @@ def test_error_network_reply(qtbot, req): reply = networkreply.ErrorNetworkReply( req, "This is an error", QNetworkReply.UnknownNetworkError) - with qtbot.waitSignal(reply.error), qtbot.waitSignal(reply.finished): + with qtbot.waitSignals([reply.error, reply.finished], order='strict'): pass reply.abort() # shouldn't do anything diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py index 1a2b6bf31..a30bfe786 100644 --- a/tests/unit/browser/webkit/test_mhtml.py +++ b/tests/unit/browser/webkit/test_mhtml.py @@ -28,12 +28,6 @@ import pytest mhtml = pytest.importorskip('qutebrowser.browser.webkit.mhtml') -try: - import cssutils -except ImportError: - cssutils = None - - @pytest.fixture(autouse=True) def patch_uuid(monkeypatch): monkeypatch.setattr(uuid, "uuid4", lambda: "UUID") @@ -255,34 +249,25 @@ def test_empty_content_type(checker): """) -@pytest.mark.parametrize('has_cssutils', [ - pytest.param(True, marks=pytest.mark.skipif( - cssutils is None, reason="requires cssutils"), id='with_cssutils'), - pytest.param(False, id='no_cssutils'), -]) -@pytest.mark.parametrize('inline, style, expected_urls', [ - pytest.param(False, "@import 'default.css'", ['default.css'], +@pytest.mark.parametrize('style, expected_urls', [ + pytest.param("@import 'default.css'", ['default.css'], id='import with apostrophe'), - pytest.param(False, '@import "default.css"', ['default.css'], + pytest.param('@import "default.css"', ['default.css'], id='import with quote'), - pytest.param(False, "@import \t 'tabbed.css'", ['tabbed.css'], + pytest.param("@import \t 'tabbed.css'", ['tabbed.css'], id='import with tab'), - pytest.param(False, "@import url('default.css')", ['default.css'], + pytest.param("@import url('default.css')", ['default.css'], id='import with url()'), - pytest.param(False, """body { + pytest.param("""body { background: url("/bg-img.png") }""", ['/bg-img.png'], id='background with body'), - pytest.param(True, 'background: url(folder/file.png) no-repeat', + pytest.param('background: url(folder/file.png) no-repeat', ['folder/file.png'], id='background'), - pytest.param(True, 'content: url()', [], id='content'), + pytest.param('content: url()', [], id='content'), ]) -def test_css_url_scanner(monkeypatch, has_cssutils, inline, style, - expected_urls): - if not has_cssutils: - monkeypatch.setattr(mhtml, '_get_css_imports_cssutils', - lambda data, inline=False: None) +def test_css_url_scanner(monkeypatch, style, expected_urls): expected_urls.sort() - urls = mhtml._get_css_imports(style, inline=inline) + urls = mhtml._get_css_imports(style) urls.sort() assert urls == expected_urls diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index 41dfee98b..4732837a1 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -18,6 +18,8 @@ # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. from unittest import mock +import hypothesis +import hypothesis.strategies import pytest from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextDocument, QColor @@ -69,6 +71,13 @@ def test_benchmark_highlight(benchmark): benchmark(bench) +@hypothesis.given(text=hypothesis.strategies.text()) +def test_pattern_hypothesis(text): + """Make sure we can't produce invalid patterns.""" + doc = QTextDocument() + completiondelegate._Highlighter(doc, text, Qt.red) + + def test_highlighted(qtbot): """Make sure highlighting works. diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py index 1fc0b4d73..98e70dc01 100644 --- a/tests/unit/completion/test_completionmodel.py +++ b/tests/unit/completion/test_completionmodel.py @@ -77,8 +77,8 @@ def test_set_pattern(pat, qtbot): for c in cats: c.set_pattern = mock.Mock(spec=[]) model.add_category(c) - with qtbot.waitSignal(model.layoutAboutToBeChanged), \ - qtbot.waitSignal(model.layoutChanged): + with qtbot.waitSignals([model.layoutAboutToBeChanged, model.layoutChanged], + order='strict'): model.set_pattern(pat) for c in cats: c.set_pattern.assert_called_with(pat) diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index 356b854c5..c0ef4b47f 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -161,6 +161,7 @@ def test_completion_item_focus_no_model(which, completionview, model, qtbot): completionview.completion_item_focus(which) +@pytest.mark.skip("Seems to disagree with reality, see #5897") def test_completion_item_focus_fetch(completionview, model, qtbot): """Test that on_next_prev_item moves the selection properly. diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 2ac7084dd..8b4653b58 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -26,6 +26,8 @@ import time from datetime import datetime from unittest import mock +import hypothesis +import hypothesis.strategies import pytest from PyQt5.QtCore import QUrl, QDateTime try: @@ -37,7 +39,8 @@ except ImportError: from qutebrowser.misc import objects from qutebrowser.completion import completer -from qutebrowser.completion.models import miscmodels, urlmodel, configmodel +from qutebrowser.completion.models import ( + miscmodels, urlmodel, configmodel, listcategory) from qutebrowser.config import configdata, configtypes from qutebrowser.utils import usertypes from qutebrowser.mainwindow import tabbedbrowser @@ -1324,3 +1327,10 @@ def test_undo_completion(tabbed_browser_stubs, info): "2020-01-01 00:00"), ], }) + + +@hypothesis.given(text=hypothesis.strategies.text()) +def test_listcategory_hypothesis(text): + """Make sure we can't produce invalid patterns.""" + cat = listcategory.ListCategory("test", []) + cat.set_pattern(text) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 588e4a5cf..b30ab4bee 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -432,12 +432,11 @@ class TestConfig: assert conf.get_obj(name1) == 'never' assert conf.get_obj(name2) is True - with qtbot.waitSignal(conf.changed), qtbot.waitSignal(conf.changed): + with qtbot.waitSignals([conf.changed, conf.changed]) as blocker: conf.clear(save_yaml=save_yaml) - # Doesn't work with PyQt 5.15.1 workaround - # options = {blocker1.args[0], blocker2.args[0]} - # assert options == {name1, name2} + options = {e.args[0] for e in blocker.all_signals_and_args} + assert options == {name1, name2} if save_yaml: assert yaml_value(name1) is usertypes.UNSET diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 2304d1655..220aa40f7 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -212,14 +212,12 @@ class TestSet: commands.set(win_id=0, option='foo?') -@pytest.mark.parametrize('old', [True, False]) -def test_diff(commands, tabbed_browser_stubs, old): +def test_diff(commands, tabbed_browser_stubs): """Run ':config-diff'. Should open qute://configdiff.""" - commands.config_diff(win_id=0, old=old) - url = QUrl('qute://configdiff/old') if old else QUrl('qute://configdiff') - assert tabbed_browser_stubs[0].loaded_url == url + commands.config_diff(win_id=0) + assert tabbed_browser_stubs[0].loaded_url == QUrl('qute://configdiff') class TestCycle: diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 76d3ac094..710018604 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -138,10 +138,16 @@ class TestValidValues: def test_descriptions(self, klass): """Test descriptions.""" - vv = klass(('foo', "foo desc"), ('bar', "bar desc"), 'baz') - assert vv.descriptions['foo'] == "foo desc" - assert vv.descriptions['bar'] == "bar desc" - assert 'baz' not in vv.descriptions + vv = klass( + ('one-with', "desc 1"), + ('two-with', "desc 2"), + 'three-without', + ('four-without', None) + ) + assert vv.descriptions['one-with'] == "desc 1" + assert vv.descriptions['two-with'] == "desc 2" + assert 'three-without' not in vv.descriptions + assert 'four-without' not in vv.descriptions @pytest.mark.parametrize('args, expected', [ (['a', 'b'], "<qutebrowser.config.configtypes.ValidValues " @@ -396,14 +402,11 @@ class MappingSubclass(configtypes.MappingType): """A MappingType we use in TestMappingType which is valid/good.""" MAPPING = { - 'one': 1, - 'two': 2, + 'one': (1, 'one doc'), + 'two': (2, 'two doc'), + 'three': (3, None), } - def __init__(self, none_ok=False): - super().__init__(none_ok) - self.valid_values = configtypes.ValidValues('one', 'two') - class TestMappingType: @@ -429,11 +432,12 @@ class TestMappingType: def test_to_str(self, klass): assert klass().to_str('one') == 'one' - @pytest.mark.parametrize('typ', [configtypes.ColorSystem(), - configtypes.Position(), - configtypes.SelectOnRemove()]) - def test_mapping_type_matches_valid_values(self, typ): - assert sorted(typ.MAPPING) == sorted(typ.valid_values) + def test_valid_values(self, klass): + assert klass().valid_values == configtypes.ValidValues( + ('one', 'one doc'), + ('two', 'two doc'), + ('three', None), + ) class TestString: diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py index 7e1a7c744..4830340cf 100644 --- a/tests/unit/config/test_configutils.py +++ b/tests/unit/config/test_configutils.py @@ -21,6 +21,7 @@ import hypothesis from hypothesis import strategies import pytest from PyQt5.QtCore import QUrl +from PyQt5.QtWidgets import QLabel from qutebrowser.config import configutils, configdata, configtypes, configexc from qutebrowser.utils import urlmatch, usertypes, qtutils @@ -364,3 +365,45 @@ class TestFontFamilies: assert family str(families) + + def test_system_default_basics(self, qapp): + families = configutils.FontFamilies.from_system_default() + assert len(families) == 1 + assert str(families) + + def test_system_default_rendering(self, qtbot): + families = configutils.FontFamilies.from_system_default() + + label = QLabel() + qtbot.add_widget(label) + label.setText("Hello World") + + stylesheet = f'font-family: {families.to_str(quote=True)}' + print(stylesheet) + label.setStyleSheet(stylesheet) + + with qtbot.waitExposed(label): + # Needed so the font gets calculated + label.show() + info = label.fontInfo() + + # Check the requested font to make sure CSS parsing worked + assert label.font().family() == families.family + + # Try to find out whether the monospace font did a fallback on a non-monospace + # font... + fallback_label = QLabel() + qtbot.add_widget(label) + fallback_label.setText("fallback") + + with qtbot.waitExposed(fallback_label): + # Needed so the font gets calculated + fallback_label.show() + + fallback_family = fallback_label.fontInfo().family() + print(f'fallback: {fallback_family}') + if info.family() == fallback_family: + return + + # If we didn't fall back, we should've gotten a fixed-pitch font. + assert info.fixedPitch(), info.family() diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index dc6b655b3..b050113b4 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -116,7 +116,7 @@ class TestQtArgs: def test_in_process_stack_traces(self, monkeypatch, parser, backend, version_check, debug_flag, expected): monkeypatch.setattr(qtargs.qtutils, 'version_check', - lambda version, compiled=False: version_check) + lambda version, compiled=False, exact=False: version_check) monkeypatch.setattr(qtargs.objects, 'backend', backend) parsed = parser.parse_args(['--debug-flag', 'stack'] if debug_flag else []) @@ -252,14 +252,31 @@ class TestQtArgs: else: assert arg in args - @pytest.mark.parametrize('referer, arg', [ - ('always', None), - ('never', '--no-referrers'), - ('same-domain', '--reduced-referrer-granularity'), + @pytest.mark.parametrize('qt_version, referer, arg', [ + # 'always' -> no arguments + ('5.15.0', 'always', None), + + # 'never' is handled via interceptor for most Qt versions + ('5.12.3', 'never', '--no-referrers'), + ('5.12.4', 'never', None), + ('5.13.0', 'never', '--no-referrers'), + ('5.13.1', 'never', None), + ('5.14.0', 'never', None), + ('5.15.0', 'never', None), + + # 'same-domain' - arguments depend on Qt versions + ('5.13.0', 'same-domain', '--reduced-referrer-granularity'), + ('5.14.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'), + ('5.15.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'), ]) - def test_referer(self, config_stub, monkeypatch, parser, referer, arg): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + def test_referer(self, config_stub, monkeypatch, parser, qt_version, referer, arg): + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) + monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: qt_version) + + # Avoid WebRTC pipewire feature + monkeypatch.setattr(qtargs.utils, 'is_linux', False) + # Avoid overlay scrollbar feature + config_stub.val.scrolling.bar = 'never' config_stub.val.content.headers.referer = referer parsed = parser.parse_args([]) @@ -268,23 +285,29 @@ class TestQtArgs: if arg is None: assert '--no-referrers' not in args assert '--reduced-referrer-granularity' not in args + assert '--enable-features=ReducedReferrerGranularity' not in args else: assert arg in args - @pytest.mark.parametrize('dark, new_qt, added', [ - (True, True, True), - (True, False, False), - (False, True, False), - (False, False, False), + @pytest.mark.parametrize('dark, qt_version, added', [ + (True, "5.13", False), # not supported + (True, "5.14", True), + (True, "5.15.0", True), + (True, "5.15.1", True), + (True, "5.15.2", False), # handled via blink setting + + (False, "5.13", False), + (False, "5.14", False), + (False, "5.15.0", False), + (False, "5.15.1", False), + (False, "5.15.2", False), ]) @utils.qt514 def test_prefers_color_scheme_dark(self, config_stub, monkeypatch, parser, - dark, new_qt, added): + dark, qt_version, added): monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) - monkeypatch.setattr(qtargs.qtutils, 'version_check', - lambda version, exact=False, compiled=True: - new_qt) + monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: qt_version) config_stub.val.colors.webpage.prefers_color_scheme_dark = dark diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py index 5d549ca68..5f2c94b56 100644 --- a/tests/unit/javascript/test_greasemonkey.py +++ b/tests/unit/javascript/test_greasemonkey.py @@ -41,12 +41,15 @@ test_gm_script = r""" console.log("Script is running."); """ -pytestmark = pytest.mark.usefixtures('data_tmpdir') +pytestmark = [ + pytest.mark.usefixtures('data_tmpdir'), + pytest.mark.usefixtures('config_tmpdir') +] def _save_script(script_text, filename): # pylint: disable=no-member - file_path = py.path.local(greasemonkey._scripts_dir()) / filename + file_path = py.path.local(greasemonkey._scripts_dirs()[0]) / filename # pylint: enable=no-member file_path.write_text(script_text, encoding='utf-8', ensure=True) diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 88d9ae66c..cd57d33cb 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -21,7 +21,7 @@ from unittest import mock -from PyQt5.QtCore import Qt, PYQT_VERSION +from PyQt5.QtCore import Qt import pytest from qutebrowser.keyinput import basekeyparser, keyutils @@ -305,8 +305,6 @@ class TestCount: # https://github.com/qutebrowser/qutebrowser/issues/3743 handle_text(prompt_keyparser, Qt.Key_twosuperior, Qt.Key_B, Qt.Key_A) - @pytest.mark.skipif(PYQT_VERSION == 0x050F01, - reason='waitSignals is broken in PyQt 5.15.1') def test_count_keystring_update(self, qtbot, handle_text, prompt_keyparser): """Make sure the keystring is updated correctly when entering count.""" diff --git a/tests/unit/misc/test_earlyinit.py b/tests/unit/misc/test_earlyinit.py index 728b4eb26..af229a40a 100644 --- a/tests/unit/misc/test_earlyinit.py +++ b/tests/unit/misc/test_earlyinit.py @@ -31,3 +31,20 @@ def test_init_faulthandler_stderr_none(monkeypatch, attr): """Make sure init_faulthandler works when sys.stderr/__stderr__ is None.""" monkeypatch.setattr(sys, attr, None) earlyinit.init_faulthandler() + + +@pytest.mark.parametrize('same', [True, False]) +def test_qt_version(same): + if same: + qt_version_str = '5.14.0' + expected = '5.14.0' + else: + qt_version_str = '5.13.0' + expected = '5.14.0 (compiled 5.13.0)' + actual = earlyinit.qt_version(qversion='5.14.0', qt_version_str=qt_version_str) + assert actual == expected + + +def test_qt_version_no_args(): + """Make sure qt_version without arguments at least works.""" + earlyinit.qt_version() diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py index 7e71478ce..4ed19f64e 100644 --- a/tests/unit/misc/test_guiprocess.py +++ b/tests/unit/misc/test_guiprocess.py @@ -54,8 +54,8 @@ def fake_proc(monkeypatch, stubs): def test_start(proc, qtbot, message_mock, py_proc): """Test simply starting a process.""" - with qtbot.waitSignal(proc.started, timeout=10000), \ - qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + order='strict'): argv = py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) @@ -70,8 +70,8 @@ def test_start_verbose(proc, qtbot, message_mock, py_proc): """Test starting a process verbosely.""" proc.verbose = True - with qtbot.waitSignal(proc.started, timeout=10000), \ - qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + order='strict'): argv = py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) @@ -99,8 +99,9 @@ def test_start_output_message(proc, qtbot, caplog, message_mock, py_proc, code.append("sys.exit(0)") with caplog.at_level(logging.ERROR, 'message'): - with qtbot.waitSignal(proc.started, timeout=10000), \ - qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], + timeout=10000, + order='strict'): argv = py_proc(';'.join(code)) proc.start(*argv) @@ -146,8 +147,8 @@ def test_start_env(monkeypatch, qtbot, py_proc): sys.exit(0) """) - with qtbot.waitSignal(proc.started, timeout=10000), \ - qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + order='strict'): proc.start(*argv) data = qutescheme.spawn_output @@ -186,12 +187,12 @@ def test_double_start(qtbot, proc, py_proc): def test_double_start_finished(qtbot, proc, py_proc): """Test starting a GUIProcess twice (with the first call finished).""" - with qtbot.waitSignal(proc.started, timeout=10000), \ - qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + order='strict'): argv = py_proc("import sys; sys.exit(0)") proc.start(*argv) - with qtbot.waitSignal(proc.started, timeout=10000), \ - qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + order='strict'): argv = py_proc("import sys; sys.exit(0)") proc.start(*argv) @@ -266,8 +267,8 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream): def test_stdout_not_decodable(proc, qtbot, message_mock, py_proc): """Test handling malformed utf-8 in stdout.""" - with qtbot.waitSignal(proc.started, timeout=10000), \ - qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + order='strict'): argv = py_proc(r""" import sys # Using \x81 because it's invalid in UTF-8 and CP1252 diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 2c9f7ea7f..3f53ca238 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -493,10 +493,10 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8') (b'{"args": [], "target_arg": null}\n', 'invalid version'), ]) def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg): + signals = [ipc_server.got_invalid_data, connected_socket.disconnected] with caplog.at_level(logging.ERROR): with qtbot.assertNotEmitted(ipc_server.got_args): - with qtbot.waitSignal(ipc_server.got_invalid_data), \ - qtbot.waitSignal(connected_socket.disconnected): + with qtbot.waitSignals(signals, order='strict'): connected_socket.write(data) invalid_msg = 'Ignoring invalid IPC data from socket ' @@ -514,8 +514,8 @@ def test_multiline(qtbot, ipc_server, connected_socket): version=ipc.PROTOCOL_VERSION)) with qtbot.assertNotEmitted(ipc_server.got_invalid_data): - with qtbot.waitSignal(ipc_server.got_args), \ - qtbot.waitSignal(ipc_server.got_args): + with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args], + order='strict'): connected_socket.write(data.encode('utf-8')) assert len(spy) == 2 diff --git a/tests/unit/scripts/test_problemmatchers.py b/tests/unit/scripts/test_problemmatchers.py new file mode 100644 index 000000000..98bd9c7a5 --- /dev/null +++ b/tests/unit/scripts/test_problemmatchers.py @@ -0,0 +1,38 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015-2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +import re + +import pytest + +from scripts.dev.ci import problemmatchers + + +@pytest.mark.parametrize('matcher_name', list(problemmatchers.MATCHERS)) +def test_patterns(matcher_name): + """Make sure all regexps are valid. + + They aren't actually Python syntax, but hopefully close enough to it to compile with + Python's re anyways. + """ + for matcher in problemmatchers.MATCHERS[matcher_name]: + for pattern in matcher['pattern']: + regexp = pattern['regexp'] + print(regexp) + re.compile(regexp) diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 81d198946..2e54fb42e 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -26,6 +26,7 @@ import os.path import unittest import unittest.mock +import attr import pytest from PyQt5.QtCore import (QDataStream, QPoint, QUrl, QByteArray, QIODevice, QTimer, QBuffer, QFile, QProcess, QFileDevice) @@ -53,23 +54,25 @@ else: @pytest.mark.parametrize(['qversion', 'compiled', 'pyqt', 'version', 'exact', 'expected'], [ # equal versions - ('5.4.0', None, None, '5.4.0', False, True), - ('5.4.0', None, None, '5.4.0', True, True), # exact=True - ('5.4.0', None, None, '5.4', True, True), # without trailing 0 + ('5.14.0', None, None, '5.14.0', False, True), + ('5.14.0', None, None, '5.14.0', True, True), # exact=True + ('5.14.0', None, None, '5.14', True, True), # without trailing 0 # newer version installed - ('5.4.1', None, None, '5.4', False, True), - ('5.4.1', None, None, '5.4', True, False), # exact=True + ('5.14.1', None, None, '5.14', False, True), + ('5.14.1', None, None, '5.14', True, False), # exact=True # older version installed - ('5.3.2', None, None, '5.4', False, False), - ('5.3.0', None, None, '5.3.2', False, False), - ('5.3.0', None, None, '5.3.2', True, False), # exact=True + ('5.13.2', None, None, '5.14', False, False), + ('5.13.0', None, None, '5.13.2', False, False), + ('5.13.0', None, None, '5.13.2', True, False), # exact=True # compiled=True # new Qt runtime, but compiled against older version - ('5.4.0', '5.3.0', '5.4.0', '5.4.0', False, False), + ('5.14.0', '5.13.0', '5.14.0', '5.14.0', False, False), # new Qt runtime, compiled against new version, but old PyQt - ('5.4.0', '5.4.0', '5.3.0', '5.4.0', False, False), + ('5.14.0', '5.14.0', '5.13.0', '5.14.0', False, False), # all up-to-date - ('5.4.0', '5.4.0', '5.4.0', '5.4.0', False, True), + ('5.14.0', '5.14.0', '5.14.0', '5.14.0', False, True), + # dev suffix + ('5.15.1', '5.15.1', '5.15.2.dev2009281246', '5.15.0', False, True), ]) # pylint: enable=bad-continuation def test_version_check(monkeypatch, qversion, compiled, pyqt, version, exact, @@ -936,3 +939,107 @@ class TestEventLoop: QTimer.singleShot(400, self.loop.quit) self.loop.exec_() assert not self.loop._executing + + +class Color(QColor): + + """A QColor with a nicer repr().""" + + def __repr__(self): + return utils.get_repr(self, constructor=True, red=self.red(), + green=self.green(), blue=self.blue(), + alpha=self.alpha()) + + +class TestInterpolateColor: + + @attr.s + class Colors: + + white = attr.ib() + black = attr.ib() + + @pytest.fixture + def colors(self): + """Example colors to be used.""" + return self.Colors(Color('white'), Color('black')) + + def test_invalid_start(self, colors): + """Test an invalid start color.""" + with pytest.raises(qtutils.QtValueError): + qtutils.interpolate_color(Color(), colors.white, 0) + + def test_invalid_end(self, colors): + """Test an invalid end color.""" + with pytest.raises(qtutils.QtValueError): + qtutils.interpolate_color(colors.white, Color(), 0) + + @pytest.mark.parametrize('perc', [-1, 101]) + def test_invalid_percentage(self, colors, perc): + """Test an invalid percentage.""" + with pytest.raises(ValueError): + qtutils.interpolate_color(colors.white, colors.white, perc) + + def test_invalid_colorspace(self, colors): + """Test an invalid colorspace.""" + with pytest.raises(ValueError): + qtutils.interpolate_color(colors.white, colors.black, 10, QColor.Cmyk) + + @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, + QColor.Hsl]) + def test_0_100(self, colors, colorspace): + """Test 0% and 100% in different colorspaces.""" + white = qtutils.interpolate_color(colors.white, colors.black, 0, colorspace) + black = qtutils.interpolate_color(colors.white, colors.black, 100, colorspace) + assert Color(white) == colors.white + assert Color(black) == colors.black + + def test_interpolation_rgb(self): + """Test an interpolation in the RGB colorspace.""" + color = qtutils.interpolate_color( + Color(0, 40, 100), Color(0, 20, 200), 50, QColor.Rgb) + assert Color(color) == Color(0, 30, 150) + + def test_interpolation_hsv(self): + """Test an interpolation in the HSV colorspace.""" + start = Color() + stop = Color() + start.setHsv(0, 40, 100) + stop.setHsv(0, 20, 200) + color = qtutils.interpolate_color(start, stop, 50, QColor.Hsv) + expected = Color() + expected.setHsv(0, 30, 150) + assert Color(color) == expected + + def test_interpolation_hsl(self): + """Test an interpolation in the HSL colorspace.""" + start = Color() + stop = Color() + start.setHsl(0, 40, 100) + stop.setHsl(0, 20, 200) + color = qtutils.interpolate_color(start, stop, 50, QColor.Hsl) + expected = Color() + expected.setHsl(0, 30, 150) + assert Color(color) == expected + + @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, + QColor.Hsl]) + def test_interpolation_alpha(self, colorspace): + """Test interpolation of colorspace's alpha.""" + start = Color(0, 0, 0, 30) + stop = Color(0, 0, 0, 100) + color = qtutils.interpolate_color(start, stop, 50, colorspace) + expected = Color(0, 0, 0, 65) + assert Color(color) == expected + + @pytest.mark.parametrize('percentage, expected', [ + (0, (0, 0, 0)), + (99, (0, 0, 0)), + (100, (255, 255, 255)), + ]) + def test_interpolation_none(self, percentage, expected): + """Test an interpolation with a gradient turned off.""" + color = qtutils.interpolate_color( + Color(0, 0, 0), Color(255, 255, 255), percentage, None) + assert isinstance(color, QColor) + assert Color(color) == Color(*expected) diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index 572d38f02..dbc8831f6 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -28,7 +28,6 @@ import textwrap import logging import subprocess -import attr from PyQt5.QtCore import QStandardPaths import pytest @@ -108,7 +107,7 @@ def test_fake_windows(tmpdir, monkeypatch, what): def test_fake_haiku(tmpdir, monkeypatch): """Test getting data dir on HaikuOS.""" locations = { - QStandardPaths.DataLocation: '', + QStandardPaths.AppDataLocation: '', QStandardPaths.ConfigLocation: str(tmpdir / 'config' / APPNAME), } monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation', @@ -129,14 +128,14 @@ class TestWritableLocation: 'qutebrowser.utils.standarddir.QStandardPaths.writableLocation', lambda typ: '') with pytest.raises(standarddir.EmptyValueError): - standarddir._writable_location(QStandardPaths.DataLocation) + standarddir._writable_location(QStandardPaths.AppDataLocation) def test_sep(self, monkeypatch): """Make sure the right kind of separator is used.""" monkeypatch.setattr(standarddir.os, 'sep', '\\') monkeypatch.setattr(standarddir.os.path, 'join', lambda *parts: '\\'.join(parts)) - loc = standarddir._writable_location(QStandardPaths.DataLocation) + loc = standarddir._writable_location(QStandardPaths.AppDataLocation) assert '/' not in loc assert '\\' in loc @@ -397,128 +396,8 @@ class TestSystemData: assert standarddir.data(system=True) == standarddir.data() -class TestMoveWindowsAndMacOS: - - """Test other invocations of _move_data.""" - - @pytest.fixture(autouse=True) - def patch_standardpaths(self, files, monkeypatch): - locations = { - QStandardPaths.DataLocation: str(files.local_data_dir), - QStandardPaths.AppDataLocation: str(files.roaming_data_dir), - } - monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation', - locations.get) - monkeypatch.setattr( - standarddir, 'config', lambda auto=False: - str(files.auto_config_dir if auto else files.config_dir)) - - @pytest.fixture - def files(self, tmpdir): - - @attr.s - class Files: - - auto_config_dir = attr.ib() - config_dir = attr.ib() - local_data_dir = attr.ib() - roaming_data_dir = attr.ib() - - return Files( - auto_config_dir=tmpdir / 'auto_config' / APPNAME, - config_dir=tmpdir / 'config' / APPNAME, - local_data_dir=tmpdir / 'data' / APPNAME, - roaming_data_dir=tmpdir / 'roaming-data' / APPNAME, - ) - - def test_move_macos(self, files): - """Test moving configs on macOS.""" - (files.auto_config_dir / 'autoconfig.yml').ensure() - (files.auto_config_dir / 'quickmarks').ensure() - files.config_dir.ensure(dir=True) - - standarddir._move_macos() - - assert (files.auto_config_dir / 'autoconfig.yml').exists() - assert not (files.config_dir / 'autoconfig.yml').exists() - assert not (files.auto_config_dir / 'quickmarks').exists() - assert (files.config_dir / 'quickmarks').exists() - - def test_move_windows(self, files): - """Test moving configs on Windows.""" - (files.local_data_dir / 'data' / 'blocked-hosts').ensure() - (files.local_data_dir / 'qutebrowser.conf').ensure() - (files.local_data_dir / 'cache' / 'cachefile').ensure() - - standarddir._move_windows() - - assert (files.roaming_data_dir / 'data' / 'blocked-hosts').exists() - assert (files.roaming_data_dir / 'config' / - 'qutebrowser.conf').exists() - assert not (files.roaming_data_dir / 'cache').exists() - assert (files.local_data_dir / 'cache' / 'cachefile').exists() - - -class TestMove: - - @pytest.fixture - def dirs(self, tmpdir): - @attr.s - class Dirs: - - old = attr.ib() - new = attr.ib() - old_file = attr.ib() - new_file = attr.ib() - - old_dir = tmpdir / 'old' - new_dir = tmpdir / 'new' - return Dirs(old=old_dir, new=new_dir, - old_file=old_dir / 'file', new_file=new_dir / 'file') - - def test_no_old_dir(self, dirs, caplog): - """Nothing should happen without any old directory.""" - standarddir._move_data(str(dirs.old), str(dirs.new)) - assert not any(message.startswith('Migrating data from') - for message in caplog.messages) - - @pytest.mark.parametrize('empty_dest', [True, False]) - def test_moving_data(self, dirs, empty_dest): - dirs.old_file.ensure() - if empty_dest: - dirs.new.ensure(dir=True) - - standarddir._move_data(str(dirs.old), str(dirs.new)) - assert not dirs.old_file.exists() - assert dirs.new_file.exists() - - def test_already_existing(self, dirs, caplog): - dirs.old_file.ensure() - dirs.new_file.ensure() - - with caplog.at_level(logging.ERROR): - standarddir._move_data(str(dirs.old), str(dirs.new)) - - expected = "Failed to move data from {} as {} is non-empty!".format( - dirs.old, dirs.new) - assert caplog.messages[-1] == expected - - def test_deleting_error(self, dirs, monkeypatch, mocker, caplog): - """When there was an error it should be logged.""" - mock = mocker.Mock(side_effect=OSError('error')) - monkeypatch.setattr(standarddir.shutil, 'move', mock) - dirs.old_file.ensure() - - with caplog.at_level(logging.ERROR): - standarddir._move_data(str(dirs.old), str(dirs.new)) - - expected = "Failed to move data from {} to {}: error".format( - dirs.old, dirs.new) - assert caplog.messages[-1] == expected - - @pytest.mark.parametrize('args_kind', ['basedir', 'normal', 'none']) -def test_init(mocker, tmpdir, monkeypatch, args_kind): +def test_init(tmpdir, monkeypatch, args_kind): """Do some sanity checks for standarddir.init(). Things like _init_cachedir_tag() are tested in more detail in other tests. @@ -527,8 +406,6 @@ def test_init(mocker, tmpdir, monkeypatch, args_kind): monkeypatch.setenv('HOME', str(tmpdir)) - m_windows = mocker.patch('qutebrowser.utils.standarddir._move_windows') - m_mac = mocker.patch('qutebrowser.utils.standarddir._move_macos') if args_kind == 'normal': args = types.SimpleNamespace(basedir=None) elif args_kind == 'basedir': @@ -540,19 +417,6 @@ def test_init(mocker, tmpdir, monkeypatch, args_kind): standarddir.init(args) assert standarddir._locations != {} - if args_kind == 'normal': - if utils.is_mac: - m_windows.assert_not_called() - assert m_mac.called - elif utils.is_windows: - assert m_windows.called - m_mac.assert_not_called() - else: - m_windows.assert_not_called() - m_mac.assert_not_called() - else: - m_windows.assert_not_called() - m_mac.assert_not_called() @pytest.mark.linux diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index eac2b6c1d..0167f6cee 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -615,7 +615,7 @@ class TestInvalidUrlError: @pytest.mark.parametrize('are_same, url1, url2', [ (True, 'http://example.com', 'http://www.example.com'), - (True, 'http://bbc.co.uk', 'https://www.bbc.co.uk'), + (True, 'http://bbc.co.uk', 'http://www.bbc.co.uk'), (True, 'http://many.levels.of.domains.example.com', 'http://www.example.com'), (True, 'http://idn.иком.museum', 'http://idn2.иком.museum'), (True, 'http://one.not_a_valid_tld', 'http://one.not_a_valid_tld'), @@ -624,6 +624,9 @@ class TestInvalidUrlError: (False, 'https://example.kids.museum', 'http://example.kunst.museum'), (False, 'http://idn.иком.museum', 'http://idn.ירושלים.museum'), (False, 'http://one.not_a_valid_tld', 'http://two.not_a_valid_tld'), + + (False, 'http://example.org', 'https://example.org'), # different scheme + (False, 'http://example.org:80', 'http://example.org:8080'), # different port ]) def test_same_domain(are_same, url1, url2): """Test same_domain.""" diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 0c39ad183..ac7ed5ce7 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -29,10 +29,8 @@ import re import shlex import math -import pkg_resources -import attr from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QColor, QClipboard +from PyQt5.QtGui import QClipboard import pytest import hypothesis from hypothesis import strategies @@ -40,22 +38,12 @@ import yaml import qutebrowser import qutebrowser.utils # for test_qualname -from qutebrowser.utils import utils, qtutils, version, usertypes +from qutebrowser.utils import utils, version, usertypes ELLIPSIS = '\u2026' -class Color(QColor): - - """A QColor with a nicer repr().""" - - def __repr__(self): - return utils.get_repr(self, constructor=True, red=self.red(), - green=self.green(), blue=self.blue(), - alpha=self.alpha()) - - class TestCompactText: """Test compact_text.""" @@ -160,110 +148,6 @@ def test_resource_filename(): assert f.read().splitlines()[0] == "Hello World!" -class TestInterpolateColor: - - """Tests for interpolate_color. - - Attributes: - white: The Color white as a valid Color for tests. - white: The Color black as a valid Color for tests. - """ - - @attr.s - class Colors: - - white = attr.ib() - black = attr.ib() - - @pytest.fixture - def colors(self): - """Example colors to be used.""" - return self.Colors(Color('white'), Color('black')) - - def test_invalid_start(self, colors): - """Test an invalid start color.""" - with pytest.raises(qtutils.QtValueError): - utils.interpolate_color(Color(), colors.white, 0) - - def test_invalid_end(self, colors): - """Test an invalid end color.""" - with pytest.raises(qtutils.QtValueError): - utils.interpolate_color(colors.white, Color(), 0) - - @pytest.mark.parametrize('perc', [-1, 101]) - def test_invalid_percentage(self, colors, perc): - """Test an invalid percentage.""" - with pytest.raises(ValueError): - utils.interpolate_color(colors.white, colors.white, perc) - - def test_invalid_colorspace(self, colors): - """Test an invalid colorspace.""" - with pytest.raises(ValueError): - utils.interpolate_color(colors.white, colors.black, 10, - QColor.Cmyk) - - @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, - QColor.Hsl]) - def test_0_100(self, colors, colorspace): - """Test 0% and 100% in different colorspaces.""" - white = utils.interpolate_color(colors.white, colors.black, 0, - colorspace) - black = utils.interpolate_color(colors.white, colors.black, 100, - colorspace) - assert Color(white) == colors.white - assert Color(black) == colors.black - - def test_interpolation_rgb(self): - """Test an interpolation in the RGB colorspace.""" - color = utils.interpolate_color(Color(0, 40, 100), Color(0, 20, 200), - 50, QColor.Rgb) - assert Color(color) == Color(0, 30, 150) - - def test_interpolation_hsv(self): - """Test an interpolation in the HSV colorspace.""" - start = Color() - stop = Color() - start.setHsv(0, 40, 100) - stop.setHsv(0, 20, 200) - color = utils.interpolate_color(start, stop, 50, QColor.Hsv) - expected = Color() - expected.setHsv(0, 30, 150) - assert Color(color) == expected - - def test_interpolation_hsl(self): - """Test an interpolation in the HSL colorspace.""" - start = Color() - stop = Color() - start.setHsl(0, 40, 100) - stop.setHsl(0, 20, 200) - color = utils.interpolate_color(start, stop, 50, QColor.Hsl) - expected = Color() - expected.setHsl(0, 30, 150) - assert Color(color) == expected - - @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, - QColor.Hsl]) - def test_interpolation_alpha(self, colorspace): - """Test interpolation of colorspace's alpha.""" - start = Color(0, 0, 0, 30) - stop = Color(0, 0, 0, 100) - color = utils.interpolate_color(start, stop, 50, colorspace) - expected = Color(0, 0, 0, 65) - assert Color(color) == expected - - @pytest.mark.parametrize('percentage, expected', [ - (0, (0, 0, 0)), - (99, (0, 0, 0)), - (100, (255, 255, 255)), - ]) - def test_interpolation_none(self, percentage, expected): - """Test an interpolation with a gradient turned off.""" - color = utils.interpolate_color(Color(0, 0, 0), Color(255, 255, 255), - percentage, None) - assert isinstance(color, QColor) - assert Color(color) == Color(*expected) - - @pytest.mark.parametrize('seconds, out', [ (-1, '-0:01'), (0, '0:00'), @@ -807,7 +691,7 @@ class TestOpenFile: info = version.DistributionInfo( id='org.kde.Platform', parsed=version.Distribution.kde_flatpak, - version=pkg_resources.parse_version('5.12'), + version=utils.parse_version('5.12'), pretty='Unknown') monkeypatch.setattr(version, 'distribution', lambda: info) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 3393ae376..593557ae8 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -33,7 +33,6 @@ import textwrap import datetime import attr -import pkg_resources import pytest import hypothesis import hypothesis.strategies @@ -77,7 +76,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='ubuntu', parsed=version.Distribution.ubuntu, - version=pkg_resources.parse_version('14.4'), + version=utils.parse_version('14.4'), pretty='Ubuntu 14.04.5 LTS')), # Ubuntu 17.04 (""" @@ -90,7 +89,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='ubuntu', parsed=version.Distribution.ubuntu, - version=pkg_resources.parse_version('17.4'), + version=utils.parse_version('17.4'), pretty='Ubuntu 17.04')), # Debian Jessie (""" @@ -102,7 +101,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='debian', parsed=version.Distribution.debian, - version=pkg_resources.parse_version('8'), + version=utils.parse_version('8'), pretty='Debian GNU/Linux 8 (jessie)')), # Void Linux (""" @@ -133,7 +132,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='fedora', parsed=version.Distribution.fedora, - version=pkg_resources.parse_version('25'), + version=utils.parse_version('25'), pretty='Fedora 25 (Twenty Five)')), # OpenSUSE (""" @@ -146,7 +145,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='opensuse', parsed=version.Distribution.opensuse, - version=pkg_resources.parse_version('42.2'), + version=utils.parse_version('42.2'), pretty='openSUSE Leap 42.2')), # Linux Mint (""" @@ -159,7 +158,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='linuxmint', parsed=version.Distribution.linuxmint, - version=pkg_resources.parse_version('18.1'), + version=utils.parse_version('18.1'), pretty='Linux Mint 18.1')), # Manjaro (""" @@ -188,7 +187,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='org.kde.Platform', parsed=version.Distribution.kde_flatpak, - version=pkg_resources.parse_version('5.12'), + version=utils.parse_version('5.12'), pretty='KDE')), # No PRETTY_NAME (""" @@ -221,7 +220,7 @@ def test_distribution(tmpdir, monkeypatch, os_release, expected): (None, False), (version.DistributionInfo( id='org.kde.Platform', parsed=version.Distribution.kde_flatpak, - version=pkg_resources.parse_version('5.12'), + version=utils.parse_version('5.12'), pretty='Unknown'), True), (version.DistributionInfo( id='arch', parsed=version.Distribution.arch, version=None, @@ -561,7 +560,6 @@ class ImportFake: ('jinja2', True), ('pygments', True), ('yaml', True), - ('cssutils', True), ('attr', True), ('PyQt5.QtWebEngineWidgets', True), ('PyQt5.QtWebEngine', True), @@ -637,7 +635,6 @@ class TestModuleVersions: @pytest.mark.parametrize('module, idx, expected', [ ('colorama', 1, 'colorama: no'), - ('cssutils', 6, 'cssutils: no'), ]) def test_missing_module(self, module, idx, expected, import_fake): """Test with a module missing. @@ -681,7 +678,6 @@ class TestModuleVersions: ('jinja2', True), ('pygments', True), ('yaml', True), - ('cssutils', True), ('attr', True), ]) def test_existing_attributes(self, name, has_version): @@ -694,8 +690,6 @@ class TestModuleVersions: name: The name of the module to check. has_version: Whether a __version__ attribute is expected. """ - if name == 'cssutils': - pytest.importorskip(name) module = importlib.import_module(name) assert hasattr(module, '__version__') == has_version diff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py index 014ef7f0c..59c3f7d43 100644 --- a/tests/unit/utils/usertypes/test_question.py +++ b/tests/unit/utils/usertypes/test_question.py @@ -53,25 +53,23 @@ def test_done(mode, answer, signal_names, question, qtbot): question.mode = mode question.answer = answer signals = [getattr(question, name) for name in signal_names] - blockers = [qtbot.waitSignal(signal) for signal in signals] - - question.done() - for blocker in blockers: - blocker.wait() - + with qtbot.waitSignals(signals, order='strict'): + question.done() assert not question.is_aborted def test_cancel(question, qtbot): """Test Question.cancel().""" - with qtbot.waitSignal(question.cancelled), qtbot.waitSignal(question.completed): + with qtbot.waitSignals([question.cancelled, question.completed], + order='strict'): question.cancel() assert not question.is_aborted def test_abort(question, qtbot): """Test Question.abort().""" - with qtbot.waitSignal(question.aborted), qtbot.waitSignal(question.completed): + with qtbot.waitSignals([question.aborted, question.completed], + order='strict'): question.abort() assert question.is_aborted @@ -12,17 +12,17 @@ minversion = 3.15 [testenv] setenv = PYTEST_QT_API=pyqt5 - pyqt{,512,513,514,515}: LINK_PYQT_SKIP=true - pyqt{,512,513,514,515}: QUTE_BDD_WEBENGINE=true + pyqt{,512,513,514,515,5150}: LINK_PYQT_SKIP=true + pyqt{,512,513,514,515,5150}: QUTE_BDD_WEBENGINE=true cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS basepython = + py: {env:PYTHON:python3} py3: {env:PYTHON:python3} py36: {env:PYTHON:python3.6} py37: {env:PYTHON:python3.7} py38: {env:PYTHON:python3.8} py39: {env:PYTHON:python3.9} -pip_version = pip deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-tests.txt @@ -31,6 +31,7 @@ deps = pyqt513: -r{toxinidir}/misc/requirements/requirements-pyqt-5.13.txt pyqt514: -r{toxinidir}/misc/requirements/requirements-pyqt-5.14.txt pyqt515: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.txt + pyqt5150: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.0.txt commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -bb -m pytest {posargs:tests} @@ -41,16 +42,14 @@ commands = [testenv:misc] ignore_errors = true basepython = {env:PYTHON:python3} -pip_version = pip # For global .gitignore files passenv = HOME deps = commands = - {envpython} scripts/dev/misc_checks.py all + {envpython} scripts/dev/misc_checks.py {posargs:all} [testenv:vulture] basepython = {env:PYTHON:python3} -pip_version = pip deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-vulture.txt @@ -61,7 +60,6 @@ commands = [testenv:vulture-pyqtlink] basepython = {env:PYTHON:python3} -pip_version = pip deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-vulture.txt @@ -72,7 +70,6 @@ commands = [testenv:pylint] basepython = {env:PYTHON:python3} -pip_version = pip ignore_errors = true passenv = deps = @@ -86,7 +83,6 @@ commands = [testenv:pylint-pyqtlink] basepython = {env:PYTHON:python3} -pip_version = pip ignore_errors = true passenv = deps = @@ -100,7 +96,6 @@ commands = [testenv:pylint-master] basepython = {env:PYTHON:python3} -pip_version = pip passenv = {[testenv:pylint]passenv} deps = -r{toxinidir}/requirements.txt @@ -113,7 +108,6 @@ commands = [testenv:flake8] basepython = {env:PYTHON:python3} -pip_version = pip passenv = deps = -r{toxinidir}/requirements.txt @@ -123,7 +117,6 @@ commands = [testenv:pyroma] basepython = {env:PYTHON:python3} -pip_version = pip passenv = deps = -r{toxinidir}/misc/requirements/requirements-pyroma.txt @@ -132,7 +125,6 @@ commands = [testenv:check-manifest] basepython = {env:PYTHON:python3} -pip_version = pip passenv = deps = -r{toxinidir}/misc/requirements/requirements-check-manifest.txt @@ -141,7 +133,6 @@ commands = [testenv:docs] basepython = {env:PYTHON:python3} -pip_version = pip whitelist_externals = git passenv = CI GITHUB_REF deps = @@ -154,7 +145,6 @@ commands = [testenv:pyinstaller-{64,32}] basepython = {env:PYTHON:python3} -pip_version = pip passenv = APPDATA HOME PYINSTALLER_DEBUG deps = -r{toxinidir}/requirements.txt @@ -179,7 +169,6 @@ commands = bash scripts/dev/run_shellcheck.sh {posargs} [testenv:mypy] basepython = {env:PYTHON:python3} -pip_version = pip passenv = TERM MYPY_FORCE_TERMINAL_WIDTH deps = -r{toxinidir}/requirements.txt @@ -191,14 +180,12 @@ commands = [testenv:yamllint] basepython = {env:PYTHON:python3} -pip_version = pip deps = -r{toxinidir}/misc/requirements/requirements-yamllint.txt commands = {envpython} -m yamllint -f colored --strict . {posargs} [testenv:mypy-diff] basepython = {env:PYTHON:python3} -pip_version = pip passenv = {[testenv:mypy]passenv} deps = {[testenv:mypy]deps} commands = @@ -207,7 +194,6 @@ commands = [testenv:sphinx] basepython = {env:PYTHON:python3} -pip_version = pip passenv = usedevelop = true deps = @@ -219,7 +205,6 @@ commands = [testenv:build-release] basepython = {env:PYTHON:python3} -pip_version = pip passenv = * usedevelop = true deps = |