summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bumpversion.cfg2
-rw-r--r--doc/changelog.asciidoc96
-rw-r--r--doc/help/settings.asciidoc180
-rw-r--r--doc/install.asciidoc11
-rw-r--r--misc/org.qutebrowser.qutebrowser.appdata.xml1
-rw-r--r--misc/requirements/requirements-check-manifest.txt4
-rw-r--r--misc/requirements/requirements-flake8.txt8
-rw-r--r--misc/requirements/requirements-mypy.txt6
-rw-r--r--misc/requirements/requirements-pyqt-pyinstaller.txt7
-rw-r--r--misc/requirements/requirements-pyqt-pyinstaller.txt-raw2
-rw-r--r--misc/requirements/requirements-tests-bleeding.txt37
-rw-r--r--misc/requirements/requirements-tests-git.txt34
-rw-r--r--misc/requirements/requirements-tests.txt2
-rw-r--r--misc/requirements/requirements-tox.txt2
-rwxr-xr-xmisc/userscripts/qute-keepassxc2
-rwxr-xr-xmisc/userscripts/qute-lastpass9
-rw-r--r--qutebrowser/__init__.py2
-rw-r--r--qutebrowser/browser/commands.py5
-rw-r--r--qutebrowser/browser/hints.py4
-rw-r--r--qutebrowser/browser/history.py24
-rw-r--r--qutebrowser/browser/shared.py13
-rw-r--r--qutebrowser/browser/webengine/darkmode.py3
-rw-r--r--qutebrowser/browser/webengine/webengineinspector.py17
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py15
-rw-r--r--qutebrowser/commands/parser.py209
-rw-r--r--qutebrowser/commands/runners.py197
-rw-r--r--qutebrowser/completion/completer.py24
-rw-r--r--qutebrowser/completion/models/configmodel.py8
-rw-r--r--qutebrowser/components/caretcommands.py8
-rw-r--r--qutebrowser/config/config.py32
-rw-r--r--qutebrowser/config/configdata.yml24
-rw-r--r--qutebrowser/config/qtargs.py3
-rw-r--r--qutebrowser/html/warning-sessions.html2
-rw-r--r--qutebrowser/keyinput/macros.py4
-rw-r--r--qutebrowser/keyinput/modeman.py14
-rw-r--r--qutebrowser/keyinput/modeparsers.py4
-rw-r--r--qutebrowser/misc/backendproblem.py9
-rw-r--r--qutebrowser/misc/earlyinit.py21
-rw-r--r--qutebrowser/misc/elf.py12
-rw-r--r--qutebrowser/misc/guiprocess.py23
-rw-r--r--qutebrowser/misc/sql.py8
-rw-r--r--qutebrowser/utils/standarddir.py16
-rw-r--r--qutebrowser/utils/utils.py17
-rw-r--r--qutebrowser/utils/version.py29
-rw-r--r--requirements.txt2
-rw-r--r--scripts/dev/misc_checks.py4
-rw-r--r--scripts/dev/recompile_requirements.py20
-rw-r--r--scripts/dev/run_pylint_on_tests.py1
-rwxr-xr-xscripts/dev/src2asciidoc.py2
-rw-r--r--scripts/dev/ua_fetch.py3
-rw-r--r--scripts/dev/update_version.py2
-rwxr-xr-xscripts/hist_importer.py3
-rwxr-xr-xscripts/mkvenv.py15
-rw-r--r--tests/conftest.py6
-rw-r--r--tests/end2end/data/darkmode/mathml.html23
-rw-r--r--tests/end2end/data/darkmode/mathml.svg159
-rw-r--r--tests/end2end/features/spawn.feature2
-rw-r--r--tests/end2end/fixtures/quteprocess.py18
-rw-r--r--tests/end2end/fixtures/webserver.py6
-rw-r--r--tests/end2end/test_invocations.py47
-rw-r--r--tests/unit/browser/test_history.py25
-rw-r--r--tests/unit/browser/webkit/test_webkitelem.py2
-rw-r--r--tests/unit/commands/test_parser.py (renamed from tests/unit/commands/test_runners.py)56
-rw-r--r--tests/unit/commands/test_userscripts.py22
-rw-r--r--tests/unit/completion/test_models.py7
-rw-r--r--tests/unit/config/test_config.py44
-rw-r--r--tests/unit/config/test_configcache.py2
-rw-r--r--tests/unit/config/test_configcommands.py34
-rw-r--r--tests/unit/config/test_configfiles.py62
-rw-r--r--tests/unit/config/test_qtargs.py78
-rw-r--r--tests/unit/mainwindow/test_prompt.py14
-rw-r--r--tests/unit/misc/test_guiprocess.py7
-rw-r--r--tests/unit/misc/userscripts/test_qute_lastpass.py1
-rw-r--r--tests/unit/utils/test_standarddir.py42
-rw-r--r--tests/unit/utils/test_utils.py7
-rw-r--r--tests/unit/utils/test_version.py42
-rw-r--r--tox.ini12
77 files changed, 1261 insertions, 659 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 098658797..0b507dc65 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 2.0.2
+current_version = 2.1.0
commit = True
message = Release v{new_version}
tag = True
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 8bbaef0b1..9df59f0dc 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -15,10 +15,73 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
+[[v2.2.0]]
+v2.2.0 (unreleased)
+-------------------
+
+Deprecated
+~~~~~~~~~~
+
+- Running qutebrowser with Qt 5.12.0 is now unsupported and logs a warning. It
+ should still work, however, a workaround for issues with the Nvidia graphic
+ driver was dropped. Newer Qt 5.12.x versions are still fully supported.
+
+Added
+~~~~~
+
+- New `input.media_keys` setting which can be used to disable Chromium's
+ handling of media keys.
+
+Changed
+~~~~~~~
+
+- The completion now also shows bindings starting with `set-cmd-text` in its
+ third column, such as `o` for `:open`.
+
+[[v2.1.1]]
+v2.1.1 (unreleased)
+-------------------
+
+Changed
+~~~~~~~
+
+- Clicking the 'x' in the devtools window to hide it now also leaves insert
+ mode.
+
+Fixed
+~~~~~
+
+- The workaround for black on (almost) black formula images in dark mode now
+ also works with Qt 5.12 and 5.13.
+- When running in Flatpak, the QtWebEngine version is now detected properly.
+ Before, a wrong version was assumed, breaking dark mode and certain workarounds
+ (resulting in crashes on websites like LinkedIn or TradingView).
+- When running in Flatpak, communicating with an existing instance now works
+ properly. Before, a new instance was always opened.
+- When the metainfo in the completion database doesn't have the expected
+ structure, qutebrowser now tries to gracefully recover from the situation
+ instead of crashing.
+
[[v2.1.0]]
-v2.1.0 (unreleased)
+v2.1.0 (2021-03-12)
-------------------
+Removed
+~~~~~~~
+
+- The following command aliases were deprecated in v2.0.0 and are now removed:
+ * `run-macro` -> `macro-run`
+ * `record-macro` -> `macro-record`
+ * `buffer` -> `tab-select`
+ * `open-editor` -> `edit-text`
+ * `toggle-selection` -> `selection-toggle`
+ * `drop-selection` -> `selection-drop`
+ * `reverse-selection` -> `selection-reverse`
+ * `follow-selected` -> `selection-follow`
+ * `follow-hint` -> `hint-follow`
+ * `enter-mode` -> `mode-enter`
+ * `leave-mode` -> `mode-leave`
+
Added
~~~~~
@@ -32,7 +95,7 @@ Added
Changed
~~~~~~~
-- Initial support for Qt 5.15.3 and PyQt 5.15.3
+- Initial support for QtWebEngine 5.15.3 and PyQt 5.15.3/.4
- The `colors.webpage.prefers_color_scheme_dark` setting got renamed to
`colors.webpage.preferred_color_scheme` and now takes the values `auto`, `light`
and `dark` (instead of being `True` for dark and `False` for auto).
@@ -70,9 +133,15 @@ Changed
Fixed
~~~~~
+- With QtWebEngine 5.15.3 and some locales, Chromium can't start its
+ subprocesses. As a result, qutebrowser only shows a blank page and logs
+ "Network service crashed, restarting service.". This release adds a
+ `qt.workarounds.locale` setting working around the issue. It is disabled by
+ default since distributions shipping 5.15.3 will probably have a proper patch
+ for it backported very soon.
- The `colors.webpage.preferred_color_scheme` and `colors.webpage.darkmode.*`
- settings now work correctly with the upcoming QtWebEngine 5.15.3 (and Gentoo,
- which at the time of writing packages 5.15.3 disguised as 5.15.2).
+ settings now work correctly with QtWebEngine 5.15.3 (and Gentoo, which at the
+ time of writing packages 5.15.3 disguised as 5.15.2).
- When dark mode settings were set, existing `blink-features` arguments in
`qt.args` (or `--qt-flag`) were overridden. They are now combined properly.
- On QtWebEngine 5.15.2, auto detection for the `prefers-color-scheme` media
@@ -97,12 +166,19 @@ Fixed
accidentally treated that as "@run-at document-idle". However, other
GreaseMonkey implementations default to "@run-at document-end" instead, which
is what qutebrowser now does, too.
-- With QtWebEngine 5.15.3 and some locales, Chromium can't start its
- subprocesses. As a result, qutebrowser only shows a blank page and logs
- "Network service crashed, restarting service.". This release adds a
- `qt.workarounds.locale` setting working around the issue. It is disabled by
- default since distributions shipping 5.15.3 will probably have a proper patch
- for it backported very soon.
+- The `hist_importer.py` script didn't work correctly after qutebrowser v2.0.0
+ and resulted in a history database qutebrowser couldn't read properly. It now
+ works properly again.
+- With certain QtWebEngine versions (5.15.0 based on Chromium 80 and 5.15.3
+ based on Chromium 87), Chromium's dark mode doesn't invert certain SVG images,
+ even with `colors.wegpage.darkmode.policy.images` set to `smart`.
+ Most notably, this causes formulae on Wikipedia to display black on (almost)
+ black. If `content.site_specific_quirks` is enabled, qutebrowser now injects
+ some CSS as a workaround, which inverts all math formula images on Wikipedia
+ (and potentially other sites, if they use the same CSS class).
+- When a hint label text started with an apostrophe, it would show an escaped
+ text until the hints first character has been pressed. It now shows up
+ correctly.
[[v2.0.2]]
v2.0.2 (2021-02-04)
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 7a5cfd47b..8b2964f4f 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -261,6 +261,7 @@
|<<input.insert_mode.leave_on_load,input.insert_mode.leave_on_load>>|Leave insert mode when starting a new page load.
|<<input.insert_mode.plugins,input.insert_mode.plugins>>|Switch to insert mode when clicking flash and other plugins.
|<<input.links_included_in_focus_chain,input.links_included_in_focus_chain>>|Include hyperlinks in the keyboard focus chain when tabbing.
+|<<input.media_keys,input.media_keys>>|Whether the underlying Chromium should handle media keys.
|<<input.mouse.back_forward_buttons,input.mouse.back_forward_buttons>>|Enable back and forward buttons on the mouse.
|<<input.mouse.rocker_gestures,input.mouse.rocker_gestures>>|Enable Opera-like mouse rocker gestures.
|<<input.partial_timeout,input.partial_timeout>>|Timeout (in milliseconds) for partially typed key bindings.
@@ -1600,6 +1601,8 @@ The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated like
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,String>>
Valid values:
@@ -1610,8 +1613,6 @@ Valid values:
Default: +pass:[lightness-cielab]+
-This setting is only available with the QtWebEngine backend.
-
[[colors.webpage.darkmode.contrast]]
=== colors.webpage.darkmode.contrast
Contrast for dark mode.
@@ -1619,12 +1620,12 @@ This only has an effect when `colors.webpage.darkmode.algorithm` is set to `ligh
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,Float>>
Default: +pass:[0.0]+
-This setting is only available with the QtWebEngine backend.
-
[[colors.webpage.darkmode.enabled]]
=== colors.webpage.darkmode.enabled
Render all web contents using a dark theme.
@@ -1645,12 +1646,12 @@ Example configurations from Chromium's `chrome://flags`:
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,Bool>>
Default: +pass:[false]+
-This setting is only available with the QtWebEngine backend.
-
[[colors.webpage.darkmode.grayscale.all]]
=== colors.webpage.darkmode.grayscale.all
Render all colors as grayscale.
@@ -1658,12 +1659,12 @@ This only has an effect when `colors.webpage.darkmode.algorithm` is set to `ligh
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,Bool>>
Default: +pass:[false]+
-This setting is only available with the QtWebEngine backend.
-
[[colors.webpage.darkmode.grayscale.images]]
=== colors.webpage.darkmode.grayscale.images
Desaturation factor for images in dark mode.
@@ -1671,14 +1672,14 @@ If set to 0, images are left as-is. If set to 1, images are completely grayscale
This setting requires a restart.
-Type: <<types,Float>>
-
-Default: +pass:[0.0]+
-
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
+Type: <<types,Float>>
+
+Default: +pass:[0.0]+
+
[[colors.webpage.darkmode.policy.images]]
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
@@ -1686,6 +1687,8 @@ With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashe
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,String>>
Valid values:
@@ -1696,8 +1699,6 @@ Valid values:
Default: +pass:[smart]+
-This setting is only available with the QtWebEngine backend.
-
[[colors.webpage.darkmode.policy.page]]
=== colors.webpage.darkmode.policy.page
Which pages to apply dark mode to.
@@ -1705,6 +1706,10 @@ The underlying Chromium setting has been removed in QtWebEngine 5.15.3, thus thi
This setting requires a restart.
+On QtWebEngine, this setting requires Qt 5.14 or newer.
+
+On QtWebKit, this setting is unavailable.
+
Type: <<types,String>>
Valid values:
@@ -1714,10 +1719,6 @@ Valid values:
Default: +pass:[smart]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
-
-On QtWebKit, this setting is unavailable.
-
[[colors.webpage.darkmode.threshold.background]]
=== colors.webpage.darkmode.threshold.background
Threshold for inverting background elements with dark mode.
@@ -1726,14 +1727,14 @@ Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`!
This setting requires a restart.
-Type: <<types,Int>>
-
-Default: +pass:[0]+
-
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
+Type: <<types,Int>>
+
+Default: +pass:[0]+
+
[[colors.webpage.darkmode.threshold.text]]
=== colors.webpage.darkmode.threshold.text
Threshold for inverting text with dark mode.
@@ -1741,14 +1742,14 @@ Text colors with brightness below this threshold will be inverted, and above it
This setting requires a restart.
-Type: <<types,Int>>
-
-Default: +pass:[256]+
-
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
+Type: <<types,Int>>
+
+Default: +pass:[256]+
+
[[colors.webpage.preferred_color_scheme]]
=== colors.webpage.preferred_color_scheme
Value to use for `prefers-color-scheme:` for websites.
@@ -1757,6 +1758,10 @@ The "auto" value is broken on QtWebEngine 5.15.2 due to a Qt bug. There, it will
This setting requires a restart.
+On QtWebEngine, this setting requires Qt 5.14 or newer.
+
+On QtWebKit, this setting is unavailable.
+
Type: <<types,String>>
Valid values:
@@ -1767,10 +1772,6 @@ Valid values:
Default: +pass:[auto]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
-
-On QtWebKit, this setting is unavailable.
-
[[completion.cmd_history_max_items]]
=== completion.cmd_history_max_items
Number of commands to save in the command history.
@@ -1942,12 +1943,12 @@ Automatically start playing `<video>` elements.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,Bool>>
Default: +pass:[true]+
-This setting is only available with the QtWebEngine backend.
-
[[content.blocking.adblock.lists]]
=== content.blocking.adblock.lists
List of URLs to ABP-style adblocking rulesets.
@@ -2040,24 +2041,24 @@ An application cache acts like an HTTP cache in some sense. For documents that u
This setting supports URL patterns.
+This setting is only available with the QtWebKit backend.
+
Type: <<types,Bool>>
Default: +pass:[true]+
-This setting is only available with the QtWebKit backend.
-
[[content.cache.maximum_pages]]
=== content.cache.maximum_pages
Maximum number of pages to hold in the global memory page cache.
The page cache allows for a nicer user experience when navigating forth or back to pages in the forward/back history, by pausing and resuming up to _n_ pages.
For more information about the feature, please refer to: https://webkit.org/blog/427/webkit-page-cache-i-the-basics/
+This setting is only available with the QtWebKit backend.
+
Type: <<types,Int>>
Default: +pass:[0]+
-This setting is only available with the QtWebKit backend.
-
[[content.cache.size]]
=== content.cache.size
Size (in bytes) of the HTTP network cache. Null to use the default value.
@@ -2074,12 +2075,12 @@ Note this is needed for some websites to work properly.
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,Bool>>
Default: +pass:[true]+
-This setting is only available with the QtWebEngine backend.
-
[[content.cookies.accept]]
=== content.cookies.accept
Which cookies to accept.
@@ -2139,12 +2140,12 @@ Try to pre-fetch DNS entries to speed up browsing.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,Bool>>
Default: +pass:[true]+
-This setting is only available with the QtWebEngine backend.
-
[[content.frame_flattening]]
=== content.frame_flattening
Expand each subframe to its contents.
@@ -2152,12 +2153,12 @@ This will flatten all the frames to become one scrollable page.
This setting supports URL patterns.
+This setting is only available with the QtWebKit backend.
+
Type: <<types,Bool>>
Default: +pass:[false]+
-This setting is only available with the QtWebKit backend.
-
[[content.fullscreen.overlay_timeout]]
=== content.fullscreen.overlay_timeout
Set fullscreen notification overlay timeout in milliseconds.
@@ -2316,12 +2317,12 @@ Allow JavaScript to close tabs.
This setting supports URL patterns.
+This setting is only available with the QtWebKit backend.
+
Type: <<types,Bool>>
Default: +pass:[false]+
-This setting is only available with the QtWebKit backend.
-
[[content.javascript.can_open_tabs_automatically]]
=== content.javascript.can_open_tabs_automatically
Allow JavaScript to open new tabs without user interaction.
@@ -2410,6 +2411,8 @@ Allow websites to record audio.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,BoolAsk>>
Valid values:
@@ -2420,14 +2423,14 @@ Valid values:
Default: +pass:[ask]+
-This setting is only available with the QtWebEngine backend.
-
[[content.media.audio_video_capture]]
=== content.media.audio_video_capture
Allow websites to record audio and video.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,BoolAsk>>
Valid values:
@@ -2438,14 +2441,14 @@ Valid values:
Default: +pass:[ask]+
-This setting is only available with the QtWebEngine backend.
-
[[content.media.video_capture]]
=== content.media.video_capture
Allow websites to record video.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,BoolAsk>>
Valid values:
@@ -2456,14 +2459,14 @@ Valid values:
Default: +pass:[ask]+
-This setting is only available with the QtWebEngine backend.
-
[[content.mouse_lock]]
=== content.mouse_lock
Allow websites to lock your mouse pointer.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,BoolAsk>>
Valid values:
@@ -2474,8 +2477,6 @@ Valid values:
Default: +pass:[ask]+
-This setting is only available with the QtWebEngine backend.
-
[[content.mute]]
=== content.mute
Automatically mute tabs.
@@ -2502,6 +2503,8 @@ Allow websites to show notifications.
This setting supports URL patterns.
+On QtWebEngine, this setting requires Qt 5.13 or newer.
+
Type: <<types,BoolAsk>>
Valid values:
@@ -2512,8 +2515,6 @@ Valid values:
Default: +pass:[ask]+
-On QtWebEngine, this setting requires Qt 5.13 or newer.
-
[[content.pdfjs]]
=== content.pdfjs
Allow pdf.js to view PDF files in the browser.
@@ -2531,6 +2532,8 @@ Allow websites to request persistent storage quota via `navigator.webkitPersiste
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,BoolAsk>>
Valid values:
@@ -2541,8 +2544,6 @@ Valid values:
Default: +pass:[ask]+
-This setting is only available with the QtWebEngine backend.
-
[[content.plugins]]
=== content.plugins
Enable plugins in Web pages.
@@ -2559,12 +2560,12 @@ Draw the background color and images also when the page is printed.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,Bool>>
Default: +pass:[true]+
-This setting is only available with the QtWebEngine backend.
-
[[content.private_browsing]]
=== content.private_browsing
Open new windows in private browsing mode which does not record visited pages.
@@ -2592,18 +2593,20 @@ Default: +pass:[system]+
=== content.proxy_dns_requests
Send DNS requests over the configured proxy.
+This setting is only available with the QtWebKit backend.
+
Type: <<types,Bool>>
Default: +pass:[true]+
-This setting is only available with the QtWebKit backend.
-
[[content.register_protocol_handler]]
=== content.register_protocol_handler
Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,BoolAsk>>
Valid values:
@@ -2614,8 +2617,6 @@ Valid values:
Default: +pass:[ask]+
-This setting is only available with the QtWebEngine backend.
-
[[content.site_specific_quirks]]
=== content.site_specific_quirks
Enable quirks (such as faked user agent headers) needed to get specific sites to work properly.
@@ -2648,6 +2649,8 @@ How navigation requests to URLs with unknown schemes are handled.
This setting supports URL patterns.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,String>>
Valid values:
@@ -2658,8 +2661,6 @@ Valid values:
Default: +pass:[allow-from-user-interaction]+
-This setting is only available with the QtWebEngine backend.
-
[[content.user_stylesheets]]
=== content.user_stylesheets
List of user stylesheet filenames to use.
@@ -2684,6 +2685,8 @@ Which interfaces to expose via WebRTC.
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,String>>
Valid values:
@@ -2695,8 +2698,6 @@ Valid values:
Default: +pass:[all-interfaces]+
-This setting is only available with the QtWebEngine backend.
-
[[content.xss_auditing]]
=== content.xss_auditing
Monitor load requests for cross-site scripting attempts.
@@ -3120,6 +3121,8 @@ Default: +pass:[/usr/share/dict/words]+
=== hints.find_implementation
Which implementation to use to find elements to hint.
+This setting is only available with the QtWebKit backend.
+
Type: <<types,String>>
Valid values:
@@ -3129,8 +3132,6 @@ Valid values:
Default: +pass:[python]+
-This setting is only available with the QtWebKit backend.
-
[[hints.hide_unmatched_rapid_hints]]
=== hints.hide_unmatched_rapid_hints
Hide unmatched hints in rapid mode.
@@ -3392,6 +3393,21 @@ Type: <<types,Bool>>
Default: +pass:[true]+
+[[input.media_keys]]
+=== input.media_keys
+Whether the underlying Chromium should handle media keys.
+On Linux, disabling this also disables Chromium's MPRIS integration.
+
+This setting requires a restart.
+
+On QtWebEngine, this setting requires Qt 5.14 or newer.
+
+On QtWebKit, this setting is unavailable.
+
+Type: <<types,Bool>>
+
+Default: +pass:[true]+
+
[[input.mouse.back_forward_buttons]]
=== input.mouse.back_forward_buttons
Enable back and forward buttons on the mouse.
@@ -3600,6 +3616,8 @@ This is needed for QtWebEngine to work with Nouveau drivers and can be useful in
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,String>>
Valid values:
@@ -3611,8 +3629,6 @@ Valid values:
Default: +pass:[none]+
-This setting is only available with the QtWebEngine backend.
-
[[qt.highdpi]]
=== qt.highdpi
Turn on Qt HighDPI scaling.
@@ -3632,6 +3648,8 @@ This improves the RAM usage of renderer processes, at the expense of performance
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,String>>
Valid values:
@@ -3642,8 +3660,6 @@ Valid values:
Default: +pass:[auto]+
-This setting is only available with the QtWebEngine backend.
-
[[qt.process_model]]
=== qt.process_model
Which Chromium process model to use.
@@ -3655,6 +3671,8 @@ See the following pages for more details:
This setting requires a restart.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,String>>
Valid values:
@@ -3665,20 +3683,18 @@ Valid values:
Default: +pass:[process-per-site-instance]+
-This setting is only available with the QtWebEngine backend.
-
[[qt.workarounds.locale]]
=== qt.workarounds.locale
Work around locale parsing issues in QtWebEngine 5.15.3.
With some locales, QtWebEngine 5.15.3 is unusable without this workaround. In affected scenarios, QtWebEngine will log "Network service crashed, restarting service." and only display a blank page.
However, It is expected that distributions shipping QtWebEngine 5.15.3 follow up with a proper fix soon, so it is disabled by default.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,Bool>>
Default: +pass:[false]+
-This setting is only available with the QtWebEngine backend.
-
[[qt.workarounds.remove_service_workers]]
=== qt.workarounds.remove_service_workers
Delete the QtWebEngine Service Worker directory on every start.
@@ -3741,12 +3757,12 @@ Default: +pass:[true]+
=== search.wrap
Wrap around at the top and bottom of the page when advancing through text matches using `:search-next` and `:search-prev`.
+On QtWebEngine, this setting requires Qt 5.14 or newer.
+
Type: <<types,Bool>>
Default: +pass:[true]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
-
[[session.default_name]]
=== session.default_name
Name of the session to save by default.
@@ -3769,6 +3785,8 @@ Default: +pass:[false]+
Languages to use for spell checking.
You can check for available languages and install dictionaries using scripts/dictcli.py. Run the script with -h/--help for instructions.
+This setting is only available with the QtWebEngine backend.
+
Type: <<types,List of String>>
Valid values:
@@ -3819,8 +3837,6 @@ Valid values:
Default: empty
-This setting is only available with the QtWebEngine backend.
-
[[statusbar.padding]]
=== statusbar.padding
Padding (in pixels) for the statusbar.
@@ -4438,12 +4454,12 @@ Apply the zoom factor on a frame only to the text or to all content.
This setting supports URL patterns.
+This setting is only available with the QtWebKit backend.
+
Type: <<types,Bool>>
Default: +pass:[false]+
-This setting is only available with the QtWebKit backend.
-
== Setting types
[[types]]
[options="header",width="75%",cols="25%,75%"]
diff --git a/doc/install.asciidoc b/doc/install.asciidoc
index 657539f89..3c2098ccc 100644
--- a/doc/install.asciidoc
+++ b/doc/install.asciidoc
@@ -239,11 +239,12 @@ qutebrowser is available
https://flathub.org/apps/details/org.qutebrowser.qutebrowser[on Flathub]
as `org.qutebrowser.qutebrowser`.
-WARNING: As of October 2020, the Flatpak package is severely outdated (qutebrowser
-v1.7.0 from July 2019) and, among other issues, misses fixes for a
-(low-severity) https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-4rcq-jv2f-898j[security issue].
-It's recommended to <<tox,install qutebrowser in a virtualenv>> instead, which
-is one of the officially maintained options and will always be up-to-date.
+NOTE: The Flatpak package is
+https://github.com/flathub/org.qutebrowser.qutebrowser/issues/8#issuecomment-799579975[looking for (co-)maintainers].
+The package recently was updated after being out of date for multiple years. It
+currently (March 2021) is up to date again. If that situation changes, consider
+to <<tox,install qutebrowser in a virtualenv>> instead, which is one of the
+officially maintained options and will always be up-to-date.
On FreeBSD
----------
diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml
index 343633cf6..a449e1ea8 100644
--- a/misc/org.qutebrowser.qutebrowser.appdata.xml
+++ b/misc/org.qutebrowser.qutebrowser.appdata.xml
@@ -44,6 +44,7 @@
</content_rating>
<releases>
<!-- Add new releases here -->
+<release version="2.1.0" date="2021-03-12"/>
<release version="2.0.2" date="2021-02-04"/>
<release version="2.0.1" date="2021-01-28"/>
<release version="2.0.0" date="2021-01-28"/>
diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt
index 570de63ee..f32dddc28 100644
--- a/misc/requirements/requirements-check-manifest.txt
+++ b/misc/requirements/requirements-check-manifest.txt
@@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-build==0.3.0
+build==0.3.1.post1
check-manifest==0.46
packaging==20.9
-pep517==0.9.1
+pep517==0.10.0
pyparsing==2.4.7
toml==0.10.2
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index c83b57860..e3c28c8e2 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==20.3.0
-flake8==3.8.4
-flake8-bugbear==21.3.1
+flake8==3.9.0
+flake8-bugbear==21.3.2
flake8-builtins==1.5.3
flake8-comprehensions==3.3.1
flake8-copyright==0.2.2
@@ -17,8 +17,8 @@ flake8-tidy-imports==4.2.1
flake8-tuple==0.4.1
mccabe==0.6.1
pep8-naming==0.11.1
-pycodestyle==2.6.0
+pycodestyle==2.7.0
pydocstyle==5.1.1
-pyflakes==2.2.0
+pyflakes==2.3.0
six==1.15.0
snowballstemmer==2.1.0
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index dfa80656b..fcbc1d3f0 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
chardet==4.0.0
-diff-cover==4.2.3
-importlib-metadata==3.7.2
+diff-cover==5.0.1
+importlib-metadata==3.7.3
importlib-resources==5.1.2
-inflect==3.0.2
+inflect==5.3.0
Jinja2==2.11.3
jinja2-pluralize==0.3.0
lxml==4.6.2
diff --git a/misc/requirements/requirements-pyqt-pyinstaller.txt b/misc/requirements/requirements-pyqt-pyinstaller.txt
new file mode 100644
index 000000000..31ecefad5
--- /dev/null
+++ b/misc/requirements/requirements-pyqt-pyinstaller.txt
@@ -0,0 +1,7 @@
+# This file is automatically generated by scripts/dev/recompile_requirements.py
+
+PyQt5==5.15.3
+PyQt5-Qt==5.15.2
+PyQt5-sip==12.8.1
+PyQtWebEngine==5.15.3
+PyQtWebEngine-Qt==5.15.2
diff --git a/misc/requirements/requirements-pyqt-pyinstaller.txt-raw b/misc/requirements/requirements-pyqt-pyinstaller.txt-raw
new file mode 100644
index 000000000..89b5644da
--- /dev/null
+++ b/misc/requirements/requirements-pyqt-pyinstaller.txt-raw
@@ -0,0 +1,2 @@
+PyQt5==5.15.3
+PyQtWebEngine==5.15.3
diff --git a/misc/requirements/requirements-tests-bleeding.txt b/misc/requirements/requirements-tests-bleeding.txt
new file mode 100644
index 000000000..cbefb99f3
--- /dev/null
+++ b/misc/requirements/requirements-tests-bleeding.txt
@@ -0,0 +1,37 @@
+# Problematic: needs bzr
+# bzr+lp:beautifulsoup
+beautifulsoup4
+git+https://github.com/cherrypy/cheroot.git
+git+https://github.com/nedbat/coveragepy.git
+git+https://github.com/pallets/flask.git
+git+https://github.com/pallets/werkzeug.git # transitive dep, but needed to work
+git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-python
+git+https://github.com/pytest-dev/pytest.git
+git+https://github.com/pytest-dev/pytest-bdd.git
+git+https://github.com/ionelmc/pytest-benchmark.git
+git+https://github.com/pytest-dev/pytest-instafail.git
+git+https://github.com/pytest-dev/pytest-mock.git
+git+https://github.com/pytest-dev/pytest-qt.git
+git+https://github.com/pytest-dev/pytest-rerunfailures.git
+
+git+https://github.com/ionelmc/python-hunter.git
+git+https://github.com/jendrikseipp/vulture.git
+git+https://github.com/pygments/pygments.git
+git+https://github.com/pytest-dev/pytest-repeat.git
+git+https://github.com/pytest-dev/pytest-cov.git
+git+https://github.com/The-Compiler/pytest-xvfb.git
+git+https://github.com/pytest-dev/pytest-xdist.git
+git+https://github.com/hjwp/pytest-icdiff.git
+git+https://github.com/john-kurkowski/tldextract
+# Problematic: needs rust (and some time to build)
+# git+https://github.com/ArniDagur/python-adblock.git
+adblock ; python_version!="3.10"
+
+## qutebrowser dependencies
+
+git+https://github.com/pallets/jinja.git
+git+https://github.com/yaml/pyyaml.git
+git+https://github.com/tartley/colorama.git
+
+# https://github.com/pyparsing/pyparsing/issues/271
+pyparsing!=3.0.0b2,!=3.0.0b1
diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt
deleted file mode 100644
index 6fc4bb460..000000000
--- a/misc/requirements/requirements-tests-git.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-bzr+lp:beautifulsoup
-git+https://github.com/cherrypy/cheroot.git
-hg+https://bitbucket.org/ned/coveragepy
-git+https://github.com/micheles/decorator.git
-git+https://github.com/pallets/flask.git
-git+https://github.com/miracle2k/python-glob2.git
-git+https://github.com/HypothesisWorks/hypothesis-python.git
-git+https://github.com/pallets/itsdangerous.git
-git+https://bitbucket.org/zzzeek/mako.git
-git+https://github.com/r1chardj0n3s/parse.git
-git+https://github.com/jenisys/parse_type.git
-hg+https://bitbucket.org/pytest-dev/py
-git+https://github.com/pytest-dev/pytest.git@features
-git+https://github.com/pytest-dev/pytest-bdd.git
-git+https://github.com/pytest-dev/pytest-cov.git
-git+https://github.com/pytest-dev/pytest-instafail.git
-git+https://github.com/pytest-dev/pytest-mock.git
-git+https://github.com/pytest-dev/pytest-qt.git
-git+https://github.com/pytest-dev/pytest-repeat.git
-git+https://github.com/pytest-dev/pytest-rerunfailures.git
-git+https://github.com/The-Compiler/pytest-xvfb.git
-hg+https://bitbucket.org/gutworth/six
-hg+https://bitbucket.org/jendrikseipp/vulture
-git+https://github.com/pallets/werkzeug.git
-
-
-## qutebrowser dependencies
-
-git+https://github.com/tartley/colorama.git
-git+https://github.com/pallets/jinja.git
-git+https://github.com/pallets/markupsafe.git
-git+https://github.com/pygments/pygments.git
-git+https://github.com/python-attrs/attrs.git
-git+https://github.com/yaml/pyyaml.git
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 2bfaf91e0..aa96c9417 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -15,7 +15,7 @@ filelock==3.0.12
Flask==1.1.2
glob2==0.7
hunter==3.3.1
-hypothesis==6.6.0
+hypothesis==6.8.1
icdiff==1.9.1
idna==2.10
iniconfig==1.1.1
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index d44522118..f301f3bbd 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -8,7 +8,7 @@ pip==21.0.1
pluggy==0.13.1
py==1.10.0
pyparsing==2.4.7
-setuptools==54.1.1
+setuptools==54.1.2
six==1.15.0
toml==0.10.2
tox==3.23.0
diff --git a/misc/userscripts/qute-keepassxc b/misc/userscripts/qute-keepassxc
index f0127590b..11d0a3384 100755
--- a/misc/userscripts/qute-keepassxc
+++ b/misc/userscripts/qute-keepassxc
@@ -70,7 +70,7 @@ GPG might then ask for your private-key passwort whenever you query the database
[3]: https://gnupg.org/
[4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md
[5]: https://github.com/qutebrowser/qutebrowser/blob/master/doc/userscripts.asciidoc
-[6]: https://keepassxc.org/docs/keepassxc-browser-migration/
+[6]: https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration
"""
import sys
diff --git a/misc/userscripts/qute-lastpass b/misc/userscripts/qute-lastpass
index ac9783641..d2a72f077 100755
--- a/misc/userscripts/qute-lastpass
+++ b/misc/userscripts/qute-lastpass
@@ -60,6 +60,8 @@ argument_parser.add_argument('--io-encoding', '-i', default='UTF-8',
help='Encoding used to communicate with subprocesses')
argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
help='Merge pass candidates for fully-qualified and registered domain name')
+argument_parser.add_argument('--no-tld-download', action='store_true',
+ help="Don't download TLD list")
group = argument_parser.add_mutually_exclusive_group()
group.add_argument('--username-only', '-e',
action='store_true', help='Only insert username')
@@ -117,7 +119,12 @@ def main(arguments):
argument_parser.print_help()
return ExitCodes.FAILURE
- extract_result = tldextract.extract(arguments.url)
+ if arguments.no_tld_download:
+ extract = tldextract.TLDExtract(suffix_list_urls=None)
+ else:
+ extract = tldextract.extract
+
+ extract_result = extract(arguments.url)
# Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
# the registered domain name and finally: the IPv4 address if that's what
diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py
index 9abb8a30e..b2542cacc 100644
--- a/qutebrowser/__init__.py
+++ b/qutebrowser/__init__.py
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2021 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
-__version__ = "2.0.2"
+__version__ = "2.1.0"
__version_info__ = tuple(int(part) for part in __version__.split('.'))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index f2dd282df..f1710adb9 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -917,7 +917,7 @@ class CommandDispatcher:
return (tabbed_browser, tabbed_browser.widget.widget(idx-1))
@cmdutils.register(instance='command-dispatcher', scope='window',
- maxsplit=0, deprecated_name='buffer')
+ maxsplit=0)
@cmdutils.argument('index', completion=miscmodels.tabs)
@cmdutils.argument('count', value=cmdutils.Value.count)
def tab_select(self, index=None, count=None):
@@ -1488,8 +1488,7 @@ class CommandDispatcher:
objreg.last_focused_window(), alert=False))
ed.edit(text, caret_position)
- @cmdutils.register(instance='command-dispatcher', scope='window',
- deprecated_name='open-editor')
+ @cmdutils.register(instance='command-dispatcher', scope='window')
def edit_text(self):
"""Open an external editor with the currently selected form field.
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 0e71f2373..333c532d3 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -92,6 +92,8 @@ class HintLabel(QLabel):
self._context = context
self.elem = elem
+ self.setTextFormat(Qt.RichText)
+
# Make sure we can style the background via a style sheet, and we don't
# get any extra text indent from Qt.
# The real stylesheet lives in mainwindow.py for performance reasons..
@@ -1000,7 +1002,7 @@ class HintManager(QObject):
self._context.first_run = False
@cmdutils.register(instance='hintmanager', scope='window',
- modes=[usertypes.KeyMode.hint], deprecated_name='follow-hint')
+ modes=[usertypes.KeyMode.hint])
def hint_follow(self, select: bool = False, keystring: str = None) -> None:
"""Follow a hint.
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
index ef4650a35..773c6cc51 100644
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -92,8 +92,11 @@ class CompletionMetaInfo(sql.SqlTable):
}
def __init__(self, parent=None):
- super().__init__("CompletionMetaInfo", ['key', 'value'],
- constraints={'key': 'PRIMARY KEY'})
+ self._fields = ['key', 'value']
+ self._constraints = {'key': 'PRIMARY KEY'}
+ super().__init__(
+ "CompletionMetaInfo", self._fields, constraints=self._constraints)
+
if sql.user_version_changed():
self._init_default_values()
@@ -101,6 +104,15 @@ class CompletionMetaInfo(sql.SqlTable):
if key not in self.KEYS:
raise KeyError(key)
+ def try_recover(self):
+ """Try recovering the table structure.
+
+ This should be called if getting a value via __getattr__ failed. In theory, this
+ should never happen, in practice, it does.
+ """
+ self._create_table(self._fields, constraints=self._constraints, force=True)
+ self._init_default_values()
+
def _init_default_values(self):
for key, default in self.KEYS.items():
if key not in self:
@@ -164,7 +176,13 @@ class WebHistory(sql.SqlTable):
self.completion = CompletionHistory(parent=self)
self.metainfo = CompletionMetaInfo(parent=self)
- rebuild_completion = self.metainfo['force_rebuild']
+ try:
+ rebuild_completion = self.metainfo['force_rebuild']
+ except sql.BugError: # pragma: no cover
+ log.sql.warning("Failed to access meta info, trying to recover...",
+ exc_info=True)
+ self.metainfo.try_recover()
+ rebuild_completion = self.metainfo['force_rebuild']
if sql.user_version_changed():
# If the DB user version changed, run a full cleanup and rebuild the
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 94332ffcb..b3a0da51d 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -30,9 +30,9 @@ from PyQt5.QtCore import QUrl
from qutebrowser.config import config
from qutebrowser.utils import (usertypes, message, log, objreg, jinja, utils,
- qtutils)
+ qtutils, version)
from qutebrowser.mainwindow import mainwindow
-from qutebrowser.misc import guiprocess
+from qutebrowser.misc import guiprocess, objects
class CallSuper(Exception):
@@ -298,6 +298,15 @@ def get_user_stylesheet(searching=False):
if setting == 'never' or setting == 'when-searching' and not searching:
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
+ if (objects.backend == usertypes.Backend.QtWebEngine and
+ version.qtwebengine_versions().chromium_major in [69, 73, 80, 87] and
+ config.val.colors.webpage.darkmode.enabled and
+ config.val.colors.webpage.darkmode.policy.images == 'smart' and
+ config.val.content.site_specific_quirks):
+ # WORKAROUND for MathML-output on Wikipedia being black on black.
+ # See https://bugs.chromium.org/p/chromium/issues/detail?id=1126606
+ css += '\nimg.mwe-math-fallback-image-inline { filter: invert(100%); }'
+
return css
diff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py
index 30a9ac6eb..1c6530b49 100644
--- a/qutebrowser/browser/webengine/darkmode.py
+++ b/qutebrowser/browser/webengine/darkmode.py
@@ -342,8 +342,7 @@ def _variant(versions: version.WebEngineVersions) -> Variant:
log.init.warning(f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}")
if (versions.webengine == utils.VersionNumber(5, 15, 2) and
- versions.chromium is not None and
- versions.chromium.startswith('87.')):
+ versions.chromium_major == 87):
# WORKAROUND for Gentoo packaging something newer as 5.15.2...
return Variant.qt_515_3
elif versions.webengine >= utils.VersionNumber(5, 15, 3):
diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py
index ee7782e10..ae31c0bee 100644
--- a/qutebrowser/browser/webengine/webengineinspector.py
+++ b/qutebrowser/browser/webengine/webengineinspector.py
@@ -28,7 +28,8 @@ from PyQt5.QtWidgets import QWidget
from qutebrowser.browser import inspector
from qutebrowser.browser.webengine import webenginesettings
from qutebrowser.misc import miscwidgets
-from qutebrowser.utils import version
+from qutebrowser.utils import version, usertypes
+from qutebrowser.keyinput import modeman
class WebEngineInspectorView(QWebEngineView):
@@ -60,9 +61,23 @@ class WebEngineInspector(inspector.AbstractWebInspector):
parent: QWidget = None) -> None:
super().__init__(splitter, win_id, parent)
self._check_devtools_resources()
+
view = WebEngineInspectorView()
self._settings = webenginesettings.WebEngineSettings(view.settings())
self._set_widget(view)
+ page = view.page()
+ page.windowCloseRequested.connect( # type: ignore[attr-defined]
+ self._on_window_close_requested)
+
+ def _on_window_close_requested(self) -> None:
+ """Called when the 'x' was clicked in the devtools."""
+ modeman.leave(
+ self._win_id,
+ usertypes.KeyMode.insert,
+ 'devtools close requested',
+ maybe=True,
+ )
+ self.hide()
def _check_devtools_resources(self) -> None:
"""Make sure that the devtools resources are available on Fedora.
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index a2e81da5f..090cdfd4c 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -38,7 +38,7 @@ from qutebrowser.browser.webengine import (spell, webenginequtescheme, cookies,
from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
from qutebrowser.utils import (standarddir, qtutils, message, log,
- urlmatch, usertypes, objreg)
+ urlmatch, usertypes, objreg, version)
if TYPE_CHECKING:
from qutebrowser.browser.webengine import interceptor
@@ -374,7 +374,17 @@ def _init_default_profile():
default_profile = QWebEngineProfile.defaultProfile()
+ assert parsed_user_agent is None # avoid earlier profile initialization
+ non_ua_version = version.qtwebengine_versions(avoid_init=True)
+
init_user_agent()
+ ua_version = version.qtwebengine_versions()
+ if ua_version.webengine != non_ua_version.webengine:
+ log.init.warning(
+ "QtWebEngine version mismatch - unexpected behavior might occur, "
+ "please open a bug about this.\n"
+ f" Early version: {non_ua_version}\n"
+ f" Real version: {ua_version}")
default_profile.setCachePath(
os.path.join(standarddir.cache(), 'webengine'))
@@ -489,13 +499,16 @@ def init():
from qutebrowser.misc import quitter
quitter.instance.shutting_down.connect(_download_manager.shutdown)
+ log.init.debug("Initializing global settings...")
global _global_settings
_global_settings = WebEngineSettings(_SettingsWrapper())
+ log.init.debug("Initializing profiles...")
_init_default_profile()
init_private_profile()
config.instance.changed.connect(_update_settings)
+ log.init.debug("Initializing site specific quirks...")
_init_site_specific_quirks()
_init_devtools_settings()
diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py
new file mode 100644
index 000000000..06a20cdf6
--- /dev/null
+++ b/qutebrowser/commands/parser.py
@@ -0,0 +1,209 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2021 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
+#
+# 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 <https://www.gnu.org/licenses/>.
+
+"""Module for parsing commands entered into the browser."""
+
+import dataclasses
+from typing import List, Iterator
+
+from qutebrowser.commands import cmdexc, command
+from qutebrowser.misc import split, objects
+from qutebrowser.config import config
+
+
+@dataclasses.dataclass
+class ParseResult:
+
+ """The result of parsing a commandline."""
+
+ cmd: command.Command
+ args: List[str]
+ cmdline: List[str]
+
+
+class CommandParser:
+
+ """Parse qutebrowser commandline commands.
+
+ Attributes:
+ _partial_match: Whether to allow partial command matches.
+ """
+
+ def __init__(self, partial_match: bool = False) -> None:
+ self._partial_match = partial_match
+
+ def _get_alias(self, text: str, *, default: str) -> str:
+ """Get an alias from the config.
+
+ Args:
+ text: The text to parse.
+ aliases: A map of aliases to commands.
+ default : Default value to return when alias was not found.
+
+ Return:
+ The new command string if an alias was found. Default value
+ otherwise.
+ """
+ parts = text.strip().split(maxsplit=1)
+ aliases = config.cache['aliases']
+ if parts[0] not in aliases:
+ return default
+ alias = aliases[parts[0]]
+
+ try:
+ new_cmd = '{} {}'.format(alias, parts[1])
+ except IndexError:
+ new_cmd = alias
+ if text.endswith(' '):
+ new_cmd += ' '
+ return new_cmd
+
+ def _parse_all_gen(
+ self,
+ text: str,
+ aliases: bool = True,
+ **kwargs: bool,
+ ) -> Iterator[ParseResult]:
+ """Split a command on ;; and parse all parts.
+
+ If the first command in the commandline is a non-split one, it only
+ returns that.
+
+ Args:
+ text: Text to parse.
+ aliases: Whether to handle aliases.
+ **kwargs: Passed to parse().
+
+ Yields:
+ ParseResult tuples.
+ """
+ text = text.strip().lstrip(':').strip()
+ if not text:
+ raise cmdexc.NoSuchCommandError("No command given")
+
+ if aliases:
+ text = self._get_alias(text, default=text)
+
+ if ';;' in text:
+ # Get the first command and check if it doesn't want to have ;;
+ # split.
+ first = text.split(';;')[0]
+ result = self.parse(first, **kwargs)
+ if result.cmd.no_cmd_split:
+ sub_texts = [text]
+ else:
+ sub_texts = [e.strip() for e in text.split(';;')]
+ else:
+ sub_texts = [text]
+ for sub in sub_texts:
+ yield self.parse(sub, **kwargs)
+
+ def parse_all(self, text: str, **kwargs: bool) -> List[ParseResult]:
+ """Wrapper over _parse_all_gen."""
+ return list(self._parse_all_gen(text, **kwargs))
+
+ def parse(self, text: str, *, keep: bool = False) -> ParseResult:
+ """Split the commandline text into command and arguments.
+
+ Args:
+ text: Text to parse.
+ keep: Whether to keep special chars and whitespace.
+ """
+ cmdstr, sep, argstr = text.partition(' ')
+
+ if not cmdstr:
+ raise cmdexc.NoSuchCommandError("No command given")
+
+ if self._partial_match:
+ cmdstr = self._completion_match(cmdstr)
+
+ try:
+ cmd = objects.commands[cmdstr]
+ except KeyError:
+ raise cmdexc.NoSuchCommandError(f'{cmdstr}: no such command')
+
+ args = self._split_args(cmd, argstr, keep)
+ if keep and args:
+ cmdline = [cmdstr, sep + args[0]] + args[1:]
+ elif keep:
+ cmdline = [cmdstr, sep]
+ else:
+ cmdline = [cmdstr] + args[:]
+
+ return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
+
+ def _completion_match(self, cmdstr: str) -> str:
+ """Replace cmdstr with a matching completion if there's only one match.
+
+ Args:
+ cmdstr: The string representing the entered command so far.
+
+ Return:
+ cmdstr modified to the matching completion or unmodified
+ """
+ matches = [cmd for cmd in sorted(objects.commands, key=len)
+ if cmdstr in cmd]
+ if len(matches) == 1:
+ cmdstr = matches[0]
+ elif len(matches) > 1 and config.val.completion.use_best_match:
+ cmdstr = matches[0]
+ return cmdstr
+
+ def _split_args(self, cmd: command.Command, argstr: str, keep: bool) -> List[str]:
+ """Split the arguments from an arg string.
+
+ Args:
+ cmd: The command we're currently handling.
+ argstr: An argument string.
+ keep: Whether to keep special chars and whitespace
+
+ Return:
+ A list containing the split strings.
+ """
+ if not argstr:
+ return []
+ elif cmd.maxsplit is None:
+ return split.split(argstr, keep=keep)
+ else:
+ # If split=False, we still want to split the flags, but not
+ # everything after that.
+ # We first split the arg string and check the index of the first
+ # non-flag args, then we re-split again properly.
+ # example:
+ #
+ # input: "--foo -v bar baz"
+ # first split: ['--foo', '-v', 'bar', 'baz']
+ # 0 1 2 3
+ # second split: ['--foo', '-v', 'bar baz']
+ # (maxsplit=2)
+ split_args = split.simple_split(argstr, keep=keep)
+ flag_arg_count = 0
+ for i, arg in enumerate(split_args):
+ arg = arg.strip()
+ if arg.startswith('-'):
+ if arg in cmd.flags_with_args:
+ flag_arg_count += 1
+ else:
+ maxsplit = i + cmd.maxsplit + flag_arg_count
+ return split.simple_split(argstr, keep=keep,
+ maxsplit=maxsplit)
+
+ # If there are only flags, we got it right on the first try
+ # already.
+ return split_args
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
index 4d53295dd..5fb054455 100644
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -22,17 +22,13 @@
import traceback
import re
import contextlib
-import dataclasses
-from typing import (TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping,
- List, Optional)
+from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.api import cmdutils
-from qutebrowser.config import config
-from qutebrowser.commands import cmdexc, command
+from qutebrowser.commands import cmdexc, parser
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
-from qutebrowser.misc import split, objects
from qutebrowser.keyinput import macros, modeman
if TYPE_CHECKING:
@@ -43,16 +39,6 @@ _ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str]
last_command = {}
-@dataclasses.dataclass
-class ParseResult:
-
- """The result of parsing a commandline."""
-
- cmd: Optional[command.Command]
- args: Optional[List[str]]
- cmdline: List[str]
-
-
def _url(tabbed_browser):
"""Convenience method to get the current url."""
try:
@@ -130,181 +116,6 @@ def replace_variables(win_id, arglist):
return args
-class CommandParser:
-
- """Parse qutebrowser commandline commands.
-
- Attributes:
- _partial_match: Whether to allow partial command matches.
- """
-
- def __init__(self, partial_match=False):
- self._partial_match = partial_match
-
- def _get_alias(self, text, default=None):
- """Get an alias from the config.
-
- Args:
- text: The text to parse.
- default : Default value to return when alias was not found.
-
- Return:
- The new command string if an alias was found. Default value
- otherwise.
- """
- parts = text.strip().split(maxsplit=1)
- aliases = config.cache['aliases']
- if parts[0] not in aliases:
- return default
- alias = aliases[parts[0]]
-
- try:
- new_cmd = '{} {}'.format(alias, parts[1])
- except IndexError:
- new_cmd = alias
- if text.endswith(' '):
- new_cmd += ' '
- return new_cmd
-
- def _parse_all_gen(self, text, *args, aliases=True, **kwargs):
- """Split a command on ;; and parse all parts.
-
- If the first command in the commandline is a non-split one, it only
- returns that.
-
- Args:
- text: Text to parse.
- aliases: Whether to handle aliases.
- *args/**kwargs: Passed to parse().
-
- Yields:
- ParseResult tuples.
- """
- text = text.strip().lstrip(':').strip()
- if not text:
- raise cmdexc.NoSuchCommandError("No command given")
-
- if aliases:
- text = self._get_alias(text, text)
-
- if ';;' in text:
- # Get the first command and check if it doesn't want to have ;;
- # split.
- first = text.split(';;')[0]
- result = self.parse(first, *args, **kwargs)
- if result.cmd.no_cmd_split:
- sub_texts = [text]
- else:
- sub_texts = [e.strip() for e in text.split(';;')]
- else:
- sub_texts = [text]
- for sub in sub_texts:
- yield self.parse(sub, *args, **kwargs)
-
- def parse_all(self, *args, **kwargs):
- """Wrapper over _parse_all_gen."""
- return list(self._parse_all_gen(*args, **kwargs))
-
- def parse(self, text, *, fallback=False, keep=False):
- """Split the commandline text into command and arguments.
-
- Args:
- text: Text to parse.
- fallback: Whether to do a fallback splitting when the command was
- unknown.
- keep: Whether to keep special chars and whitespace
-
- Return:
- A ParseResult tuple.
- """
- cmdstr, sep, argstr = text.partition(' ')
-
- if not cmdstr and not fallback:
- raise cmdexc.NoSuchCommandError("No command given")
-
- if self._partial_match:
- cmdstr = self._completion_match(cmdstr)
-
- try:
- cmd = objects.commands[cmdstr]
- except KeyError:
- if not fallback:
- raise cmdexc.NoSuchCommandError(
- '{}: no such command'.format(cmdstr))
- cmdline = split.split(text, keep=keep)
- return ParseResult(cmd=None, args=None, cmdline=cmdline)
-
- args = self._split_args(cmd, argstr, keep)
- if keep and args:
- cmdline = [cmdstr, sep + args[0]] + args[1:]
- elif keep:
- cmdline = [cmdstr, sep]
- else:
- cmdline = [cmdstr] + args[:]
-
- return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
-
- def _completion_match(self, cmdstr):
- """Replace cmdstr with a matching completion if there's only one match.
-
- Args:
- cmdstr: The string representing the entered command so far
-
- Return:
- cmdstr modified to the matching completion or unmodified
- """
- matches = [cmd for cmd in sorted(objects.commands, key=len)
- if cmdstr in cmd]
- if len(matches) == 1:
- cmdstr = matches[0]
- elif len(matches) > 1 and config.val.completion.use_best_match:
- cmdstr = matches[0]
- return cmdstr
-
- def _split_args(self, cmd, argstr, keep):
- """Split the arguments from an arg string.
-
- Args:
- cmd: The command we're currently handling.
- argstr: An argument string.
- keep: Whether to keep special chars and whitespace
-
- Return:
- A list containing the split strings.
- """
- if not argstr:
- return []
- elif cmd.maxsplit is None:
- return split.split(argstr, keep=keep)
- else:
- # If split=False, we still want to split the flags, but not
- # everything after that.
- # We first split the arg string and check the index of the first
- # non-flag args, then we re-split again properly.
- # example:
- #
- # input: "--foo -v bar baz"
- # first split: ['--foo', '-v', 'bar', 'baz']
- # 0 1 2 3
- # second split: ['--foo', '-v', 'bar baz']
- # (maxsplit=2)
- split_args = split.simple_split(argstr, keep=keep)
- flag_arg_count = 0
- for i, arg in enumerate(split_args):
- arg = arg.strip()
- if arg.startswith('-'):
- if arg in cmd.flags_with_args:
- flag_arg_count += 1
- else:
- maxsplit = i + cmd.maxsplit + flag_arg_count
- return split.simple_split(argstr, keep=keep,
- maxsplit=maxsplit)
-
- # If there are only flags, we got it right on the first try
- # already.
- return split_args
-
-
class AbstractCommandRunner(QObject):
"""Abstract base class for CommandRunner."""
@@ -329,7 +140,7 @@ class CommandRunner(AbstractCommandRunner):
def __init__(self, win_id, partial_match=False, parent=None):
super().__init__(parent)
- self._parser = CommandParser(partial_match=partial_match)
+ self._parser = parser.CommandParser(partial_match=partial_match)
self._win_id = win_id
@contextlib.contextmanager
@@ -362,7 +173,7 @@ class CommandRunner(AbstractCommandRunner):
parsed = self._parser.parse_all(text)
if parsed is None:
- return
+ return # type: ignore[unreachable]
for result in parsed:
with self._handle_error(safely):
diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py
index 52d4dc5f2..778333854 100644
--- a/qutebrowser/completion/completer.py
+++ b/qutebrowser/completion/completer.py
@@ -25,8 +25,8 @@ from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
-from qutebrowser.commands import runners
-from qutebrowser.misc import objects
+from qutebrowser.commands import parser, cmdexc
+from qutebrowser.misc import objects, split
from qutebrowser.utils import log, utils, debug, objreg
from qutebrowser.completion.models import miscmodels
if TYPE_CHECKING:
@@ -139,13 +139,18 @@ class Completer(QObject):
if not text or not text.strip():
# Only ":", empty part under the cursor with nothing before/after
return [], '', []
- parser = runners.CommandParser()
- result = parser.parse(text, fallback=True, keep=True)
- parts = [x for x in result.cmdline if x]
+
+ try:
+ parse_result = parser.CommandParser().parse(text, keep=True)
+ except cmdexc.NoSuchCommandError:
+ cmdline = split.split(text, keep=True)
+ else:
+ cmdline = parse_result.cmdline
+
+ parts = [x for x in cmdline if x]
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
- log.completion.debug('partitioning {} around position {}'.format(parts,
- pos))
+ log.completion.debug(f'partitioning {parts} around position {pos}')
for i, part in enumerate(parts):
pos -= len(part)
if pos <= 0:
@@ -156,11 +161,10 @@ class Completer(QObject):
center = parts[i].strip()
# strip trailing whitespace included as a separate token
postfix = [x.strip() for x in parts[i+1:] if not x.isspace()]
- log.completion.debug(
- "partitioned: {} '{}' {}".format(prefix, center, postfix))
+ log.completion.debug(f"partitioned: {prefix} '{center}' {postfix}")
return prefix, center, postfix
- raise utils.Unreachable("Not all parts consumed: {}".format(parts))
+ raise utils.Unreachable(f"Not all parts consumed: {parts}")
@pyqtSlot(str)
def on_selection_changed(self, text):
diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py
index a942b868a..736d09644 100644
--- a/qutebrowser/completion/models/configmodel.py
+++ b/qutebrowser/completion/models/configmodel.py
@@ -21,7 +21,7 @@
from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel, listcategory, util
-from qutebrowser.commands import runners, cmdexc
+from qutebrowser.commands import parser, cmdexc
from qutebrowser.keyinput import keyutils
@@ -117,9 +117,8 @@ def _bind_current_default(key, info):
cmd_text = info.keyconf.get_command(seq, 'normal')
if cmd_text:
- parser = runners.CommandParser()
try:
- cmd = parser.parse(cmd_text).cmd
+ cmd = parser.CommandParser().parse(cmd_text).cmd
except cmdexc.NoSuchCommandError:
data.append((cmd_text, '(Current) Invalid command!', key))
else:
@@ -127,8 +126,7 @@ def _bind_current_default(key, info):
cmd_text = info.keyconf.get_command(seq, 'normal', default=True)
if cmd_text:
- parser = runners.CommandParser()
- cmd = parser.parse(cmd_text).cmd
+ cmd = parser.CommandParser().parse(cmd_text).cmd
data.append((cmd_text, '(Default) {}'.format(cmd.desc), key))
return data
diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py
index 7b1eb47ea..8ab175012 100644
--- a/qutebrowser/components/caretcommands.py
+++ b/qutebrowser/components/caretcommands.py
@@ -183,7 +183,7 @@ def move_to_end_of_document(tab: apitypes.Tab) -> None:
tab.caret.move_to_end_of_document()
-@cmdutils.register(modes=[cmdutils.KeyMode.caret], deprecated_name='toggle-selection')
+@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def selection_toggle(tab: apitypes.Tab, line: bool = False) -> None:
"""Toggle caret selection mode.
@@ -194,14 +194,14 @@ def selection_toggle(tab: apitypes.Tab, line: bool = False) -> None:
tab.caret.toggle_selection(line)
-@cmdutils.register(modes=[cmdutils.KeyMode.caret], deprecated_name='drop-selection')
+@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def selection_drop(tab: apitypes.Tab) -> None:
"""Drop selection and keep selection mode enabled."""
tab.caret.drop_selection()
-@cmdutils.register(deprecated_name='follow-selected')
+@cmdutils.register()
@cmdutils.argument('tab_obj', value=cmdutils.Value.cur_tab)
def selection_follow(tab_obj: apitypes.Tab, *, tab: bool = False) -> None:
"""Follow the selected text.
@@ -215,7 +215,7 @@ def selection_follow(tab_obj: apitypes.Tab, *, tab: bool = False) -> None:
raise cmdutils.CommandError(str(e))
-@cmdutils.register(modes=[cmdutils.KeyMode.caret], deprecated_name='reverse-selection')
+@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def selection_reverse(tab: apitypes.Tab) -> None:
"""Swap the stationary and moving end of the current selection."""
diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py
index c644725b5..07d16ea92 100644
--- a/qutebrowser/config/config.py
+++ b/qutebrowser/config/config.py
@@ -27,6 +27,7 @@ from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping,
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
+from qutebrowser.commands import cmdexc, parser
from qutebrowser.config import configdata, configexc, configutils
from qutebrowser.utils import utils, log, urlmatch
from qutebrowser.misc import objects
@@ -162,13 +163,38 @@ class KeyConfig:
bindings[key] = binding
return bindings
+ def _implied_cmd(self, cmdline: str) -> Optional[str]:
+ """Return cmdline, or the implied cmd if cmdline is a set-cmd-text."""
+ try:
+ results = parser.CommandParser().parse_all(cmdline)
+ except cmdexc.NoSuchCommandError:
+ return None
+
+ result = results[0]
+ if result.cmd.name != "set-cmd-text":
+ return cmdline
+ *flags, cmd = result.args
+ if "-a" in flags or "--append" in flags or not cmd.startswith(":"):
+ return None # doesn't look like this sets a command
+ return cmd.lstrip(":")
+
def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings':
- """Get a dict of commands to a list of bindings for the mode."""
+ """Get a dict of commands to a list of bindings for the mode.
+
+ This is intented for user-facing display of keybindings.
+ As such, bindings for 'set-cmd-text [flags] :<cmd> ...' are translated
+ to '<cmd> ...', as from the user's perspective these keys behave like
+ bindings for '<cmd>' (that allow for further input before running).
+
+ See #5942.
+ """
cmd_to_keys: KeyConfig._ReverseBindings = {}
bindings = self.get_bindings_for(mode)
for seq, full_cmd in sorted(bindings.items()):
- for cmd in full_cmd.split(';;'):
- cmd = cmd.strip()
+ for cmdtext in full_cmd.split(';;'):
+ cmd = self._implied_cmd(cmdtext.strip())
+ if not cmd:
+ continue
cmd_to_keys.setdefault(cmd, [])
# Put bindings involving modifiers last
if any(info.modifiers for info in seq):
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 6b5687fc2..45d8d1a7c 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -621,14 +621,14 @@ content.headers.user_agent:
# Vim-protip: Place your cursor below this comment and run
# :r!python scripts/dev/ua_fetch.py
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
- Gecko) Chrome/87.0.4280.66 Safari/537.36"
- - Chrome 87 Linux
+ Gecko) Chrome/88.0.4324.96 Safari/537.36"
+ - Chrome 88 Linux
- - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
- like Gecko) Chrome/87.0.4280.66 Safari/537.36"
- - Chrome 87 Win10
+ like Gecko) Chrome/88.0.4324.104 Safari/537.36"
+ - Chrome 88 Win10
- - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
- (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36"
- - Chrome 87 macOS
+ (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36"
+ - Chrome 88 macOS
supports_pattern: true
desc: |
User agent to send.
@@ -1613,6 +1613,18 @@ input.spatial_navigation:
Right key, heuristics determine whether there is an element he might be
trying to reach towards the right and which element he probably wants.
+input.media_keys:
+ default: true
+ type: Bool
+ backend:
+ QtWebEngine: Qt 5.14
+ QtWebKit: false
+ restart: true
+ desc: >-
+ Whether the underlying Chromium should handle media keys.
+
+ On Linux, disabling this also disables Chromium's MPRIS integration.
+
## keyhint
keyhint.blacklist:
diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py
index 407ccb37e..d9564556a 100644
--- a/qutebrowser/config/qtargs.py
+++ b/qutebrowser/config/qtargs.py
@@ -157,6 +157,9 @@ def _qtwebengine_features(
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89740
disabled_features.append('InstalledApp')
+ if not config.val.input.media_keys:
+ disabled_features.append('HardwareMediaKeyHandling')
+
return (enabled_features, disabled_features)
diff --git a/qutebrowser/html/warning-sessions.html b/qutebrowser/html/warning-sessions.html
index d297a2656..422b409a9 100644
--- a/qutebrowser/html/warning-sessions.html
+++ b/qutebrowser/html/warning-sessions.html
@@ -9,7 +9,7 @@ qute://warning/sessions</span> to show it again at a later time.</span>
<p>Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.</p>
-<p>At the time of writing (January 2021), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a>. However, it unfortunately wasn't ready in time for qutebrowser v2.0.0, as it's a rather big refactoring. It's currently expected to be released as part of qutebrowser v2.1.0.</p>
+<p>At the time of writing (January 2021), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a>. However, it unfortunately wasn't ready in time for qutebrowser v2.0.0, as it's a rather big refactoring. It's currently expected to be released in a future v2.x.0 release.</p>
<p>As a stop-gap measure:</p>
diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py
index fc67a7387..fdef7c669 100644
--- a/qutebrowser/keyinput/macros.py
+++ b/qutebrowser/keyinput/macros.py
@@ -52,7 +52,7 @@ class MacroRecorder:
self._macro_count: Dict[int, int] = {}
self._last_register: Optional[str] = None
- @cmdutils.register(instance='macro-recorder', deprecated_name='record-macro')
+ @cmdutils.register(instance='macro-recorder')
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
def macro_record(self, win_id: int, register: str = None) -> None:
"""Start or stop recording a macro.
@@ -77,7 +77,7 @@ class MacroRecorder:
self._macros[register] = []
self._recording_macro = register
- @cmdutils.register(instance='macro-recorder', deprecated_name='run-macro')
+ @cmdutils.register(instance='macro-recorder')
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
@cmdutils.argument('count', value=cmdutils.Value.count)
def macro_run(self, win_id: int, count: int = 1, register: str = None) -> None:
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index c00120596..3c47fafe3 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -386,11 +386,7 @@ class ModeManager(QObject):
self.mode = mode
self.entered.emit(mode, self._win_id)
- @cmdutils.register(
- instance='mode-manager',
- scope='window',
- deprecated_name='enter-mode',
- )
+ @cmdutils.register(instance='mode-manager', scope='window')
def mode_enter(self, mode: str) -> None:
"""Enter a key mode.
@@ -443,12 +439,8 @@ class ModeManager(QObject):
self.enter(self._prev_mode,
reason='restore mode before {}'.format(mode.name))
- @cmdutils.register(
- instance='mode-manager',
- not_modes=[usertypes.KeyMode.normal],
- scope='window',
- deprecated_name='leave-mode',
- )
+ @cmdutils.register(instance='mode-manager',
+ not_modes=[usertypes.KeyMode.normal], scope='window')
def mode_leave(self) -> None:
"""Leave the mode we're currently in."""
if self.mode == usertypes.KeyMode.normal:
diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py
index 1a8b171c2..bd5d4e801 100644
--- a/qutebrowser/keyinput/modeparsers.py
+++ b/qutebrowser/keyinput/modeparsers.py
@@ -86,6 +86,8 @@ class NormalKeyParser(CommandKeyParser):
_partial_timer: Timer to clear partial keypresses.
"""
+ _sequence: keyutils.KeySequence
+
def __init__(self, *, win_id: int,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
@@ -154,6 +156,8 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
_last_press: The nature of the last keypress, a LastPress member.
"""
+ _sequence: keyutils.KeySequence
+
def __init__(self, *, win_id: int,
commandrunner: 'runners.CommandRunner',
hintmanager: hints.HintManager,
diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py
index 4751e1cea..001aa3047 100644
--- a/qutebrowser/misc/backendproblem.py
+++ b/qutebrowser/misc/backendproblem.py
@@ -194,14 +194,6 @@ class _BackendProblemChecker:
sys.exit(usertypes.Exit.err_init)
- def _nvidia_shader_workaround(self) -> None:
- """Work around QOpenGLShaderProgram issues.
-
- See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
- """
- self._assert_backend(usertypes.Backend.QtWebEngine)
- utils.libgl_workaround()
-
def _xwayland_options(self) -> Tuple[str, List[_Button]]:
"""Get buttons/text for a possible XWayland solution."""
buttons = []
@@ -435,7 +427,6 @@ class _BackendProblemChecker:
self._check_backend_modules()
if objects.backend == usertypes.Backend.QtWebEngine:
self._handle_ssl_support()
- self._nvidia_shader_workaround()
self._handle_wayland_webgl()
self._handle_cache_nuking()
self._handle_serviceworker_nuking()
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index 420f90f9a..ca8f9e8fe 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -185,6 +185,11 @@ def check_qt_version():
PYQT_VERSION_STR))
_die(text)
+ if qt_ver == QVersionNumber(5, 12, 0):
+ from qutebrowser.utils import log
+ log.init.warning("Running on Qt 5.12.0. Doing so is unsupported "
+ "(newer 5.12.x versions are fine).")
+
def check_ssl_support():
"""Check if SSL support is available."""
@@ -274,6 +279,21 @@ def check_optimize_flag():
"unexpected behavior may occur.")
+def webengine_early_import():
+ """If QtWebEngine is available, import it early.
+
+ We need to ensure that QtWebEngine is imported before a QApplication is created for
+ everything to work properly.
+
+ This needs to be done even when using the QtWebKit backend, to ensure that e.g.
+ error messages in backendproblem.py are accurate.
+ """
+ try:
+ from PyQt5 import QtWebEngineWidgets # pylint: disable=unused-import
+ except ImportError:
+ pass
+
+
def early_init(args):
"""Do all needed early initialization.
@@ -298,3 +318,4 @@ def early_init(args):
configure_pyqt()
check_ssl_support()
check_optimize_flag()
+ webengine_early_import()
diff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py
index 1da4709af..18cb9634c 100644
--- a/qutebrowser/misc/elf.py
+++ b/qutebrowser/misc/elf.py
@@ -69,7 +69,7 @@ from typing import IO, ClassVar, Dict, Optional, Tuple, cast
from PyQt5.QtCore import QLibraryInfo
-from qutebrowser.utils import log
+from qutebrowser.utils import log, version
class ParseError(Exception):
@@ -141,7 +141,7 @@ class Ident:
@classmethod
def parse(cls, fobj: IO[bytes]) -> 'Ident':
"""Parse an ELF ident header from a file."""
- magic, klass, data, version, osabi, abiversion = _unpack(cls._FORMAT, fobj)
+ magic, klass, data, elfversion, osabi, abiversion = _unpack(cls._FORMAT, fobj)
try:
bitness = Bitness(klass)
@@ -153,7 +153,7 @@ class Ident:
except ValueError:
raise ParseError(f"Invalid endianness {data}")
- return cls(magic, bitness, endianness, version, osabi, abiversion)
+ return cls(magic, bitness, endianness, elfversion, osabi, abiversion)
@dataclasses.dataclass
@@ -310,7 +310,11 @@ def _parse_from_file(f: IO[bytes]) -> Versions:
def parse_webenginecore() -> Optional[Versions]:
"""Parse the QtWebEngineCore library file."""
- library_path = pathlib.Path(QLibraryInfo.location(QLibraryInfo.LibrariesPath))
+ if version.is_flatpak():
+ # Flatpak has Qt in /usr/lib/x86_64-linux-gnu, but QtWebEngine in /app/lib.
+ library_path = pathlib.Path("/app/lib")
+ else:
+ library_path = pathlib.Path(QLibraryInfo.location(QLibraryInfo.LibrariesPath))
# PyQt bundles those files with a .5 suffix
lib_file = library_path / 'libQt5WebEngineCore.so.5'
diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py
index 79c84c346..95bfd64af 100644
--- a/qutebrowser/misc/guiprocess.py
+++ b/qutebrowser/misc/guiprocess.py
@@ -84,8 +84,27 @@ class GUIProcess(QObject):
if error == QProcess.Crashed and not utils.is_windows:
# Already handled via ExitStatus in _on_finished
return
- msg = self._proc.errorString()
- message.error("Error while spawning {}: {}".format(self._what, msg))
+
+ what = f"{self._what} {self.cmd!r}"
+ error_descriptions = {
+ QProcess.FailedToStart: f"{what.capitalize()} failed to start",
+ QProcess.Crashed: f"{what.capitalize()} crashed",
+ QProcess.Timedout: f"{what.capitalize()} timed out",
+ QProcess.WriteError: f"Write error for {what}",
+ QProcess.WriteError: f"Read error for {what}",
+ }
+ error_string = self._proc.errorString()
+ msg = ': '.join([error_descriptions[error], error_string])
+
+ # We can't get some kind of error code from Qt...
+ # https://bugreports.qt.io/browse/QTBUG-44769
+ # However, it looks like those strings aren't actually translated?
+ known_errors = ['No such file or directory', 'Permission denied']
+ if (': ' in error_string and # pragma: no branch
+ error_string.split(': ', maxsplit=1)[1] in known_errors):
+ msg += f'\n(Hint: Make sure {self.cmd!r} exists and is executable)'
+
+ message.error(msg)
@pyqtSlot(int, QProcess.ExitStatus)
def _on_finished(self, code, status):
diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py
index 7a3626f6e..68c0fd538 100644
--- a/qutebrowser/misc/sql.py
+++ b/qutebrowser/misc/sql.py
@@ -351,13 +351,13 @@ class SqlTable(QObject):
self._name = name
self._create_table(fields, constraints)
- def _create_table(self, fields, constraints):
+ def _create_table(self, fields, constraints, *, force=False):
"""Create the table if the database is uninitialized.
- If the table already exists, this does nothing, so it can e.g. be called on
- every user_version change.
+ If the table already exists, this does nothing (except with force=True), so it
+ can e.g. be called on every user_version change.
"""
- if not user_version_changed():
+ if not user_version_changed() and not force:
return
constraints = constraints or {}
diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py
index 91cbf0399..7bb632b57 100644
--- a/qutebrowser/utils/standarddir.py
+++ b/qutebrowser/utils/standarddir.py
@@ -30,7 +30,7 @@ from typing import Iterator, Optional
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
-from qutebrowser.utils import log, debug, utils
+from qutebrowser.utils import log, debug, utils, version
# The cached locations
_locations = {}
@@ -232,7 +232,16 @@ def _init_runtime(args: Optional[argparse.Namespace]) -> None:
# Unfortunately this path could get too long for sockets (which have a
# maximum length of 104 chars), so we don't add the username here...
- _create(path)
+ if version.is_flatpak():
+ # We need a path like /run/user/1000/app/org.qutebrowser.qutebrowser rather than
+ # /run/user/1000/qutebrowser on Flatpak, since that's bind-mounted in a way that
+ # it is accessible by any other qutebrowser instances.
+ *parts, app_name = os.path.split(path)
+ assert app_name == APPNAME, app_name
+ path = os.path.join(*parts, 'app', os.environ['FLATPAK_ID'])
+ else:
+ _create(path)
+
_locations[_Location.runtime] = path
@@ -314,6 +323,9 @@ def _create(path: str) -> None:
should not be changed.
"""
if APPNAME == 'qute_test' and path.startswith('/home'): # pragma: no cover
+ for k, v in os.environ.items():
+ if k == 'HOME' or k.startswith('XDG_'):
+ log.init.debug(f"{k} = {v}")
raise Exception("Trying to create directory inside /home during "
"tests, this should not happen.")
os.makedirs(path, 0o700, exist_ok=True)
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 03a3c7842..2a47d60aa 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -32,8 +32,6 @@ import functools
import contextlib
import shlex
import mimetypes
-import ctypes
-import ctypes.util
from typing import (Any, Callable, IO, Iterator,
Optional, Sequence, Tuple, Type, Union,
TypeVar, TYPE_CHECKING)
@@ -607,7 +605,7 @@ def open_file(filename: str, cmdline: str = None) -> None:
# if we want to use the default
override = config.val.downloads.open_dispatcher
- if version.is_sandboxed():
+ if version.is_flatpak():
if cmdline:
message.error("Cannot spawn download dispatcher from sandbox")
return
@@ -753,19 +751,6 @@ def ceil_log(number: int, base: int) -> int:
return result
-def libgl_workaround() -> None:
- """Work around QOpenGLShaderProgram issues, especially for Nvidia.
-
- See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
- """
- if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'):
- return
-
- libgl = ctypes.util.find_library("GL")
- if libgl is not None: # pragma: no branch
- ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
-
-
def parse_duration(duration: str) -> int:
"""Parse duration in format XhYmZs into milliseconds duration."""
if duration.isdigit():
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 46916c516..89da353fc 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -58,12 +58,6 @@ from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf
from qutebrowser.browser import pdfjs
from qutebrowser.config import config, websettings
-try:
- from qutebrowser.browser.webengine import webenginesettings
-except ImportError: # pragma: no cover
- webenginesettings = None # type: ignore[assignment]
-
-
_LOGO = r'''
______ ,,
,.-"` | ,-` |
@@ -189,8 +183,12 @@ def distribution() -> Optional[DistributionInfo]:
parsed=parsed, version=dist_version, pretty=pretty, id=dist_id)
-def is_sandboxed() -> bool:
- """Whether the environment has restricted access to the host system."""
+def is_flatpak() -> bool:
+ """Whether qutebrowser is running via Flatpak.
+
+ If packaged via Flatpak, the environment is has restricted access to the host
+ system.
+ """
current_distro = distribution()
if current_distro is None:
return False
@@ -525,6 +523,7 @@ class WebEngineVersions:
webengine: utils.VersionNumber
chromium: Optional[str]
source: str
+ chromium_major: Optional[int] = dataclasses.field(init=False)
_CHROMIUM_VERSIONS: ClassVar[Dict[utils.VersionNumber, str]] = {
# Qt 5.12: Chromium 69
@@ -562,11 +561,20 @@ class WebEngineVersions:
# 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.183 (2020-11-02)
+ # 5.15.3: Updated to 87.0.4280.144 (~2020-12-02)
+ # Security fixes up to 88.0.4324.150 (2021-02-04)
utils.VersionNumber(5, 15): '80.0.3987.163',
utils.VersionNumber(5, 15, 2): '83.0.4103.122',
utils.VersionNumber(5, 15, 3): '87.0.4280.144',
}
+ def __post_init__(self) -> None:
+ """Set the major Chromium version."""
+ if self.chromium is None:
+ self.chromium_major = None
+ else:
+ self.chromium_major = int(self.chromium.split('.')[0])
+
def __str__(self) -> str:
s = f'QtWebEngine {self.webengine}'
if self.chromium is not None:
@@ -674,7 +682,7 @@ def qtwebengine_versions(avoid_init: bool = False) -> WebEngineVersions:
- https://www.chromium.org/developers/calendar
- https://chromereleases.googleblog.com/
"""
- assert webenginesettings is not None
+ from qutebrowser.browser.webengine import webenginesettings
if webenginesettings.parsed_user_agent is None and not avoid_init:
webenginesettings.init_user_agent()
@@ -872,9 +880,6 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
"""
assert QApplication.instance()
- # Some setups can segfault in here if we don't do this.
- utils.libgl_workaround()
-
override = os.environ.get('QUTE_FAKE_OPENGL')
if override is not None:
log.init.debug("Using override {}".format(override))
diff --git a/requirements.txt b/requirements.txt
index 5572e206c..8a831c1c9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@
adblock==0.4.2 ; python_version!="3.10"
colorama==0.4.4
dataclasses==0.6 ; python_version<"3.7"
-importlib-metadata==3.7.2 ; python_version<"3.8"
+importlib-metadata==3.7.3 ; python_version<"3.8"
importlib-resources==5.1.2 ; python_version<"3.9"
Jinja2==2.11.3
MarkupSafe==1.1.1
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index 3a93b05e1..bae51e372 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -155,7 +155,7 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]:
"""Check commonly misspelled words."""
# Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
- 'occur[^rs .!]', 'seperator', 'explicitely', 'auxillary',
+ 'occur[^rs .!,]', 'seperator', 'explicitely', 'auxillary',
'accidentaly', 'ambigious', 'loosly', 'initialis', 'convienence',
'similiar', 'uncommited', 'reproducable', 'an user',
'convienience', 'wether', 'programatically', 'splitted',
@@ -249,7 +249,7 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]:
"use snake-case instead",
),
(
- re.compile(r'\.joinpath\('),
+ re.compile(r'\.joinpath\((?!\*)'),
"use the / operator for joining paths",
),
(
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index 1849a5218..ce50cd504 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -137,8 +137,10 @@ CHANGELOG_URLS = {
'cryptography': 'https://cryptography.io/en/latest/changelog.html',
'toml': 'https://github.com/uiri/toml/releases',
'PyQt5': 'https://www.riverbankcomputing.com/news',
+ 'PyQt5-Qt': 'https://www.riverbankcomputing.com/news',
'PyQt5-Qt5': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine': 'https://www.riverbankcomputing.com/news',
+ 'PyQtWebEngine-Qt': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine-Qt5': 'https://www.riverbankcomputing.com/news',
'PyQt-builder': 'https://www.riverbankcomputing.com/news',
'PyQt5-sip': 'https://www.riverbankcomputing.com/news',
@@ -279,7 +281,7 @@ def run_pip(venv_dir, *args, quiet=False, **kwargs):
arg_str = ' '.join(str(arg) for arg in args)
utils.print_col('venv$ pip {}'.format(arg_str), 'blue')
- venv_python = os.path.join(venv_dir, 'bin', 'python')
+ venv_python = get_venv_python(venv_dir)
return subprocess.run([venv_python, '-m', 'pip'] + args, check=True, **kwargs)
@@ -399,7 +401,13 @@ def _get_changes(diff):
for line in diff:
if not line.startswith('-') and not line.startswith('+'):
continue
- if line.startswith('+++ ') or line.startswith('--- '):
+ elif line.startswith('+++ ') or line.startswith('--- '):
+ continue
+ elif not line.strip():
+ # Could be newline changes on Windows
+ continue
+ elif line[1:].startswith('# This file is automatically'):
+ # Could be newline changes on Windows
continue
name, version = parse_versioned_line(line[1:])
@@ -458,6 +466,12 @@ def get_host_python(name):
return sys.executable
+def get_venv_python(venv_dir):
+ """Get the path to Python inside a virtualenv."""
+ subdir = 'Scripts' if os.name == 'nt' else 'bin'
+ return os.path.join(venv_dir, subdir, 'python')
+
+
def get_outfile(name):
"""Get the path to the output requirements.txt file."""
if name == 'qutebrowser':
@@ -510,7 +524,7 @@ def test_tox():
with tempfile.TemporaryDirectory() as tmpdir:
venv_dir = os.path.join(tmpdir, 'venv')
tox_workdir = os.path.join(tmpdir, 'tox-workdir')
- venv_python = os.path.join(venv_dir, 'bin', 'python')
+ venv_python = get_venv_python(venv_dir)
init_venv(host_python, venv_dir, req_path)
list_proc = subprocess.run([venv_python, '-m', 'tox', '--listenvs'],
check=True,
diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py
index 16a281d57..d0385bd17 100644
--- a/scripts/dev/run_pylint_on_tests.py
+++ b/scripts/dev/run_pylint_on_tests.py
@@ -58,6 +58,7 @@ def main():
'protected-access',
'len-as-condition',
'compare-to-empty-string',
+ 'pointless-statement',
# directories without __init__.py...
'import-error',
]
diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py
index 4f05f98ca..375868349 100755
--- a/scripts/dev/src2asciidoc.py
+++ b/scripts/dev/src2asciidoc.py
@@ -447,6 +447,7 @@ def _generate_setting_option(f, opt):
f.write("\nThis setting supports URL patterns.\n")
if opt.no_autoconfig:
f.write("\nThis setting can only be set in config.py.\n")
+ _generate_setting_backend_info(f, opt)
f.write("\n")
typ = opt.typ.get_name().replace(',', '&#44;')
f.write('Type: <<types,{typ}>>\n'.format(typ=typ))
@@ -465,7 +466,6 @@ def _generate_setting_option(f, opt):
f.write("\n")
f.write("Default: {}\n".format(opt.typ.to_doc(opt.default)))
- _generate_setting_backend_info(f, opt)
def generate_settings(filename):
diff --git a/scripts/dev/ua_fetch.py b/scripts/dev/ua_fetch.py
index a4ef889a0..7c0692cb4 100644
--- a/scripts/dev/ua_fetch.py
+++ b/scripts/dev/ua_fetch.py
@@ -50,6 +50,9 @@ for ua_string in reversed(response.json()):
continue
if any(part.startswith("OPR/") or part.startswith("Edg/") for part in parts):
continue
+ if 'Chrome/99.0.7113.93' in parts:
+ # Fake or false-positive entry
+ continue
user_agent = qutebrowser.config.websettings.UserAgent.parse(ua_string)
diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py
index 1f476113c..975787415 100644
--- a/scripts/dev/update_version.py
+++ b/scripts/dev/update_version.py
@@ -82,7 +82,7 @@ if __name__ == "__main__":
.format(v=version))
print("* Windows: git fetch; git checkout v{v}; "
"py -3.9 -m tox -e build-release -- --asciidoc "
- "$env:userprofile\\bin\\asciidoc-9.0.5\\asciidoc.py --upload"
+ "$env:userprofile\\bin\\asciidoc-9.1.0\\asciidoc.py --upload"
.format(v=version))
print("* macOS: git fetch && git checkout v{v} && "
"tox -e build-release -- --upload"
diff --git a/scripts/hist_importer.py b/scripts/hist_importer.py
index 6f2b9fa87..df12bcf2e 100755
--- a/scripts/hist_importer.py
+++ b/scripts/hist_importer.py
@@ -135,7 +135,8 @@ def insert_qb(history, dest):
'INSERT INTO History (url,title,atime,redirect) VALUES (?,?,?,?)',
history
)
- cursor.execute('DROP TABLE CompletionHistory')
+ cursor.execute('UPDATE CompletionMetaInfo SET value = 1 '
+ 'WHERE key = "force_rebuild"')
conn.commit()
conn.close()
diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py
index d510ba75b..a3b3bf32d 100755
--- a/scripts/mkvenv.py
+++ b/scripts/mkvenv.py
@@ -99,6 +99,18 @@ def parse_args(argv: List[str] = None) -> argparse.Namespace:
return parser.parse_args(argv)
+def _version_key(v):
+ """Sort PyQt requirement file prefixes.
+
+ If we have a filename like requirements-pyqt-pyinstaller.txt, that should
+ always be sorted after all others (hence we return a "999" key).
+ """
+ try:
+ return tuple(int(v) for c in v.split('.'))
+ except ValueError:
+ return 999
+
+
def pyqt_versions() -> List[str]:
"""Get a list of all available PyQt versions.
@@ -110,8 +122,7 @@ def pyqt_versions() -> List[str]:
for req in requirements_dir.glob('requirements-pyqt-*.txt'):
version_set.add(req.stem.split('-')[-1])
- versions = sorted(version_set,
- key=lambda v: [int(c) for c in v.split('.')])
+ versions = sorted(version_set, key=_version_key)
return versions + ['auto']
diff --git a/tests/conftest.py b/tests/conftest.py
index ee945ac4c..7b8cf2753 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -239,12 +239,6 @@ def set_backend(monkeypatch, request):
monkeypatch.setattr(objects, 'backend', backend)
-@pytest.fixture(autouse=True, scope='session')
-def apply_libgl_workaround():
- """Make sure we load libGL early so QtWebEngine tests run properly."""
- utils.libgl_workaround()
-
-
@pytest.fixture(autouse=True)
def apply_fake_os(monkeypatch, request):
fake_os = request.node.get_closest_marker('fake_os')
diff --git a/tests/end2end/data/darkmode/mathml.html b/tests/end2end/data/darkmode/mathml.html
new file mode 100644
index 000000000..fa2371638
--- /dev/null
+++ b/tests/end2end/data/darkmode/mathml.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>MathML-like SVG</title>
+ </head>
+ <body style="margin: 0; background-color: #ffff99">
+ <!--
+ Image based on: https://en.wikipedia.org/wiki/Pythagorean_theorem
+ with a black square added for testing.
+
+ onload based on:
+ https://stackoverflow.com/questions/53423742/waiting-for-an-image-to-finish-rendering
+ -->
+ <img
+ class="mwe-math-fallback-image-inline"
+ src="mathml.svg"
+ alt="Pythagorean theorem"
+ onload="requestAnimationFrame(() => requestAnimationFrame(() => console.log('Image loaded')));"
+ >
+ <!-- -->
+ </body>
+</html>
diff --git a/tests/end2end/data/darkmode/mathml.svg b/tests/end2end/data/darkmode/mathml.svg
new file mode 100644
index 000000000..30b03ffac
--- /dev/null
+++ b/tests/end2end/data/darkmode/mathml.svg
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="12.983ex"
+ height="2.843ex"
+ style="vertical-align: -0.505ex;"
+ viewBox="0 -1006.6 5589.7 1223.9"
+ role="img"
+ focusable="false"
+ aria-labelledby="MathJax-SVG-1-Title"
+ version="1.1"
+ id="svg36"
+ sodipodi:docname="mathml.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata40">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>{\displaystyle a^{2}+b^{2}=c^{2}.}</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="944"
+ inkscape:window-height="1036"
+ id="namedview38"
+ showgrid="false"
+ inkscape:zoom="0.27282322"
+ inkscape:cx="1686.0735"
+ inkscape:cy="602.78657"
+ inkscape:window-x="964"
+ inkscape:window-y="22"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg36" />
+ <title
+ id="MathJax-SVG-1-Title">{\displaystyle a^{2}+b^{2}=c^{2}.}</title>
+ <defs
+ aria-hidden="true"
+ id="defs10">
+ <path
+ stroke-width="1"
+ id="E1-MJMATHI-61"
+ d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" />
+ <path
+ stroke-width="1"
+ id="E1-MJMAIN-32"
+ d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" />
+ <path
+ stroke-width="1"
+ id="E1-MJMAIN-2B"
+ d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" />
+ <path
+ stroke-width="1"
+ id="E1-MJMATHI-62"
+ d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" />
+ <path
+ stroke-width="1"
+ id="E1-MJMAIN-3D"
+ d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" />
+ <path
+ stroke-width="1"
+ id="E1-MJMATHI-63"
+ d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" />
+ <path
+ stroke-width="1"
+ id="E1-MJMAIN-2E"
+ d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" />
+ </defs>
+ <g
+ stroke="currentColor"
+ fill="currentColor"
+ stroke-width="0"
+ transform="matrix(1 0 0 -1 0 0)"
+ aria-hidden="true"
+ id="g34">
+ <use
+ xlink:href="#E1-MJMATHI-61"
+ x="0"
+ y="0"
+ id="use12" />
+ <use
+ transform="scale(0.707)"
+ xlink:href="#E1-MJMAIN-32"
+ x="748"
+ y="583"
+ id="use14" />
+ <use
+ xlink:href="#E1-MJMAIN-2B"
+ x="1205"
+ y="0"
+ id="use16" />
+ <g
+ transform="translate(2206,0)"
+ id="g22">
+ <use
+ xlink:href="#E1-MJMATHI-62"
+ x="0"
+ y="0"
+ id="use18" />
+ <use
+ transform="scale(0.707)"
+ xlink:href="#E1-MJMAIN-32"
+ x="607"
+ y="583"
+ id="use20" />
+ </g>
+ <use
+ xlink:href="#E1-MJMAIN-3D"
+ x="3367"
+ y="0"
+ id="use24" />
+ <g
+ transform="translate(4423,0)"
+ id="g30">
+ <use
+ xlink:href="#E1-MJMATHI-63"
+ x="0"
+ y="0"
+ id="use26" />
+ <use
+ transform="scale(0.707)"
+ xlink:href="#E1-MJMAIN-32"
+ x="613"
+ y="583"
+ id="use28" />
+ </g>
+ <use
+ xlink:href="#E1-MJMAIN-2E"
+ x="5311"
+ y="0"
+ id="use32" />
+ </g>
+ <rect
+ style="fill:#000000"
+ id="rect865"
+ width="338.88928"
+ height="316.48901"
+ x="2.5373409"
+ y="-1004.8583" />
+</svg>
diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature
index 11b344439..1c360893c 100644
--- a/tests/end2end/features/spawn.feature
+++ b/tests/end2end/features/spawn.feature
@@ -8,7 +8,7 @@ Feature: :spawn
Scenario: Running :spawn with command that does not exist
When I run :spawn command_does_not_exist127623
- Then the error "Error while spawning command: *" should be shown
+ Then the error "Command 'command_does_not_exist127623' failed to start: *" should be shown
Scenario: Starting a userscript which doesn't exist
When I run :spawn -u this_does_not_exist
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index 9ef338768..90d7f9647 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -29,6 +29,7 @@ import logging
import tempfile
import contextlib
import itertools
+import collections
import json
import yaml
@@ -453,6 +454,7 @@ class QuteProc(testprocess.Process):
self.basedir = None
self._instance_id = next(instance_counter)
self._run_counter = itertools.count()
+ self._screenshot_counters = collections.defaultdict(itertools.count)
def _process_line(self, log_line):
"""Check if the line matches any initial lines we're interested in."""
@@ -902,9 +904,14 @@ class QuteProc(testprocess.Process):
"""
for _ in range(5):
tmp_path = self.request.getfixturevalue('tmp_path')
- path = tmp_path / 'screenshot.png'
- self.send_cmd(f':screenshot --force {path}')
- self.wait_for(message=f'Screenshot saved to {path}')
+ counter = self._screenshot_counters[self.request.node.nodeid]
+
+ path = tmp_path / f'screenshot-{next(counter)}.png'
+ self.send_cmd(f':screenshot {path}')
+
+ screenshot_msg = f'Screenshot saved to {path}'
+ self.wait_for(message=screenshot_msg)
+ print(screenshot_msg)
img = QImage(str(path))
assert not img.isNull()
@@ -919,8 +926,9 @@ class QuteProc(testprocess.Process):
# Rendering might not be completed yet...
time.sleep(0.5)
- raise ValueError(
- f"Pixel probing for {probe_color} failed (got {probed_color} on last try)")
+ # Using assert again for pytest introspection
+ assert probed_color == probe_color, "Color probing failed, values on last try:"
+ raise utils.Unreachable()
def press_keys(self, keys):
"""Press the given keys using :fake-key."""
diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py
index 81a864c8e..658ff0e56 100644
--- a/tests/end2end/fixtures/webserver.py
+++ b/tests/end2end/fixtures/webserver.py
@@ -62,7 +62,11 @@ class Request(testprocess.Line):
def _check_status(self):
"""Check if the http status is what we expected."""
path_to_statuses = {
- '/favicon.ico': [HTTPStatus.OK, HTTPStatus.PARTIAL_CONTENT],
+ '/favicon.ico': [
+ HTTPStatus.OK,
+ HTTPStatus.PARTIAL_CONTENT,
+ HTTPStatus.NOT_MODIFIED,
+ ],
'/does-not-exist': [HTTPStatus.NOT_FOUND],
'/does-not-exist-2': [HTTPStatus.NOT_FOUND],
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
index 38e40f9b7..82473e50d 100644
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -501,19 +501,21 @@ def test_preferred_colorscheme_with_dark_mode(
quteproc_new.open_path('data/darkmode/prefers-color-scheme.html')
content = quteproc_new.get_content()
- if webengine_versions.webengine == utils.VersionNumber(5, 15, 3):
+ qtwe_version = webengine_versions.webengine
+ xfail = None
+ if utils.VersionNumber(5, 15, 3) <= qtwe_version <= utils.VersionNumber(6):
# https://bugs.chromium.org/p/chromium/issues/detail?id=1177973
# No workaround known.
expected_text = 'Light preference detected.'
# light website color, inverted by darkmode
expected_color = testutils.Color(127, 127, 127)
- xfail = True
- elif webengine_versions.webengine == utils.VersionNumber(5, 15, 2):
+ xfail = "Chromium bug 1177973"
+ elif qtwe_version == utils.VersionNumber(5, 15, 2):
# Our workaround breaks when dark mode is enabled...
# Also, for some reason, dark mode doesn't work on that page either!
expected_text = 'No preference detected.'
expected_color = testutils.Color(0, 170, 0) # green
- xfail = True
+ xfail = "QTBUG-89753"
else:
# Qt 5.14 and 5.15.0/.1 work correctly.
# Hopefully, so does Qt 6.x in the future?
@@ -529,7 +531,7 @@ def test_preferred_colorscheme_with_dark_mode(
assert color == expected_color
if xfail:
# We still do some checks, but we want to mark the test outcome as xfail.
- pytest.xfail("QTBUG-89753")
+ pytest.xfail(xfail)
@pytest.mark.qtwebkit_skip
@@ -664,12 +666,37 @@ def test_dark_mode(webengine_versions, quteproc_new, request,
# Position chosen by fair dice roll.
# https://xkcd.com/221/
- pos = QPoint(4, 4)
- img = quteproc_new.get_screenshot(probe_pos=pos, probe_color=expected)
+ quteproc_new.get_screenshot(
+ probe_pos=QPoint(4, 4),
+ probe_color=expected,
+ )
- color = testutils.Color(img.pixelColor(pos))
- # For pytest debug output
- assert color == expected
+
+def test_dark_mode_mathml(quteproc_new, request, qtbot):
+ if not request.config.webengine:
+ pytest.skip("Skipped with QtWebKit")
+
+ args = _base_args(request.config) + [
+ '--temp-basedir',
+ '-s', 'colors.webpage.darkmode.enabled', 'true',
+ '-s', 'colors.webpage.darkmode.algorithm', 'brightness-rgb',
+ ]
+ quteproc_new.start(args)
+
+ quteproc_new.open_path('data/darkmode/mathml.html')
+ quteproc_new.wait_for_js('Image loaded')
+
+ # First make sure loading finished by looking outside of the image
+ quteproc_new.get_screenshot(
+ probe_pos=QPoint(105, 0),
+ probe_color=testutils.Color(0, 0, 204),
+ )
+
+ # Then get the actual formula color, probing again in case it's not displayed yet...
+ quteproc_new.get_screenshot(
+ probe_pos=QPoint(4, 4),
+ probe_color=testutils.Color(255, 255, 255),
+ )
def test_unavailable_backend(request, quteproc_new):
diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py
index 9b08de30d..1a46c5be0 100644
--- a/tests/unit/browser/test_history.py
+++ b/tests/unit/browser/test_history.py
@@ -488,12 +488,11 @@ class TestCompletionMetaInfo:
def test_contains_keyerror(self, metainfo):
with pytest.raises(KeyError):
- # pylint: disable=pointless-statement
'does_not_exist' in metainfo # noqa: B015
def test_getitem_keyerror(self, metainfo):
with pytest.raises(KeyError):
- metainfo['does_not_exist'] # pylint: disable=pointless-statement
+ metainfo['does_not_exist']
def test_setitem_keyerror(self, metainfo):
with pytest.raises(KeyError):
@@ -508,6 +507,28 @@ class TestCompletionMetaInfo:
metainfo['excluded_patterns'] = value
assert metainfo['excluded_patterns'] == value
+ # FIXME: It'd be good to test those two things via WebHistory (and not just
+ # CompletionMetaInfo in isolation), but we can't do that right now - see the
+ # docstring of TestRebuild for details.
+
+ def test_recovery_no_key(self, metainfo):
+ metainfo.delete('key', 'force_rebuild')
+
+ with pytest.raises(sql.BugError, match='No result for single-result query'):
+ metainfo['force_rebuild']
+
+ metainfo.try_recover()
+ assert not metainfo['force_rebuild']
+
+ def test_recovery_no_table(self, metainfo):
+ sql.Query("DROP TABLE CompletionMetaInfo").run()
+
+ with pytest.raises(sql.BugError, match='no such table: CompletionMetaInfo'):
+ metainfo['force_rebuild']
+
+ metainfo.try_recover()
+ assert not metainfo['force_rebuild']
+
class TestHistoryProgress:
diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py
index 33af45b6c..593896e96 100644
--- a/tests/unit/browser/webkit/test_webkitelem.py
+++ b/tests/unit/browser/webkit/test_webkitelem.py
@@ -303,7 +303,7 @@ class TestWebKitElement:
def test_getitem_keyerror(self, elem):
with pytest.raises(KeyError):
- elem['foo'] # pylint: disable=pointless-statement
+ elem['foo']
def test_setitem(self, elem):
elem['foo'] = 'bar'
diff --git a/tests/unit/commands/test_runners.py b/tests/unit/commands/test_parser.py
index ac9fee485..b851ad3b0 100644
--- a/tests/unit/commands/test_runners.py
+++ b/tests/unit/commands/test_parser.py
@@ -17,12 +17,12 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
-"""Tests for qutebrowser.commands.runners."""
+"""Tests for qutebrowser.commands.parser."""
import pytest
from qutebrowser.misc import objects
-from qutebrowser.commands import runners, cmdexc
+from qutebrowser.commands import parser, cmdexc
class TestCommandParser:
@@ -35,12 +35,12 @@ class TestCommandParser:
Args:
cmdline_test: A pytest fixture which provides testcases.
"""
- parser = runners.CommandParser()
+ p = parser.CommandParser()
if cmdline_test.valid:
- parser.parse_all(cmdline_test.cmd, aliases=False)
+ p.parse_all(cmdline_test.cmd, aliases=False)
else:
with pytest.raises(cmdexc.NoSuchCommandError):
- parser.parse_all(cmdline_test.cmd, aliases=False)
+ p.parse_all(cmdline_test.cmd, aliases=False)
def test_parse_all_with_alias(self, cmdline_test, monkeypatch,
config_stub):
@@ -49,12 +49,12 @@ class TestCommandParser:
config_stub.val.aliases = {'alias_name': cmdline_test.cmd}
- parser = runners.CommandParser()
+ p = parser.CommandParser()
if cmdline_test.valid:
- assert len(parser.parse_all("alias_name")) > 0
+ assert len(p.parse_all("alias_name")) > 0
else:
with pytest.raises(cmdexc.NoSuchCommandError):
- parser.parse_all("alias_name")
+ p.parse_all("alias_name")
@pytest.mark.parametrize('command', ['', ' '])
def test_parse_empty_with_alias(self, command):
@@ -63,9 +63,33 @@ class TestCommandParser:
See https://github.com/qutebrowser/qutebrowser/issues/1690
and https://github.com/qutebrowser/qutebrowser/issues/1773
"""
- parser = runners.CommandParser()
+ p = parser.CommandParser()
with pytest.raises(cmdexc.NoSuchCommandError):
- parser.parse_all(command)
+ p.parse_all(command)
+
+ @pytest.mark.parametrize('command, name, args', [
+ ("set-cmd-text -s :open", "set-cmd-text", ["-s", ":open"]),
+ ("set-cmd-text :open {url:pretty}", "set-cmd-text",
+ [":open {url:pretty}"]),
+ ("set-cmd-text -s :open -t", "set-cmd-text", ["-s", ":open -t"]),
+ ("set-cmd-text :open -t -r {url:pretty}", "set-cmd-text",
+ [":open -t -r {url:pretty}"]),
+ ("set-cmd-text -s :open -b", "set-cmd-text", ["-s", ":open -b"]),
+ ("set-cmd-text :open -b -r {url:pretty}", "set-cmd-text",
+ [":open -b -r {url:pretty}"]),
+ ("set-cmd-text -s :open -w", "set-cmd-text",
+ ["-s", ":open -w"]),
+ ("set-cmd-text :open -w {url:pretty}", "set-cmd-text",
+ [":open -w {url:pretty}"]),
+ ("set-cmd-text /", "set-cmd-text", ["/"]),
+ ("set-cmd-text ?", "set-cmd-text", ["?"]),
+ ("set-cmd-text :", "set-cmd-text", [":"]),
+ ])
+ def test_parse_result(self, config_stub, command, name, args):
+ p = parser.CommandParser()
+ result = p.parse_all(command)[0]
+ assert result.cmd.name == name
+ assert result.args == args
class TestCompletions:
@@ -86,8 +110,8 @@ class TestCompletions:
The same with it being disabled is tested by test_parse_all.
"""
- parser = runners.CommandParser(partial_match=True)
- result = parser.parse('on')
+ p = parser.CommandParser(partial_match=True)
+ result = p.parse('on')
assert result.cmd.name == 'one'
def test_dont_use_best_match(self, config_stub):
@@ -96,10 +120,10 @@ class TestCompletions:
Should raise NoSuchCommandError
"""
config_stub.val.completion.use_best_match = False
- parser = runners.CommandParser(partial_match=True)
+ p = parser.CommandParser(partial_match=True)
with pytest.raises(cmdexc.NoSuchCommandError):
- parser.parse('tw')
+ p.parse('tw')
def test_use_best_match(self, config_stub):
"""Test multiple completion options with use_best_match set to true.
@@ -107,7 +131,7 @@ class TestCompletions:
The resulting command should be the best match
"""
config_stub.val.completion.use_best_match = True
- parser = runners.CommandParser(partial_match=True)
+ p = parser.CommandParser(partial_match=True)
- result = parser.parse('tw')
+ result = p.parse('tw')
assert result.cmd.name == 'two'
diff --git a/tests/unit/commands/test_userscripts.py b/tests/unit/commands/test_userscripts.py
index 48bc31c32..436e9e2a7 100644
--- a/tests/unit/commands/test_userscripts.py
+++ b/tests/unit/commands/test_userscripts.py
@@ -18,6 +18,7 @@
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
import os
+import pathlib
import json
import time
import logging
@@ -34,8 +35,8 @@ from qutebrowser.utils import utils
class TestQtFIFOReader:
@pytest.fixture
- def reader(self, tmpdir, qapp):
- fifo_path = str(tmpdir / 'fifo')
+ def reader(self, tmp_path, qapp):
+ fifo_path = str(tmp_path / 'fifo')
os.mkfifo(fifo_path) # pylint: disable=no-member,useless-suppression
reader = userscripts._QtFIFOReader(fifo_path)
yield reader
@@ -142,8 +143,8 @@ def test_source(qtbot, py_proc, runner):
assert parsed['text'] == 'This is text'
assert parsed['html'] == 'This is HTML'
- assert not os.path.exists(parsed['text_file'])
- assert not os.path.exists(parsed['html_file'])
+ assert not pathlib.Path(parsed['text_file']).exists()
+ assert not pathlib.Path(parsed['html_file']).exists()
def test_command_with_error(qtbot, py_proc, runner, caplog):
@@ -165,13 +166,13 @@ def test_command_with_error(qtbot, py_proc, runner, caplog):
runner.store_html('')
data = json.loads(blocker.args[0])
- assert not os.path.exists(data)
+ assert not pathlib.Path(data).exists()
-def test_killed_command(qtbot, tmpdir, py_proc, runner, caplog):
- data_file = tmpdir / 'data'
+def test_killed_command(qtbot, tmp_path, py_proc, runner, caplog):
+ data_file = tmp_path / 'data'
watcher = QFileSystemWatcher()
- watcher.addPath(str(tmpdir))
+ watcher.addPath(str(tmp_path))
cmd, args = py_proc(r"""
import os
@@ -203,13 +204,14 @@ def test_killed_command(qtbot, tmpdir, py_proc, runner, caplog):
# Make sure the PID was written to the file, not just the file created
time.sleep(0.5)
- data = json.load(data_file)
+ with data_file.open() as f:
+ data = json.load(f)
with caplog.at_level(logging.ERROR):
with qtbot.wait_signal(runner.finished):
os.kill(int(data['pid']), signal.SIGTERM)
- assert not os.path.exists(data['text_file'])
+ assert not pathlib.Path(data['text_file']).exists()
def test_temporary_files_failed_cleanup(caplog, qtbot, py_proc, runner):
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index 22e9c6490..12e623517 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -21,6 +21,7 @@
import collections
import os
+import pathlib
import random
import string
import time
@@ -426,11 +427,11 @@ def test_filesystem_completion(qtmodeltester, config_stub, info,
homedir = str(local_files_path)
monkeypatch.setenv('HOME', homedir) # POSIX
monkeypatch.setenv('USERPROFILE', homedir) # Windows
- assert os.path.expanduser('~') == homedir
+ assert str(pathlib.Path.home()) == homedir
base = '~'
- expected_1 = os.path.join('~', 'file1.txt')
- expected_2 = os.path.join('~', 'file2.txt')
+ expected_1 = str(pathlib.Path('~') / 'file1.txt')
+ expected_2 = str(pathlib.Path('~') / 'file2.txt')
config_stub.val.completion.open_categories = ['filesystem']
model = urlmodel.url(info=info)
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
index 8a9d8154d..dd6ef54fa 100644
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -187,17 +187,39 @@ class TestKeyConfig:
@pytest.mark.parametrize('bindings, expected', [
# Simple
- ({'a': 'message-info foo', 'b': 'message-info bar'},
- {'message-info foo': ['a'], 'message-info bar': ['b']}),
+ ({'a': 'open foo', 'b': 'open bar'},
+ {'open foo': ['a'], 'open bar': ['b']}),
# Multiple bindings
- ({'a': 'message-info foo', 'b': 'message-info foo'},
- {'message-info foo': ['b', 'a']}),
+ ({'a': 'open foo', 'b': 'open foo'},
+ {'open foo': ['b', 'a']}),
# With modifier keys (should be listed last and normalized)
- ({'a': 'message-info foo', '<ctrl-a>': 'message-info foo'},
- {'message-info foo': ['a', '<Ctrl+a>']}),
+ ({'a': 'open foo', '<ctrl-a>': 'open foo'},
+ {'open foo': ['a', '<Ctrl+a>']}),
# Chained command
- ({'a': 'message-info foo ;; message-info bar'},
- {'message-info foo': ['a'], 'message-info bar': ['a']}),
+ ({'a': 'open foo ;; open bar'},
+ {'open foo': ['a'], 'open bar': ['a']}),
+ # Command using set-cmd-text (#5942)
+ (
+ {
+ "o": "set-cmd-text -s :open",
+ "O": "set-cmd-text -s :open -t",
+ "go": "set-cmd-text :open {url:pretty}",
+ # all of these should be ignored
+ "/": "set-cmd-text /",
+ "?": "set-cmd-text ?",
+ ":": "set-cmd-text :",
+ "a": "set-cmd-text no_leading_colon",
+ "b": "set-cmd-text -s -a :skip_cuz_append",
+ "c": "set-cmd-text --append :skip_cuz_append",
+ },
+ {
+ "open": ["o"],
+ "open -t": ["O"],
+ "open {url:pretty}": ["go"],
+ }
+ ),
+ # Empty/unknown commands
+ ({"a": "", "b": "notreal"}, {}),
])
def test_get_reverse_bindings_for(self, key_config_stub, config_stub,
no_bindings, bindings, expected):
@@ -725,7 +747,7 @@ class TestContainer:
def test_getattr_invalid_private(self, container):
"""Make sure an invalid _attribute doesn't try getting a container."""
with pytest.raises(AttributeError):
- container._foo # pylint: disable=pointless-statement
+ container._foo
def test_getattr_prefix(self, container):
new_container = container.tabs
@@ -744,7 +766,7 @@ class TestContainer:
def test_getattr_invalid(self, container):
with pytest.raises(configexc.NoOptionError) as excinfo:
- container.tabs.foobar # pylint: disable=pointless-statement
+ container.tabs.foobar
assert excinfo.value.option == 'tabs.foobar'
def test_setattr_option(self, config_stub, container):
@@ -754,7 +776,7 @@ class TestContainer:
def test_confapi_errors(self, container):
configapi = types.SimpleNamespace(errors=[])
container._configapi = configapi
- container.tabs.foobar # pylint: disable=pointless-statement
+ container.tabs.foobar
assert len(configapi.errors) == 1
error = configapi.errors[0]
diff --git a/tests/unit/config/test_configcache.py b/tests/unit/config/test_configcache.py
index 6bd841a65..87514bada 100644
--- a/tests/unit/config/test_configcache.py
+++ b/tests/unit/config/test_configcache.py
@@ -55,12 +55,10 @@ def test_configcache_get_after_set(config_stub):
def test_configcache_naive_benchmark(config_stub, benchmark):
def _run_bench():
for _i in range(10000):
- # pylint: disable=pointless-statement
config.cache['tabs.padding']
config.cache['tabs.indicator.width']
config.cache['tabs.indicator.padding']
config.cache['tabs.min_width']
config.cache['tabs.max_width']
config.cache['tabs.pinned.shrink']
- # pylint: enable=pointless-statement
benchmark(_run_bench)
diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py
index a15a6a334..72af2ad3e 100644
--- a/tests/unit/config/test_configcommands.py
+++ b/tests/unit/config/test_configcommands.py
@@ -480,7 +480,7 @@ class TestSource:
@pytest.mark.parametrize('location', ['default', 'absolute', 'relative'])
@pytest.mark.parametrize('clear', [True, False])
- def test_config_source(self, tmpdir, commands, config_stub, config_tmpdir,
+ def test_config_source(self, tmp_path, commands, config_stub, config_tmpdir,
location, clear):
assert config_stub.val.content.javascript.enabled
config_stub.val.search.ignore_case = 'always'
@@ -489,7 +489,7 @@ class TestSource:
pyfile = config_tmpdir / 'config.py'
arg = None
elif location == 'absolute':
- pyfile = tmpdir / 'sourced.py'
+ pyfile = tmp_path / 'sourced.py'
arg = str(pyfile)
elif location == 'relative':
pyfile = config_tmpdir / 'sourced.py'
@@ -607,8 +607,8 @@ class TestWritePy:
"""Tests for :config-write-py."""
- def test_custom(self, commands, config_stub, key_config_stub, tmpdir):
- confpy = tmpdir / 'config.py'
+ def test_custom(self, commands, config_stub, key_config_stub, tmp_path):
+ confpy = tmp_path / 'config.py'
config_stub.val.content.javascript.enabled = True
key_config_stub.bind(keyseq(',x'), 'message-info foo', mode='normal')
@@ -618,8 +618,8 @@ class TestWritePy:
assert "c.content.javascript.enabled = True" in lines
assert "config.bind(',x', 'message-info foo')" in lines
- def test_defaults(self, commands, tmpdir):
- confpy = tmpdir / 'config.py'
+ def test_defaults(self, commands, tmp_path):
+ confpy = tmp_path / 'config.py'
commands.config_write_py(str(confpy), defaults=True)
lines = confpy.read_text('utf-8').splitlines()
@@ -639,10 +639,10 @@ class TestWritePy:
assert '# Autogenerated config.py' in lines
@pytest.mark.posix
- def test_expanduser(self, commands, monkeypatch, tmpdir):
+ def test_expanduser(self, commands, monkeypatch, tmp_path):
"""Make sure that using a path with ~/... works correctly."""
- home = tmpdir / 'home'
- home.ensure(dir=True)
+ home = tmp_path / 'home'
+ home.mkdir()
monkeypatch.setenv('HOME', str(home))
commands.config_write_py('~/config.py')
@@ -651,9 +651,9 @@ class TestWritePy:
lines = confpy.read_text('utf-8').splitlines()
assert '# Autogenerated config.py' in lines
- def test_existing_file(self, commands, tmpdir):
- confpy = tmpdir / 'config.py'
- confpy.ensure()
+ def test_existing_file(self, commands, tmp_path):
+ confpy = tmp_path / 'config.py'
+ confpy.touch()
with pytest.raises(cmdutils.CommandError) as excinfo:
commands.config_write_py(str(confpy))
@@ -661,19 +661,19 @@ class TestWritePy:
expected = " already exists - use --force to overwrite!"
assert str(excinfo.value).endswith(expected)
- def test_existing_file_force(self, commands, tmpdir):
- confpy = tmpdir / 'config.py'
- confpy.ensure()
+ def test_existing_file_force(self, commands, tmp_path):
+ confpy = tmp_path / 'config.py'
+ confpy.touch()
commands.config_write_py(str(confpy), force=True)
lines = confpy.read_text('utf-8').splitlines()
assert '# Autogenerated config.py' in lines
- def test_oserror(self, commands, tmpdir):
+ def test_oserror(self, commands, tmp_path):
"""Test writing to a directory which does not exist."""
with pytest.raises(cmdutils.CommandError):
- commands.config_write_py(str(tmpdir / 'foo' / 'config.py'))
+ commands.config_write_py(str(tmp_path / 'foo' / 'config.py'))
def test_config_py_arg(self, commands, config_py_arg):
config_py_arg.ensure()
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
index 255ea8acc..4d70b7d25 100644
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -744,13 +744,13 @@ class ConfPy:
"""Helper class to get a confpy fixture."""
- def __init__(self, tmpdir, filename: str = "config.py"):
- self._file = tmpdir / filename
+ def __init__(self, tmp_path, filename: str = "config.py"):
+ self._file = tmp_path / filename
self.filename = str(self._file)
def write(self, *lines):
text = '\n'.join(lines)
- self._file.write_text(text, 'utf-8', ensure=True)
+ self._file.write_text(text, 'utf-8')
def read(self, error=False, warn_autoconfig=False):
"""Read the config.py via configfiles and check for errors."""
@@ -777,8 +777,8 @@ class ConfPy:
@pytest.fixture
-def confpy(tmpdir, config_tmpdir, data_tmpdir, config_stub, key_config_stub):
- return ConfPy(tmpdir)
+def confpy(tmp_path, config_tmpdir, data_tmpdir, config_stub, key_config_stub):
+ return ConfPy(tmp_path)
class TestConfigPyModules:
@@ -786,8 +786,8 @@ class TestConfigPyModules:
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
@pytest.fixture
- def qbmodulepy(self, tmpdir):
- return ConfPy(tmpdir, filename="qbmodule.py")
+ def qbmodulepy(self, tmp_path):
+ return ConfPy(tmp_path, filename="qbmodule.py")
@pytest.fixture(autouse=True)
def restore_sys_path(self):
@@ -795,7 +795,7 @@ class TestConfigPyModules:
yield
sys.path = old_path
- def test_bind_in_module(self, confpy, qbmodulepy, tmpdir):
+ def test_bind_in_module(self, confpy, qbmodulepy, tmp_path):
qbmodulepy.write(
'def run(config):',
' config.bind(",a", "message-info foo", mode="normal")')
@@ -804,9 +804,9 @@ class TestConfigPyModules:
expected = {'normal': {',a': 'message-info foo'}}
assert config.instance.get_obj('bindings.commands') == expected
assert "qbmodule" not in sys.modules.keys()
- assert tmpdir not in sys.path
+ assert tmp_path not in sys.path
- def test_restore_sys_on_err(self, confpy, qbmodulepy, tmpdir):
+ def test_restore_sys_on_err(self, confpy, qbmodulepy, tmp_path):
confpy.write_qbmodule()
qbmodulepy.write('def run(config):',
' 1/0')
@@ -815,9 +815,9 @@ class TestConfigPyModules:
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError)
assert "qbmodule" not in sys.modules.keys()
- assert tmpdir not in sys.path
+ assert tmp_path not in sys.path
- def test_fail_on_nonexistent_module(self, confpy, qbmodulepy, tmpdir):
+ def test_fail_on_nonexistent_module(self, confpy, qbmodulepy, tmp_path):
qbmodulepy.write('def run(config):',
' pass')
confpy.write('import foobar',
@@ -832,13 +832,13 @@ class TestConfigPyModules:
assert tblines[0] == "Traceback (most recent call last):"
assert tblines[-1].endswith("Error: No module named 'foobar'")
- def test_no_double_if_path_exists(self, confpy, qbmodulepy, tmpdir):
- sys.path.insert(0, tmpdir)
+ def test_no_double_if_path_exists(self, confpy, qbmodulepy, tmp_path):
+ sys.path.insert(0, tmp_path)
confpy.write('import sys',
'if sys.path[0] in sys.path[1:]:',
' raise Exception("Path not expected")')
confpy.read()
- assert sys.path.count(tmpdir) == 1
+ assert sys.path.count(tmp_path) == 1
class TestConfigPy:
@@ -1004,9 +1004,9 @@ class TestConfigPy:
confpy.read()
assert config.instance.get_obj(option)[-1] == value
- def test_oserror(self, tmpdir, data_tmpdir, config_tmpdir):
+ def test_oserror(self, tmp_path, data_tmpdir, config_tmpdir):
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
- configfiles.read_config_py(str(tmpdir / 'foo'))
+ configfiles.read_config_py(str(tmp_path / 'foo'))
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
@@ -1154,12 +1154,12 @@ class TestConfigPy:
assert error.traceback is not None
@pytest.mark.parametrize('location', ['abs', 'rel'])
- def test_source(self, tmpdir, confpy, location):
+ def test_source(self, tmp_path, confpy, location):
if location == 'abs':
- subfile = tmpdir / 'subfile.py'
+ subfile = tmp_path / 'subfile.py'
arg = str(subfile)
else:
- subfile = tmpdir / 'config' / 'subfile.py'
+ subfile = tmp_path / 'config' / 'subfile.py'
arg = 'subfile.py'
subfile.write_text("c.content.javascript.enabled = False",
@@ -1169,11 +1169,11 @@ class TestConfigPy:
assert not config.instance.get_obj('content.javascript.enabled')
- def test_source_configpy_arg(self, tmpdir, data_tmpdir, monkeypatch):
+ def test_source_configpy_arg(self, tmp_path, data_tmpdir, monkeypatch):
alt_filename = 'alt-config.py'
- alt_confpy_dir = tmpdir / 'alt-confpy-dir'
- alt_confpy_dir.ensure(dir=True)
+ alt_confpy_dir = tmp_path / 'alt-confpy-dir'
+ alt_confpy_dir.mkdir()
monkeypatch.setattr(standarddir, 'config_py',
lambda: str(alt_confpy_dir / alt_filename))
@@ -1187,8 +1187,8 @@ class TestConfigPy:
assert not config.instance.get_obj('content.javascript.enabled')
- def test_source_errors(self, tmpdir, confpy):
- subfile = tmpdir / 'config' / 'subfile.py'
+ def test_source_errors(self, tmp_path, confpy):
+ subfile = tmp_path / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')
confpy.write("config.source('subfile.py')")
error = confpy.read(error=True)
@@ -1196,8 +1196,8 @@ class TestConfigPy:
assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError)
- def test_source_multiple_errors(self, tmpdir, confpy):
- subfile = tmpdir / 'config' / 'subfile.py'
+ def test_source_multiple_errors(self, tmp_path, confpy):
+ subfile = tmp_path / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')
confpy.write("config.source('subfile.py')", "c.bar = 23")
@@ -1218,8 +1218,8 @@ class TestConfigPy:
assert isinstance(error.exception, FileNotFoundError)
@pytest.mark.parametrize('reverse', [True, False])
- def test_source_warn_autoconfig(self, tmpdir, confpy, reverse):
- subfile = tmpdir / 'config' / 'subfile.py'
+ def test_source_warn_autoconfig(self, tmp_path, confpy, reverse):
+ subfile = tmp_path / 'config' / 'subfile.py'
subfile.write_text("c.content.javascript.enabled = False",
encoding='utf-8')
lines = [
@@ -1383,8 +1383,8 @@ class TestConfigPyWriter:
expected = "config.set('opt', 'ask', 'https://www.example.com/')"
assert expected in text
- def test_write(self, tmpdir):
- pyfile = tmpdir / 'config.py'
+ def test_write(self, tmp_path):
+ pyfile = tmp_path / 'config.py'
writer = configfiles.ConfigPyWriter(options=[], bindings={},
commented=False)
writer.write(str(pyfile))
diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py
index 695649213..4c31c5b07 100644
--- a/tests/unit/config/test_qtargs.py
+++ b/tests/unit/config/test_qtargs.py
@@ -52,10 +52,14 @@ def version_patcher(monkeypatch):
@pytest.fixture
-def reduce_args(config_stub, version_patcher):
+def reduce_args(config_stub, version_patcher, monkeypatch):
"""Make sure no --disable-shared-workers/referer argument get added."""
- version_patcher('5.15.0')
+ version_patcher('5.15.3')
config_stub.val.content.headers.referer = 'always'
+ config_stub.val.scrolling.bar = 'never'
+ monkeypatch.setattr(qtargs.utils, 'is_mac', False)
+ # Avoid WebRTC pipewire feature
+ monkeypatch.setattr(qtargs.utils, 'is_linux', False)
@pytest.mark.usefixtures('reduce_args')
@@ -78,11 +82,6 @@ class TestQtArgs:
])
def test_qt_args(self, monkeypatch, config_stub, args, expected, parser):
"""Test commandline with no Qt arguments given."""
- # Avoid scrollbar overlay argument
- config_stub.val.scrolling.bar = 'never'
- # Avoid WebRTC pipewire feature
- monkeypatch.setattr(qtargs.utils, 'is_linux', False)
-
parsed = parser.parse_args(args)
assert qtargs.qt_args(parsed) == expected
@@ -112,7 +111,6 @@ def test_no_webengine_available(monkeypatch, config_stub, parser, stubs):
here.
"""
monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
- monkeypatch.setattr(qtargs.version, 'webenginesettings', None)
fake = stubs.ImportFake({'qutebrowser.browser.webengine': False}, monkeypatch)
fake.patch()
@@ -126,9 +124,10 @@ def test_no_webengine_available(monkeypatch, config_stub, parser, stubs):
class TestWebEngineArgs:
@pytest.fixture(autouse=True)
- def ensure_webengine(self):
+ def ensure_webengine(self, monkeypatch):
"""Skip all tests if QtWebEngine is unavailable."""
pytest.importorskip("PyQt5.QtWebEngine")
+ monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
@pytest.mark.parametrize('backend, qt_version, expected', [
(usertypes.Backend.QtWebEngine, '5.13.0', False),
@@ -184,7 +183,6 @@ class TestWebEngineArgs:
(['--debug-flag', 'wait-renderer-process'], ['--renderer-startup-dialog']),
])
def test_chromium_flags(self, monkeypatch, parser, flags, args):
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
parsed = parser.parse_args(flags)
args = qtargs.qt_args(parsed)
@@ -203,7 +201,6 @@ class TestWebEngineArgs:
('chromium', True),
])
def test_disable_gpu(self, config, added, config_stub, monkeypatch, parser):
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
config_stub.val.qt.force_software_rendering = config
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
@@ -225,7 +222,6 @@ class TestWebEngineArgs:
'disable_non_proxied_udp'),
])
def test_webrtc(self, config_stub, monkeypatch, parser, policy, arg):
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
config_stub.val.content.webrtc_ip_handling_policy = policy
parsed = parser.parse_args([])
@@ -241,10 +237,7 @@ class TestWebEngineArgs:
(True, False), # canvas reading enabled
(False, True),
])
- def test_canvas_reading(self, config_stub, monkeypatch, parser,
- canvas_reading, added):
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
-
+ def test_canvas_reading(self, config_stub, parser, canvas_reading, added):
config_stub.val.content.canvas_reading = canvas_reading
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
@@ -255,10 +248,7 @@ class TestWebEngineArgs:
('process-per-site', True),
('single-process', True),
])
- def test_process_model(self, config_stub, monkeypatch, parser,
- process_model, added):
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
-
+ def test_process_model(self, config_stub, parser, process_model, added):
config_stub.val.qt.process_model = process_model
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
@@ -276,10 +266,7 @@ class TestWebEngineArgs:
('always', '--enable-low-end-device-mode'),
('never', '--disable-low-end-device-mode'),
])
- def test_low_end_device_mode(self, config_stub, monkeypatch, parser,
- low_end_device_mode, arg):
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
-
+ def test_low_end_device_mode(self, config_stub, parser, low_end_device_mode, arg):
config_stub.val.qt.low_end_device_mode = low_end_device_mode
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
@@ -307,16 +294,10 @@ class TestWebEngineArgs:
('5.14.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'),
('5.15.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'),
])
- def test_referer(self, config_stub, monkeypatch, version_patcher, parser,
+ def test_referer(self, config_stub, version_patcher, parser,
qt_version, referer, arg):
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
version_patcher(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([])
args = qtargs.qt_args(parsed)
@@ -380,10 +361,7 @@ class TestWebEngineArgs:
])
def test_overlay_scrollbar(self, config_stub, monkeypatch, parser,
bar, is_mac, added):
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
monkeypatch.setattr(qtargs.utils, 'is_mac', is_mac)
- # Avoid WebRTC pipewire feature
- monkeypatch.setattr(qtargs.utils, 'is_linux', False)
config_stub.val.scrolling.bar = bar
@@ -392,15 +370,6 @@ class TestWebEngineArgs:
assert ('--enable-features=OverlayScrollbar' in args) == added
- @pytest.fixture
- def feature_flag_patch(self, monkeypatch, config_stub, version_patcher):
- """Patch away things affecting feature flags."""
- config_stub.val.scrolling.bar = 'never'
- version_patcher('5.15.3')
- monkeypatch.setattr(qtargs.utils, 'is_mac', False)
- # Avoid WebRTC pipewire feature
- monkeypatch.setattr(qtargs.utils, 'is_linux', False)
-
@pytest.mark.parametrize('via_commandline', [True, False])
@pytest.mark.parametrize('overlay, passed_features, expected_features', [
(True,
@@ -413,7 +382,7 @@ class TestWebEngineArgs:
'CustomFeature',
'CustomFeature'),
])
- def test_overlay_features_flag(self, config_stub, parser, feature_flag_patch,
+ def test_overlay_features_flag(self, config_stub, parser,
via_commandline, overlay, passed_features,
expected_features):
"""If enable-features is already specified, we should combine both."""
@@ -442,7 +411,7 @@ class TestWebEngineArgs:
['CustomFeature'],
['CustomFeature1', 'CustomFeature2'],
])
- def test_disable_features_passthrough(self, config_stub, parser, feature_flag_patch,
+ def test_disable_features_passthrough(self, config_stub, parser,
via_commandline, passed_features):
flag = qtargs._DISABLE_FEATURES + ','.join(passed_features)
@@ -458,7 +427,7 @@ class TestWebEngineArgs:
]
assert disable_features_args == [flag]
- def test_blink_settings_passthrough(self, parser, config_stub, feature_flag_patch):
+ def test_blink_settings_passthrough(self, parser, config_stub):
config_stub.val.colors.webpage.darkmode.enabled = True
flag = qtargs._BLINK_SETTINGS + 'foo=bar'
@@ -492,6 +461,16 @@ class TestWebEngineArgs:
expected = ['--disable-features=InstalledApp'] if has_workaround else []
assert disable_features_args == expected
+ @pytest.mark.parametrize('enabled', [True, False])
+ @testutils.qt514
+ def test_media_keys(self, config_stub, parser, enabled):
+ config_stub.val.input.media_keys = enabled
+
+ parsed = parser.parse_args([])
+ args = qtargs.qt_args(parsed)
+
+ assert ('--disable-features=HardwareMediaKeyHandling' in args) != enabled
+
@pytest.mark.parametrize('variant, expected', [
(
'qt_515_1',
@@ -518,7 +497,6 @@ class TestWebEngineArgs:
def test_dark_mode_settings(self, config_stub, monkeypatch, parser,
variant, expected):
from qutebrowser.browser.webengine import darkmode
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
monkeypatch.setattr(
darkmode, '_variant', lambda _versions: darkmode.Variant[variant])
@@ -531,16 +509,16 @@ class TestWebEngineArgs:
assert arg in args
@pytest.mark.linux
- def test_locale_workaround(self, config_stub, monkeypatch, version_patcher,
- parser):
+ def test_locale_workaround(self, config_stub, monkeypatch, version_patcher, parser):
class FakeLocale:
def bcp47Name(self):
return 'de-CH'
- monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
+ monkeypatch.setattr(qtargs.utils, 'is_linux', True) # patched in reduce_args
monkeypatch.setattr(qtargs, 'QLocale', FakeLocale)
version_patcher('5.15.3')
+
config_stub.val.qt.workarounds.locale = True
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
diff --git a/tests/unit/mainwindow/test_prompt.py b/tests/unit/mainwindow/test_prompt.py
index f8d4cdee6..668cd0710 100644
--- a/tests/unit/mainwindow/test_prompt.py
+++ b/tests/unit/mainwindow/test_prompt.py
@@ -53,26 +53,26 @@ class TestFileCompletion:
(2, 'next', 'a'),
(2, 'prev', 'b'),
])
- def test_simple_completion(self, tmpdir, get_prompt, steps, where,
+ def test_simple_completion(self, tmp_path, get_prompt, steps, where,
subfolder):
"""Simply trying to tab through items."""
- testdir = tmpdir / 'test'
+ testdir = tmp_path / 'test'
for directory in 'abc':
- (testdir / directory).ensure(dir=True)
+ (testdir / directory).mkdir(parents=True)
prompt = get_prompt(str(testdir) + os.sep)
for _ in range(steps):
prompt.item_focus(where)
- assert prompt._lineedit.text() == str(testdir / subfolder)
+ assert prompt._lineedit.text() == str((testdir / subfolder).resolve())
- def test_backspacing_path(self, qtbot, tmpdir, get_prompt):
+ def test_backspacing_path(self, qtbot, tmp_path, get_prompt):
"""When we start deleting a path we want to see the subdir."""
- testdir = tmpdir / 'test'
+ testdir = tmp_path / 'test'
for directory in ['bar', 'foo']:
- (testdir / directory).ensure(dir=True)
+ (testdir / directory).mkdir(parents=True)
prompt = get_prompt(str(testdir / 'foo') + os.sep)
diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py
index 9e1b3916c..18e926fab 100644
--- a/tests/unit/misc/test_guiprocess.py
+++ b/tests/unit/misc/test_guiprocess.py
@@ -226,7 +226,12 @@ def test_error(qtbot, proc, caplog, message_mock):
proc.start('this_does_not_exist_either', [])
msg = message_mock.getmsg(usertypes.MessageLevel.error)
- assert msg.text.startswith("Error while spawning testprocess:")
+ assert msg.text.startswith(
+ "Testprocess 'this_does_not_exist_either' failed to start:")
+
+ if not utils.is_windows:
+ assert msg.text.endswith(
+ "(Hint: Make sure 'this_does_not_exist_either' exists and is executable)")
def test_exit_unsuccessful(qtbot, proc, message_mock, py_proc, caplog):
diff --git a/tests/unit/misc/userscripts/test_qute_lastpass.py b/tests/unit/misc/userscripts/test_qute_lastpass.py
index 3846028ee..24272a048 100644
--- a/tests/unit/misc/userscripts/test_qute_lastpass.py
+++ b/tests/unit/misc/userscripts/test_qute_lastpass.py
@@ -79,6 +79,7 @@ def arguments_mock():
arguments.merge_candidates = False
arguments.password_only = False
arguments.username_only = False
+ arguments.no_tld_download = True
return arguments
diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py
index 035ce87a3..1a9107995 100644
--- a/tests/unit/utils/test_standarddir.py
+++ b/tests/unit/utils/test_standarddir.py
@@ -42,6 +42,14 @@ APPNAME = 'qute_test'
pytestmark = pytest.mark.usefixtures('qapp')
+@pytest.fixture
+def fake_home_envvar(monkeypatch, tmp_path):
+ """Fake a different HOME via environment variables."""
+ for k in ['XDG_DATA_HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME']:
+ monkeypatch.delenv(k, raising=False)
+ monkeypatch.setenv('HOME', str(tmp_path))
+
+
@pytest.fixture(autouse=True)
def clear_standarddir_cache_and_patch(qapp, monkeypatch):
"""Make sure the standarddir cache is cleared before/after each test.
@@ -79,10 +87,9 @@ def test_unset_organization_no_qapp(monkeypatch):
@pytest.mark.fake_os('mac')
@pytest.mark.posix
-def test_fake_mac_config(tmpdir, monkeypatch):
+def test_fake_mac_config(tmp_path, fake_home_envvar):
"""Test standardir.config on a fake Mac."""
- monkeypatch.setenv('HOME', str(tmpdir))
- expected = str(tmpdir) + '/.qute_test' # always with /
+ expected = str(tmp_path) + '/.qute_test' # always with /
standarddir._init_config(args=None)
assert standarddir.config() == expected
@@ -175,13 +182,10 @@ class TestStandardDir:
(standarddir.download, ['Downloads']),
])
@pytest.mark.linux
- def test_linux_normal(self, monkeypatch, tmpdir, func, subdirs):
+ def test_linux_normal(self, fake_home_envvar, tmp_path, func, subdirs):
"""Test dirs with XDG_*_HOME not set."""
- monkeypatch.setenv('HOME', str(tmpdir))
- for var in ['DATA', 'CONFIG', 'CACHE']:
- monkeypatch.delenv('XDG_{}_HOME'.format(var), raising=False)
standarddir._init_dirs()
- assert func() == str(tmpdir.join(*subdirs))
+ assert func() == str(tmp_path.joinpath(*subdirs))
@pytest.mark.linux
@pytest.mark.qt_log_ignore(r'^QStandardPaths: ')
@@ -198,6 +202,22 @@ class TestStandardDir:
standarddir._init_runtime(args=None)
assert standarddir.runtime() == str(tmpdir_env / APPNAME)
+ @pytest.mark.linux
+ def test_flatpak_runtimedir(self, monkeypatch, tmp_path):
+ runtime_path = tmp_path / 'runtime'
+ runtime_path.mkdir()
+ runtime_path.chmod(0o0700)
+
+ app_id = 'org.qutebrowser.qutebrowser'
+ expected = runtime_path / 'app' / app_id
+
+ monkeypatch.setattr(standarddir.version, 'is_flatpak', lambda: True)
+ monkeypatch.setenv('XDG_RUNTIME_DIR', str(runtime_path))
+ monkeypatch.setenv('FLATPAK_ID', app_id)
+
+ standarddir._init_runtime(args=None)
+ assert standarddir.runtime() == str(expected)
+
@pytest.mark.fake_os('windows')
def test_runtimedir_empty_tempdir(self, monkeypatch, tmpdir):
"""With an empty tempdir on non-Linux, we should raise."""
@@ -397,19 +417,17 @@ class TestSystemData:
@pytest.mark.parametrize('args_kind', ['basedir', 'normal', 'none'])
-def test_init(tmpdir, monkeypatch, args_kind):
+def test_init(tmp_path, args_kind, fake_home_envvar):
"""Do some sanity checks for standarddir.init().
Things like _init_cachedir_tag() are tested in more detail in other tests.
"""
assert standarddir._locations == {}
- monkeypatch.setenv('HOME', str(tmpdir))
-
if args_kind == 'normal':
args = types.SimpleNamespace(basedir=None)
elif args_kind == 'basedir':
- args = types.SimpleNamespace(basedir=str(tmpdir))
+ args = types.SimpleNamespace(basedir=str(tmp_path))
else:
assert args_kind == 'none'
args = None
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
index b43638cb3..2c726ddb6 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -892,13 +892,6 @@ def test_ceil_log_invalid(number, base):
utils.ceil_log(number, base)
-@pytest.mark.parametrize('skip', [True, False])
-def test_libgl_workaround(monkeypatch, skip):
- if skip:
- monkeypatch.setenv('QUTE_SKIP_LIBGL_WORKAROUND', '1')
- utils.libgl_workaround() # Just make sure it doesn't crash.
-
-
@pytest.mark.parametrize('duration, out', [
("0", 0),
("0s", 0),
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 879f84a1f..a53b4bdce 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -40,6 +40,11 @@ from qutebrowser.utils import version, usertypes, utils, standarddir
from qutebrowser.misc import pastebin, objects, elf
from qutebrowser.browser import pdfjs
+try:
+ from qutebrowser.browser.webengine import webenginesettings
+except ImportError:
+ webenginesettings = None
+
@pytest.mark.parametrize('os_release, expected', [
# No file
@@ -314,9 +319,9 @@ def test_distribution(tmpdir, monkeypatch, os_release, expected):
id='arch', parsed=version.Distribution.arch, version=None,
pretty='Arch Linux'), False)
])
-def test_is_sandboxed(monkeypatch, distribution, expected):
+def test_is_flatpak(monkeypatch, distribution, expected):
monkeypatch.setattr(version, "distribution", lambda: distribution)
- assert version.is_sandboxed() == expected
+ assert version.is_flatpak() == expected
class GitStrSubprocessFake:
@@ -926,6 +931,25 @@ class TestWebEngineVersions:
def test_str(self, version, expected):
assert str(version) == expected
+ @pytest.mark.parametrize('version, expected', [
+ (
+ version.WebEngineVersions(
+ webengine=utils.VersionNumber(5, 15, 2),
+ chromium=None,
+ source='test'),
+ None,
+ ),
+ (
+ version.WebEngineVersions(
+ webengine=utils.VersionNumber(5, 15, 2),
+ chromium='87.0.4280.144',
+ source='test'),
+ 87,
+ ),
+ ])
+ def test_chromium_major(self, version, expected):
+ assert version.chromium_major == expected
+
def test_from_ua(self):
ua = websettings.UserAgent(
os_info='X11; Linux x86_64',
@@ -985,7 +1009,6 @@ class TestWebEngineVersions:
versions = version.WebEngineVersions.from_pyqt(pyqt_webengine_version)
- from qutebrowser.browser.webengine import webenginesettings
webenginesettings.init_user_agent()
expected = webenginesettings.parsed_user_agent.upstream_browser_version
@@ -1026,26 +1049,24 @@ class TestChromiumVersion:
@pytest.fixture(autouse=True)
def clear_parsed_ua(self, monkeypatch):
pytest.importorskip('PyQt5.QtWebEngineWidgets')
- if version.webenginesettings is not None:
+ if webenginesettings is not None:
# Not available with QtWebKit
- monkeypatch.setattr(version.webenginesettings, 'parsed_user_agent', None)
+ monkeypatch.setattr(webenginesettings, 'parsed_user_agent', None)
def test_fake_ua(self, monkeypatch, caplog):
ver = '77.0.3865.98'
- version.webenginesettings._init_user_agent_str(
- _QTWE_USER_AGENT.format(ver))
+ webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format(ver))
assert version.qtwebengine_versions().chromium == ver
def test_prefers_saved_user_agent(self, monkeypatch):
- version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT)
+ webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87'))
class FakeProfile:
def defaultProfile(self):
raise AssertionError("Should not be called")
- monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile',
- FakeProfile())
+ monkeypatch.setattr(webenginesettings, 'QWebEngineProfile', FakeProfile())
version.qtwebengine_versions()
@@ -1231,7 +1252,6 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
if params.with_webkit:
patches['qWebKitVersion'] = lambda: 'WEBKIT VERSION'
patches['objects.backend'] = usertypes.Backend.QtWebKit
- patches['webenginesettings'] = None
substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)'
else:
monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False)
diff --git a/tox.ini b/tox.ini
index 5ccf486a7..e305e5c4d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -38,6 +38,16 @@ commands =
{envpython} -bb -m pytest {posargs:tests}
cov: {envpython} scripts/dev/check_coverage.py {posargs}
+[testenv:bleeding]
+basepython = {env:PYTHON:python3}
+setenv =
+ PYTEST_QT_API=pyqt5
+ QUTE_BDD_WEBENGINE=true
+pip_pre = true
+deps = -r{toxinidir}/misc/requirements/requirements-tests-bleeding.txt
+commands_pre = pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt5 PyQtWebEngine
+commands = {envpython} -bb -m pytest {posargs:tests}
+
# other envs
[testenv:misc]
@@ -150,7 +160,7 @@ passenv = APPDATA HOME PYINSTALLER_DEBUG
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-pyinstaller.txt
- -r{toxinidir}/misc/requirements/requirements-pyqt.txt
+ -r{toxinidir}/misc/requirements/requirements-pyqt-pyinstaller.txt
commands =
{envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec