Compare commits

...

209 Commits
v3.5.1 ... main

Author SHA1 Message Date
Florian Bruhin 7e3df43463 Disable child focus workaround on unaffected Qt versions
The workaround added for #7820 seems to cause datalist dropdowns to lose focus
on Wayland. Let's just disable the old workaround on Qt versions that are not affected
by the original issue, which seems to be Qt 6.6.3+.

Fixes #8831.
2026-01-04 22:25:49 +01:00
Florian Bruhin 9ae082b29b version: Add QtWebEngine 6.9.3 2026-01-04 22:25:49 +01:00
dependabot[bot] b417f2a23b build(deps): bump peter-evans/create-pull-request from 7 to 8
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7 to 8.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v7...v8)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 12:51:26 +01:00
dependabot[bot] 615cee7309 build(deps): bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 12:51:19 +01:00
dependabot[bot] 65c1ca9691 build(deps): bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 12:51:09 +01:00
qutebrowser bot 12bed611c5 Update dependencies 2025-12-15 17:05:29 +01:00
Florian Bruhin bc72687d7d Update changelog URLs 2025-12-08 16:55:52 +01:00
Florian Bruhin c32b7d4b60 Fix dependency update issues 2025-12-08 10:53:13 +01:00
qutebrowser bot 3f9ef123e7 Update dependencies 2025-12-08 04:29:09 +00:00
Florian Bruhin f2547f8a09 scripts: Make smoke test fail output less confusing 2025-11-30 18:46:03 +01:00
qutebrowser bot edd5114492 Release v3.6.3
(cherry picked from commit ee13dac738)
2025-11-30 17:37:53 +00:00
Florian Bruhin 184a242937 ci: Make sure git knows the main branch
Something seems to have changed about how sparse checkouts are done on GHA, so the main branch isn't found.
2025-11-30 18:35:05 +01:00
Florian Bruhin 17c19a09b7 build_release: Try to get more information on crashes
Currently CI crashes on macOS, but without any useful logs.
2025-11-30 18:11:25 +01:00
Florian Bruhin 88aa47c377 Add qt.workarounds.disable_accessibility setting
This disables accessibility with Qt 6.10.1, which
causes frequent segfaults.

Closes #8797
2025-11-30 17:54:33 +01:00
dependabot[bot] 0570545342 build(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-30 00:15:59 +01:00
qutebrowser bot f408f20ad9 Release v3.6.2
(cherry picked from commit b2c5d5fa0d)
2025-11-27 20:57:13 +00:00
Florian Bruhin 500a8df209 Update user agents 2025-11-27 21:55:44 +01:00
Florian Bruhin 13d9904b90 Update changelog for v3.6.2 2025-11-27 21:29:21 +01:00
Florian Bruhin b3e4dba731 qtargs: Remove old workaround with Qt 6.10.1 2025-11-27 21:28:34 +01:00
qutebrowser bot 4164205663 Update dependencies 2025-11-27 17:21:32 +01:00
qutebrowser bot f5e2660890 Update dependencies 2025-11-24 16:37:39 +01:00
Florian Bruhin 69f3882ce3 tests: Skip hangouts extension test on Qt 5 2025-11-23 11:42:49 +01:00
Florian Bruhin 8e42727d31 Update changelog 2025-11-22 10:40:30 +01:00
Florian Bruhin 31a5737c61 Avoid disabling off-the-record profile Hangouts extension with Qt 6.10.1
Otherwise this results in a crash, see #8785
2025-11-22 10:38:46 +01:00
Florian Bruhin 8ae5e3d83b version: Use correct profile for extension list
See #8785
2025-11-21 22:50:52 +01:00
Florian Bruhin 4f40a8b46b tests: Improve test_version output 2025-11-21 18:39:24 +01:00
Florian Bruhin 59a64af67f tests: Adjust permissions storage workaround for Qt 6.10.1 2025-11-21 18:35:03 +01:00
Florian Bruhin 66cbe0d9c9 Add QtWebEngine 6.10.1 security patch version 2025-11-21 18:31:43 +01:00
Florian Bruhin 0ef5053a65 tests: Stabilize flaky session scrolling test
Equivalent of d8079515fa
See #5390
2025-11-18 15:00:20 +01:00
qutebrowser bot 6ddff3ae0d Update dependencies 2025-11-18 14:08:13 +01:00
Florian Bruhin 9316d428ef ci: Drop Archlinux Qt 5 images/jobs
For now, Qt 5 is still tested via the Qt 5.15 PyPI wheels.

See https://github.com/qutebrowser/qutebrowser/issues/8417#issuecomment-3495979318
https://lists.archlinux.org/archives/list/arch-dev-public@lists.archlinux.org/thread/U45C4RAW4IXVLO376XGFNLEGGFFXCULV/
2025-11-11 09:13:30 +01:00
Florian Bruhin 71ed8cdbf5 Update changelog 2025-11-11 09:09:05 +01:00
Florian Bruhin 62fdb15532 Merge commit 'bc191b798' 2025-11-11 09:07:00 +01:00
Florian Bruhin bc191b798d wmname: Remove trivial functions 2025-11-11 09:06:49 +01:00
Florian Bruhin f8fbb0609f Update changelog 2025-11-11 08:46:36 +01:00
Florian Bruhin 55fb26fce1 Revert "fix: change fullscreen state when switching tab"
This reverts commit b89bf07d1e.

This turned out to be annoying when enabling fullscreen manually and then
switching through tabs.
2025-11-11 08:45:55 +01:00
Jan Palus 25dc019886
Unify librarry loading for X11/Wayland wmname
libwayland-client.so is development symlink used during linking and there's no need to
have it installed (usually shipped in -devel/-dev packages) on user's machines. Instead
of hardcoding library file name, use same mechanism as in libX11 which let's Python
figure the details and share common logic between X11 and Wayland.

Fixes #8771
2025-11-11 00:51:05 +01:00
Florian Bruhin 81d7b6a74c tests: Use star-unpacking instead of itertools.chain
pytest will soon deprecate using a non-collection iterable in parametrize:
https://docs.pytest.org/en/latest/deprecations.html#parametrize-iterators
2025-11-10 13:02:26 +01:00
qutebrowser bot 6ec5504ab3 Update dependencies 2025-11-10 07:39:05 +01:00
Florian Bruhin 9b69c889ef doc: Add additional chrome:// pages 2025-11-07 11:45:51 +01:00
Florian Bruhin 4e87ef303f ci: Update macOS runners / versions
https://github.blog/changelog/2025-09-19-github-actions-macos-13-runner-image-is-closing-down/

- CI: Switch to macOS 15 Intel runner
  (macOS 14 is still tested with Apple Silicon)
- Nightly: Use macOS 15 Intel runner for nightly releases
  (macOS 14 would be better to align with actual Intel releases, but it is
  a -large runner, thus possibly metered)
- Releases: Use macOS 14 for Intel releases
  This is a -large runner, but releases don't happen often.
2025-11-05 16:51:43 +01:00
gesh 2f8234ee2e doc: Correct Arch Linux links
Arch hasn't been using the [community] repository for 9 months now[1],
correct the links for that.
Also, youtube-dl has been replaced in [extra] by yt-dlp[2][3], unsure
when -- I think this was in 2023?
Finally (and the trigger for this commit), given #8332, correct the
guidance on Arch Linux to point to pdfjs-legacy instead of pdfjs.

[1]: https://archlinux.org/news/cleaning-up-old-repositories/
[2]: https://aur.archlinux.org/packages/youtube-dl
[3]: https://archlinux.org/packages/extra/any/yt-dlp/
2025-11-05 14:26:04 +01:00
Florian Bruhin a8f0b47451 tests: Ignore more bogus Chromium messages 2025-11-03 18:32:17 +01:00
Florian Bruhin 4a5b7bd6e4 Update changelog 2025-11-03 18:10:24 +01:00
OmeletWithoutEgg b89bf07d1e fix: change fullscreen state when switching tab 2025-11-03 18:09:28 +01:00
Rebecca 70bf4689fc Fixed whitespace
Removed excess line of whitespace.
2025-11-03 18:09:01 +01:00
Rebecca 1cbb6fccf0 Fixed minor issue in configuration docs
The docs show an example for adding domain filtering for configuration options. However the example only matches the root of a domain rather than all pages on a domain which is for example, the default case when using the `tsh` shortcut to disable/enable javascript on a page.
2025-11-03 18:09:01 +01:00
Rebecca d3e4245d0f Improved logging for configuration loading
- When loading a configuration file, if the configuration uses domain filtering then the domain filter expression is logged in the debug log.
2025-11-03 18:09:01 +01:00
qutebrowser bot 3160048619 Release v3.6.1
(cherry picked from commit 2e5f805cce)
2025-11-03 15:30:02 +00:00
Florian Bruhin aa93eb1614 Adjust stack trace parsing for newer Python 2025-11-03 16:26:12 +01:00
qutebrowser bot df9cef3a58 Update dependencies 2025-11-03 06:33:33 +01:00
dependabot[bot] 214e2e9ac2 build(deps): bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 20:41:53 +01:00
qutebrowser bot 68574b88cc Update dependencies 2025-10-27 08:21:55 +01:00
Florian Bruhin 6e8e24050d Fix changelog 2025-10-26 23:02:14 +01:00
dependabot[bot] 5fe9bf97e3 build(deps): bump actions/setup-node from 5 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-25 22:36:25 +02:00
Florian Bruhin b646d606d7 Fix releasing focus when leaving command mode
The fix for #8223 in 6f21accfae
was misguided: We don't really care about the statusbar being hidden,
controlled release of keyboard focus needs to happen in any case where
we're hiding the command widget (as that's when we lose keyboard focus).

Fixes #8750.
2025-10-25 16:43:06 +02:00
Florian Bruhin 1e4ddc2c6b ci: Fix finding existing draft release 2025-10-24 17:02:41 +02:00
Florian Bruhin 3fce0518bd scripts: Avoid showing entire file tree diff in CI log 2025-10-24 17:00:19 +02:00
Florian Bruhin 3808ebfdb3 ci: Find existing draft release for reuploads 2025-10-24 16:23:03 +02:00
Florian Bruhin 0421aacd64 ci: Check out release branch for reuploads 2025-10-24 15:45:15 +02:00
Florian Bruhin 0c2a673e27 scripts: Clean up build_release 2025-10-24 15:06:20 +02:00
Florian Bruhin afa456f396 ci: Skip existing artifacts when reuploading
After a release failed and we want to retry,
some assets might already have been uploaded.

Skip those instead of attempting to reupload.
2025-10-24 14:53:40 +02:00
Florian Bruhin 461077b6cf ci: Fix reupload in script 2025-10-24 14:34:20 +02:00
Florian Bruhin e208f5e121 ci: Add ability to reupload after borked release 2025-10-24 14:33:24 +02:00
Florian Bruhin 0f320051e0 scripts: Ignore another line during smoke tests 2025-10-24 14:17:29 +02:00
Florian Bruhin ee40f13a30 Update release checklist
minor releases happen far more often
2025-10-24 14:17:29 +02:00
qutebrowser bot 06c4fac54a Release v3.6.0 2025-10-24 12:11:30 +00:00
Florian Bruhin 1392d55f53 Update changelog 2025-10-24 13:53:21 +02:00
Florian Bruhin 269d81371d Update user agents 2025-10-24 13:50:15 +02:00
qutebrowser bot 346a59a6f0 Update dependencies 2025-10-24 12:28:48 +02:00
Florian Bruhin d8079515fa tests: Stabilize flaky scrolling test
See #5390
2025-10-24 12:26:00 +02:00
Florian Bruhin 294534cf66 tests: Mark found lines more clearly 2025-10-24 12:25:45 +02:00
Florian Bruhin da9bd515fd scripts: Update build-release ignores for Qt 6.10 2025-10-24 12:05:35 +02:00
Florian Bruhin 5ac0cb97a8 Fix Qt 5 2025-10-24 00:01:28 +02:00
Florian Bruhin 242cf2a22e version: Add basic info about loaded WebExtensions 2025-10-23 23:24:40 +02:00
Florian Bruhin 6f9cf43d6d Fix mypy 2025-10-22 22:03:33 +02:00
Florian Bruhin 00b3bba99a Qt 6.10: Disable Hangouts extension via API
See https://github.com/qutebrowser/qutebrowser/issues/8694#issuecomment-3276414407
2025-10-22 21:45:48 +02:00
Florian Bruhin 111178358a tests: Add new ignores for Qt 6.10 + GHA + Windows
See #8694
2025-10-22 17:36:45 +02:00
Florian Bruhin e098609a8a Add missing PyQt 6.10 requirement files 2025-10-22 17:01:19 +02:00
Florian Bruhin 721ba1e036 Add PyQt 6.10 release
Needs --extra-index-url due to PyQt6-WebEngine-Qt6 missing because of PyPI size limits.
2025-10-22 16:03:46 +02:00
Florian Bruhin c5e7a73ab7 tests: Set pythonpath in pytest.ini
Makes pytest work without PYTHONPATH=. if running outside tox.
2025-10-22 16:03:46 +02:00
qutebrowser bot 67db09a9c9 Update dependencies 2025-10-20 12:16:34 +02:00
dependabot[bot] 4925715dfc build(deps): bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 20:30:12 +02:00
Florian Bruhin 6f8576b343 Update changelog 2025-10-13 17:59:44 +02:00
qutebrowser bot 7bffed47ed Update dependencies 2025-10-13 18:00:02 +02:00
Florian Bruhin 642c5fe2fe ci: Fix python versions, take 2 2025-10-12 15:03:49 +02:00
Florian Bruhin 621f20adb3 configcommands: Fix :config-list-remove with invalid value 2025-10-12 14:53:45 +02:00
Florian Bruhin 9d80bc2076 Fix Dict configtype hypothesis test
If dict keys are too long for YAML, .from_str()
will fail with a YAML parse error.

Closes #8530
2025-10-12 14:43:03 +02:00
Florian Bruhin 88415c6aa0 ci: Adjust Python versions
Run Python 3.14 with PyQt 6.9 instead of 6.8, and remove -dev
2025-10-12 13:39:57 +02:00
Florian Bruhin 5c2076ab2d tests: Add Qt.Key.Key_Keyboard
New in Qt 6.10:
https://codereview.qt-project.org/c/qt/qtbase/+/605015
2025-10-12 13:32:01 +02:00
Florian Bruhin 7b2773814b Update changelog 2025-10-12 13:17:48 +02:00
Florian Bruhin 467b9a7d4a Add site-specific quirk for gitlab.gnome.org
Fixes #8509
2025-10-12 13:17:43 +02:00
Florian Bruhin 816b8aaa00 Adjust comment for workaround 2025-10-12 01:16:30 +02:00
Florian Bruhin d38b5a423e Update changelog 2025-10-11 21:17:03 +02:00
Florian Bruhin 7f45b178c9 Adjust imports 2025-10-11 20:49:52 +02:00
Florian Bruhin 26368f0b90 Work around per-domain UA header not working on redirects
See https://bugreports.qt.io/browse/QTBUG-140515
Fixes #8679
2025-10-11 20:49:19 +02:00
Teddy ff76871f4e replacing os.path with pathlib 2025-10-11 17:14:45 +02:00
Florian Bruhin 9ddbc964d4 Fix hardware acceleration on Wayland with Qt <= 6.9
Equivalent to https://codereview.qt-project.org/c/qt/qtwebengine/+/663568

    Specify native platform for ANGLE's EGL backend on Wayland

    Set EGL_PLATFORM=wayland to force ANGLE to obtain EGL display connection
    for wayland platform. Otherwise, the display connection for
    EGL_DEFAULT_DISPLAY may belong to a platform which Nvidia's EGL driver
    doesn't support. In case of unsupported platform, EGL may fallback to
    Mesa software renderer (LLVMPipe) disabling hardware acceleration in
    Chromium.

Fixes #8637
2025-10-10 23:33:28 +02:00
Florian Bruhin 2b4e5757b0 Reenable accelerated 3D canvas on QtWebEngine 6.8.2+
Qt 6.8.2 has a more fine-grained workaround:
https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/606122

This never seems to have it made to Qt 6.9+, but I can't seem to reproduce the
issue anymore (neither with PDF.js nor with Google Sheets), even on older
affected Qt versions, even on older Intel hardware. Maybe something else (mesa
etc.?) changed and this was fixed there?

Let's reenable this and find out if it breaks things again for someone.

Fixes #8346
See #7489, #8001, #8006
2025-10-10 23:07:09 +02:00
Florian Bruhin ddfd17d749 Fix focus issues with fullscreen notification
This is conceptually similar to the release_focus
signal added in 6aa19eb90f and
6f21accfae, but without having to thread the
signal through to TabbedBrowser and back.

Weirdly, doing self.setFocusPolicy(Qt.FocusPolicy.NoFocus) in
FullscreenNotification.__init__ did not help, so let's just set the focus
manually instead.

Fixes #8174.
Fixes #8625.
2025-10-10 20:46:48 +02:00
Florian Bruhin 6f21accfae Fix focus handling when closing hidden statusbar
From knezi's analysis in #8722:

    The problem was that in Qt, slots are called in the order of connection, so
    even though there's a code that tries to set up the focus correctly, it's
    run after the cmd widget is hidden and hence the focus is already moved and
    it doesn't work as expected.

Follow-up for #2236/#8024.
Fixes #8223.
Supersedes and closes #8722.
Also see #8625 and #8174 (which are not fixed by this).
2025-10-10 20:15:12 +02:00
Florian Bruhin 6b48ae4084 Update security patch version for Qt 6.10 release 2025-10-10 19:03:14 +02:00
Florian Bruhin e300327eea Update certifi changelog URL 2025-10-10 19:03:14 +02:00
mathis renaud 9d1dfcfe5c Make 'tox -e mypy' work again in tox.ini
Fix #8344
2025-10-10 19:03:16 +02:00
Florian Bruhin 897fc40f4d
Merge pull request #8725 from qutebrowser/update-dependencies
Update dependencies
2025-10-06 12:58:14 +02:00
qutebrowser bot 24aab77607 Update dependencies 2025-10-06 04:22:56 +00:00
Florian Bruhin c37b9ebf5b tests: Adjust for changed exception
Follow-up to 2ae3086244
2025-10-02 09:28:55 +02:00
Florian Bruhin 2ae3086244 qutescheme: Improve handling of invalid path -> host redirects 2025-10-02 09:07:15 +02:00
Florian Bruhin 4aa807032f qutescheme: Simplify URL/path normalization logic
See #8711
2025-10-02 09:04:17 +02:00
Florian Bruhin 0867a95abb qutescheme: Add tests for redirects 2025-10-02 08:59:23 +02:00
Florian Bruhin 25ff649efe Fix lint 2025-10-01 17:14:43 +02:00
Florian Bruhin f57afa39e8 webenginetab: Improve _on_navigation_request workarounds
- Use QTimer.singleShot to avoid crashes with QtWebEngine 6.10:
  https://bugreports.qt.io/browse/QTBUG-140543
- Disable back/forward workaround for QtWebEngine >= 6.6,
  as the underlying issue seems to be fixed there.

See https://github.com/qutebrowser/qutebrowser/issues/8694#issuecomment-3276397840
and #8711.
2025-10-01 17:09:01 +02:00
Florian Bruhin da8428f9ba webenginetab: Move QtWebEngine specific workaround
See #8711
2025-10-01 16:31:30 +02:00
Florian Bruhin 75475ee87b version: Adjust security patch version for Qt 6.10 RC 2025-09-30 09:02:07 +02:00
Florian Bruhin 8ebce896ad
Merge pull request #8717 from qutebrowser/update-dependencies
Update dependencies
2025-09-30 09:02:27 +02:00
qutebrowser bot 43b5618c0a Update dependencies 2025-09-29 04:23:12 +00:00
Florian Bruhin c9498995df
Merge pull request #8709 from andyflesner/patch-1
Update Python performance reference link in FAQ
2025-09-24 09:12:28 +02:00
Florian Bruhin aaba038a16
Merge pull request #8708 from qutebrowser/update-dependencies
Update dependencies
2025-09-24 08:00:56 +02:00
Florian Bruhin 92f3633662 tests: Skip nested prompts test on unaffected Qt versions
Qt < 6.9 doesn't crash in this situation, and seems to handle nested prompts
differently, causing the test to still wait for a prompt answer.

Follow-up to a13306a79f
2025-09-24 07:59:21 +02:00
Andy Flesner 7ce0e03614
Update Python performance reference link in FAQ 2025-09-23 21:45:27 -05:00
qutebrowser bot d64781c31c Update dependencies 2025-09-22 04:24:08 +00:00
Florian Bruhin a8f9fc1396 Update changelog 2025-09-21 18:36:36 +02:00
Florian Bruhin a13306a79f Fix exception when quitting with download prompt open
If we have a pending download with a prompt and quit qutebrowser, we get:

    mainwindow:closeEvent:715 Closing window 0
    quitter:shutdown:221 Shutting down with status 0, session None...
    prompt:shutdown:121 Shutting down with loops [<qutebrowser.utils.qtutils.EventLoop object at 0x7fa034725770>]
    quitter:shutdown:235 Deferring shutdown stage 2
    prompt:ask_question:179 Ending loop.exec() for <qutebrowser.utils.usertypes.Question default='/tmp/qbdl/download/' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>https://proof.ovh.net/files/1Mb.dat</b>' title='Save file to:'>
    prompt:ask_question:181 Restoring old question None
    prompt:_on_show_prompts:299 Deleting old prompt qutebrowser.mainwindow.prompt.DownloadFilenamePrompt(question=<qutebrowser.utils.usertypes.Question default='/tmp/qbdl/download/' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>https://proof.ovh.net/files/1Mb.dat</b>' title='Save file to:'>)
    prompt:_on_show_prompts:303 No prompts left, hiding prompt container.
    objreg:on_destroyed:112 schedule removal: 0
    objreg:on_destroyed:112 schedule removal: tab
    [...]
    objreg:on_destroyed:112 schedule removal: 0
    objreg:on_destroyed:112 schedule removal: main-window
    objreg:on_destroyed:112 schedule removal: tabbed-browser
    objreg:on_destroyed:112 schedule removal: status-command
    objreg:on_destroyed:112 schedule removal: completion
    objreg:on_destroyed:112 schedule removal: mode-manager
    objreg:on_destroyed:112 schedule removal: hintmanager
    objreg:on_destroyed:112 schedule removal: prompt-container
    quitter:_shutdown_2:241 Stage 2 of shutting down...
    [...]
    webenginedownloads:_on_state_changed:63 State for <qutebrowser.browser.webengine.webenginedownloads.DownloadItem basename='1Mb.dat'> changed to DownloadCancelled
    downloads:_on_begin_remove_row:1076 _on_begin_remove_row with idx 0, webengine True
    downloads:_remove_item:995 Removed download 1: 1Mb.dat [100%|?.??B]
    modeman:leave:429 Leaving mode KeyMode.prompt (reason: aborted)
    crashsignal:_handle_early_exits:229 Uncaught exception
    Traceback (most recent call last):
    File "[...]/qutebrowser/mainwindow/prompt.py", line 344, in _on_aborted
        modeman.leave(self._win_id, key_mode, 'aborted', maybe=True)
        ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "[...]/qutebrowser/keyinput/modeman.py", line 214, in leave
        instance(win_id).leave(mode, reason, maybe=maybe)
        ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "[...]/qutebrowser/keyinput/modeman.py", line 435, in leave
        self.left.emit(mode, self.mode, self._win_id)
        ^^^^^^^^^
    RuntimeError: wrapped C/C++ object of type ModeManager has been deleted
2025-09-21 18:33:05 +02:00
Florian Bruhin c6b486ce81 Fix segfault with nested download prompts
For an unknown reason, if a download prompt is triple-nested, Qt segfaults here:

    #0  add_lazy_attrs (td=0x7ffff52e0dd1 <QtPrivate::sizedFree(void*, unsigned long)+32>) at sip_core.c:6255
    #1  sip_add_all_lazy_attrs (td=0x7ffff52e0dd1 <QtPrivate::sizedFree(void*, unsigned long)+32>) at sip_core.c:6304
    #2  0x00007ffff636598e in sip_api_is_py_method_12_8 (gil=gil@entry=0x7fffffff8b8c, pymc=pymc@entry=0x7fffffff8b8b "", sipSelfp=sipSelfp@entry=0x7fffffff8ba8, cname=cname@entry=0x0, mname=mname@entry=0x7ffff636d57b "__dtor__") at sip_core.c:7402
    #3  0x00007ffff6365c2c in sip_api_is_py_method_12_8 (mname=0x7ffff636d57b "__dtor__", cname=0x0, sipSelfp=0x7fffffff8ba8, pymc=0x7fffffff8b8b "", gil=0x7fffffff8b8c) at sip_core.c:7356
    #4  callPyDtor (self=<optimized out>) at sip_core.c:5375
    #5  sip_api_instance_destroyed_ex (sipSelfp=0x7fffffff8c40) at sip_core.c:5311
    #6  0x00007ffff5fc9967 in sipQEventLoop::~sipQEventLoop() () from [...]/python3.13/site-packages/PyQt6/QtCore.abi3.so
    #7  0x00007ffff0bcd749 in QFileInfoGatherer::getInfo (this=0x5555583f9bc0, fileInfo=...) at [...]/qt5/qtbase/src/gui/itemmodels/qfileinfogatherer.cpp:349
    #8  0x00007ffff0be2629 in QFileSystemModelPrivate::fileSystemChanged (this=0x5555583f9870, path="/tmp/qbdl/download", updates=QList<std::pair<QString, QFileInfo>> (size = 3) = {...}) at [...]/qt5/qtbase/src/gui/itemmodels/qfilesystemmodel.cpp:1966
    (full stacktrace has 183 frames)

After a lot of experimentation, I figured out that moving the `NullIconProvider`
to a class variable (or removing it entirely) seems to help. Note that making it
`global` (but keeping the instanciation inside `FilenamePrompt._init_fileview()`)
did not!

This is semi-blind fix: It's unclear to me if this is a proper fix, or if the
changed memory layout just causes the issue to temporary disappear.

However, given that `QFileInfoGatherer::getInfo()` calls into the icon provider,
it might very well be the real deal.

Closes #8674
2025-09-21 18:07:34 +02:00
Florian Bruhin 18bea32975 Revert "pylint: Remove unneeded suppression"
This reverts commit 7e770765a9.
2025-09-17 18:08:06 +02:00
Florian Bruhin 7e770765a9 pylint: Remove unneeded suppression 2025-09-17 16:06:01 +02:00
Florian Bruhin 1aaf621409 pakjoy: Move main block to function 2025-09-17 15:59:08 +02:00
Florian Bruhin 7d8d14eb07 config: Add file select completions for yazi 2025-09-17 15:44:37 +02:00
Florian Bruhin 226d167ec7 Qt 6.10: Skip incompatible pakjoy tests
See https://github.com/qutebrowser/qutebrowser/issues/8694#issuecomment-3276414407
2025-09-17 15:35:22 +02:00
Florian Bruhin a7822d1e2a pakjoy: Add simple example code 2025-09-16 11:14:47 +02:00
Florian Bruhin 1f4ca40f78 tests: Work around Qt 6.10 permission persistence bug
https://bugreports.qt.io/browse/QTBUG-140194
https://github.com/qutebrowser/qutebrowser/issues/8694#issuecomment-3274635002
2025-09-16 10:58:47 +02:00
Florian Bruhin 75239be8b6 Merge branch 'update-dependencies' 2025-09-15 18:00:46 +02:00
Florian Bruhin 488339b3d5 coverage: Set patch = subprocess
Subprocess measuring got integrated into coverage.py and removed from
pytest-cov:

https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst#700-2025-09-09
https://coverage.readthedocs.io/en/latest/config.html#run-patch
2025-09-15 13:12:38 +02:00
qutebrowser bot 52917f9c31 Update dependencies 2025-09-15 04:21:54 +00:00
Florian Bruhin 326a83309a version: Update for Qt 6.10 Beta 4 2025-09-11 01:00:01 +02:00
Florian Bruhin 264d91eaec ci: Fix shellcheck 2025-09-09 23:27:24 +02:00
Florian Bruhin 9e280eb80f ci: Add scoop dir to PATH 2025-09-09 23:23:07 +02:00
Florian Bruhin c278069946 ci: Fix actionlint 2025-09-09 23:11:03 +02:00
Florian Bruhin 5894221924 ci: Upgrade release workflow to windows 2022
windows-2019 is unsupported by GitHub Actions:
https://github.com/actions/runner-images/issues/12045
2025-09-09 22:07:55 +02:00
Florian Bruhin af2450591c ci: Install NSIS manually for nightly builds
window-latest switched to windows-2025, where GitHub doesn't preinstall NSIS:
https://github.com/actions/runner-images/issues/12677

Let's install it manually (untested, might need follow-up commits).

The release.yml workflow uses windows-2019 (and will switch to windows-2022 in a
follow-up commit), so it is unaffected for now.
2025-09-09 22:07:55 +02:00
Florian Bruhin 65de4fde16 pakjoy: Fix filename for debug builds 2025-09-09 22:07:55 +02:00
Florian Bruhin 4d6dccfed5 Qt 6.10: Ignore new logging messages
See #8694
2025-09-09 22:07:55 +02:00
Florian Bruhin 45483d0994
Merge pull request #8698 from qutebrowser/update-dependencies
Update dependencies
2025-09-08 23:38:42 +02:00
Florian Bruhin c3eef39050
Merge pull request #8701 from qutebrowser/dependabot/github_actions/actions/setup-node-5
build(deps): bump actions/setup-node from 4 to 5
2025-09-08 23:31:04 +02:00
Florian Bruhin a93f2e917d
Merge pull request #8700 from qutebrowser/dependabot/github_actions/actions/github-script-8
build(deps): bump actions/github-script from 7 to 8
2025-09-08 23:30:33 +02:00
Florian Bruhin d8beaa1e9d
Merge pull request #8699 from qutebrowser/dependabot/github_actions/actions/setup-python-6
build(deps): bump actions/setup-python from 5 to 6
2025-09-08 23:30:11 +02:00
dependabot[bot] aa6fcab23a
build(deps): bump actions/setup-node from 4 to 5
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 19:00:03 +00:00
dependabot[bot] 1e243b0d06
build(deps): bump actions/github-script from 7 to 8
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 18:59:59 +00:00
dependabot[bot] ce6be4f741
build(deps): bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 18:59:56 +00:00
qutebrowser bot 195d19ec3f Update dependencies 2025-09-08 04:22:17 +00:00
Florian Bruhin 78b55f84cc scripts: Handle ImportError without QUTE_QT_WRAPPER set in link_pyqt
If link_pyqt() is run from mkvenv.py, QUTE_QT_WRAPPER may not be set,
so we shouldn't crash when trying to display import error messages.
2025-09-05 11:24:51 +02:00
Florian Bruhin 7f532f69d2 requirements: Qt 6.9.2 2025-09-03 00:38:32 +02:00
Florian Bruhin 50080e8607
Merge pull request #8692 from qutebrowser/update-dependencies
Update dependencies
2025-09-03 00:31:42 +02:00
Florian Bruhin 17522478db version: Update for QtWebEngine 6.9.2 and 6.10 2025-09-02 23:47:00 +02:00
qutebrowser bot 44dbef482e Update dependencies 2025-09-01 04:26:18 +00:00
Florian Bruhin c0216a1309 Update changelog 2025-08-25 10:27:11 +02:00
Florian Bruhin c63a98e3e6 Merge remote-tracking branch 'origin/pr/8681' 2025-08-25 10:26:38 +02:00
Florian Bruhin 08f5509bed Fix tests with broken asciidoc install 2025-08-25 10:26:25 +02:00
Florian Bruhin 4180e04f24
Merge pull request #8686 from qutebrowser/update-dependencies
Update dependencies
2025-08-25 09:07:41 +02:00
qutebrowser bot 3cdacde13e Update dependencies 2025-08-25 04:23:50 +00:00
Sander ada069b3e3
feat(qutedmenu): sort history results by last access time 2025-08-21 23:20:34 +02:00
Florian Bruhin 9e99d6df34
Merge pull request #8676 from qutebrowser/update-dependencies
Update dependencies
2025-08-18 23:08:52 +02:00
qutebrowser bot afd1515d8d Update dependencies 2025-08-18 04:33:54 +00:00
Florian Bruhin 6d1cd0282a
Merge pull request #8670 from qutebrowser/update-dependencies
Update dependencies
2025-08-14 00:52:36 +02:00
Florian Bruhin 21082a85a4
Merge pull request #8673 from qutebrowser/dependabot/github_actions/actions/checkout-5
build(deps): bump actions/checkout from 4 to 5
2025-08-14 00:50:12 +02:00
dependabot[bot] 745bb7f4f0
build(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 01:09:55 +00:00
qutebrowser bot ec86816be1 Update dependencies 2025-08-11 04:37:07 +00:00
Florian Bruhin 85e1a132bb scripts: Update shellcheck error ignores for password_fill 2025-08-04 11:35:04 +02:00
Florian Bruhin d8fd61811c
Merge pull request #8659 from qutebrowser/update-dependencies
Update dependencies
2025-08-04 10:14:55 +02:00
qutebrowser bot 40b3e70659 Update dependencies 2025-08-04 04:43:36 +00:00
Florian Bruhin 0c035c41d8 Adjust qt.workarounds.disable_accelerated_2d_canvas description
Closes #8660
2025-07-29 10:15:06 +02:00
Florian Bruhin f2ec540a1b tests: Fix sys.flags workaround with older Python versions
types.SimpleNamespace only accepts a positional argument with Python 3.13: 'https://docs.python.org/3/whatsnew/3.13.html#types'
2025-07-29 10:10:13 +02:00
Florian Bruhin b9aec6777a tests: Fix monkeypatching of sys.flags on Python 3.14
With Python 3.14, since argparse enabled coloring by default:
17c5959aa3
running test_cmdutils.py failed with a lengthy traceback finishing in:

```
INTERNALERROR> pluggy.PluggyTeardownRaisedWarning: A plugin raised an exception during an old-style hookwrapper teardown.
INTERNALERROR> Plugin: /home/florian/proj/qutebrowser/git/tests/conftest.py, Hook: pytest_runtest_makereport
INTERNALERROR> PluggyTeardownRaisedWarning: A plugin raised an exception during an old-style hookwrapper teardown.
INTERNALERROR> Plugin: rerunfailures, Hook: pytest_runtest_makereport
INTERNALERROR> PluggyTeardownRaisedWarning: A plugin raised an exception during an old-style hookwrapper teardown.
INTERNALERROR> Plugin: hypothesispytest, Hook: pytest_runtest_makereport
INTERNALERROR> PluggyTeardownRaisedWarning: A plugin raised an exception during an old-style hookwrapper teardown.
INTERNALERROR> Plugin: pytest-bdd, Hook: pytest_runtest_makereport
INTERNALERROR> AttributeError: 'types.SimpleNamespace' object has no attribute 'verbose'
```

with one part pointing to Python internals:

```pytb
INTERNALERROR>   File "/home/florian/proj/qutebrowser/git/.tox/py314-pyqt68/lib/python3.14/site-packages/_pytest/_code/code.py", line 669, in exconly
INTERNALERROR>     lines = format_exception_only(self.type, self.value)
INTERNALERROR>   File "/usr/lib/python3.14/traceback.py", line 180, in format_exception_only
INTERNALERROR>     te = TracebackException(type(value), value, None, compact=True)
INTERNALERROR>   File "/usr/lib/python3.14/traceback.py", line 1106, in __init__
INTERNALERROR>     suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
INTERNALERROR>   File "/usr/lib/python3.14/traceback.py", line 1653, in _compute_suggestion_error
INTERNALERROR>     import _suggestions
INTERNALERROR>   File "<frozen importlib._bootstrap>", line 1371, in _find_and_load
INTERNALERROR>   File "<frozen importlib._bootstrap>", line 1342, in _find_and_load_unlocked
INTERNALERROR>   File "<frozen importlib._bootstrap>", line 951, in _load_unlocked
INTERNALERROR>   File "<frozen importlib._bootstrap>", line 496, in _verbose_message
INTERNALERROR> AttributeError: 'types.SimpleNamespace' object has no attribute 'verbose'
```

This happens when Python tries to access sys.flags.verbose, but we replaced it
with a fake object that does not have a .verbose attribute anymore (only
temporarily, but for some reason that's enough to trigger the issue anyways).

Since we can't mutate sys.flags, instead we create an object that copies all
sys.flags attributes, and then use that as a nicer replacement.

See #8529
2025-07-27 13:58:15 +02:00
Florian Bruhin 6b3b83981a Qt 6.10: Adjust security patch version 2025-07-21 13:47:45 +02:00
Florian Bruhin d80d80b867 Qt 6.10: Adjust CI test skipping 2025-07-21 13:47:45 +02:00
Florian Bruhin c9d3b61ca2
Merge pull request #8656 from qutebrowser/update-dependencies
Update dependencies
2025-07-21 13:42:03 +02:00
qutebrowser bot 87ad9dce57 Update dependencies 2025-07-21 04:37:45 +00:00
Florian Bruhin 961b4a212d
Merge pull request #8644 from qutebrowser/update-dependencies
Update dependencies
2025-07-14 07:27:52 +02:00
qutebrowser bot e151c2d0cf Update dependencies 2025-07-14 04:34:13 +00:00
Florian Bruhin c79257dc8d Remove pytest-mock pytest warning ignore
Fixed in pytest-mock 3.14.1
2025-07-08 21:15:15 +02:00
Florian Bruhin 22f1b57347 lint: Move check-manifest into pyroma
pyroma now runs check-manifest if installed,
so we can simplify things there.
2025-07-07 08:09:47 +02:00
Florian Bruhin 2536cc2313 tox: Fail pyroma run on degradations
The only rating we're satisfied with is Mascarpone!
2025-07-07 08:04:25 +02:00
Florian Bruhin d15052a67a Add stub pyproject.toml file
This keeps our setup.py around for now, while still supporting a PEP-517
compliant build. It's the minimum required change to make modern pyroma stop
complaining, and hopefully to avoid deprecation warnings.

Partially duplicates #8560
See #3526
2025-07-07 08:00:29 +02:00
qutebrowser bot be87927923 Update dependencies 2025-07-07 04:27:56 +00:00
Florian Bruhin 7315c34957 Add support for hinting elements inside shadow DOMs
Supersedes and closes #7617
See #3569 (fixed partially)
2025-07-01 16:49:10 +02:00
Florian Bruhin 8bf6a9c448
Merge pull request #8629 from qutebrowser/update-dependencies
Update dependencies
2025-06-30 08:04:38 +02:00
qutebrowser bot 9e459a004c Update dependencies 2025-06-30 04:28:14 +00:00
Florian Bruhin e7af54898e Add X11/Wayland information to version info
Unfortunately there is no way to get this information from Qt, so I had to
resort to some funny low-level C-like Python programming to directly use
libwayland-client and Xlib. Fun was had! Hopefully this avoids having to ask
for this information every time someone shows a bug/crash report, as there
are various subtleties that can be specific to the Wayland compositor in use.
2025-06-25 09:34:35 +02:00
Florian Bruhin 7664fdbb34
Merge pull request #8623 from qutebrowser/update-dependencies
Update dependencies
2025-06-24 20:53:48 +02:00
Florian Bruhin 81546c97ae version: Add security patch level for QtWebEngine 6.10 Beta 1 2025-06-24 20:42:03 +02:00
qutebrowser bot b2f58856a8 Update dependencies 2025-06-23 04:29:58 +00:00
Florian Bruhin 5e565f4dc7 Fix PermissionElement tests 2025-06-16 22:27:55 +02:00
Florian Bruhin 879c964c35 Update changelog 2025-06-16 14:09:41 +02:00
Florian Bruhin 2ec0f8fa77 Extend PermissionElement workaround to Qt 6.9.1+
Fixes #8612
2025-06-16 14:09:41 +02:00
Florian Bruhin 9d5aeefa8c
Merge pull request #8610 from qutebrowser/update-dependencies
Update dependencies
2025-06-16 10:33:39 +02:00
qutebrowser bot ed6329737f Update dependencies 2025-06-16 04:28:21 +00:00
Florian Bruhin 04a1d11905
Merge pull request #8603 from qutebrowser/update-dependencies
Update dependencies
2025-06-09 10:47:18 +02:00
qutebrowser bot 47724f913e Update dependencies 2025-06-09 04:26:50 +00:00
Florian Bruhin a45d57feee tests: Adjust ignored log message for newer Chromium
QtWebEngine 6.9 seems to use webengine/DawnWebGPUCache/ instead of DawnCache/
2025-06-06 12:53:10 +02:00
Florian Bruhin eca17870ae tests: Extend Qt 6.9.0 workaround to .1
Still seems to be failing, see https://github.com/qutebrowser/qutebrowser/issues/8444#issuecomment-2569610110
2025-06-06 11:30:27 +02:00
Florian Bruhin 959dc5a9e5 tests: Correctly deactivate SignalHandler after tests
Otherwise, the python_hacks timer might continue running
and affects test_early_timeout_check, similiarly to what was fixed in
74c7ff2641.
2025-06-06 10:53:38 +02:00
Florian Bruhin 7b388017c3 Update changelog 2025-06-06 08:51:00 +02:00
Florian Bruhin 55ca67f3e2 tests: Stop hardcoding indices for module versions 2025-06-06 08:49:28 +02:00
Florian Bruhin 6b86a9072f version: Rely on importlib.metadata.version too
Packages are slowly migrating to not having a __version__ attribute anymore,
instead relying on importlib.metadata to query the installed version.

jinja2 now shows a deprecation warning when accessing the __version__
attribute: https://github.com/pallets/jinja/pull/2098

For now we keep accessing __version__ for other packages (we still need the
logic for PyQt and its special version attributes anyways), but we fall back on
importlib.metadata.version if we can't get a version that way, and we stop
trying __version__ for jinja2.
2025-06-06 08:47:00 +02:00
qutebrowser bot 96e535c7ed Release v3.5.1
(cherry picked from commit be7cf1ab15)
2025-06-05 14:55:03 +00:00
Florian Bruhin 1217f7fa45 Update user agents 2025-06-05 16:38:10 +02:00
Florian Bruhin c1e7b6e8f3 tests: Ignore test_restart hang on Windows 2025-06-05 15:09:57 +02:00
Florian Bruhin 2e343403de tests: Ignore another bogus Chromium log message 2025-06-05 15:08:00 +02:00
Florian Bruhin 2ec16a947f ci: Switch nightly builds to windows-latest
We don't really care which exact Windows version we use there
2025-06-05 15:01:29 +02:00
100 changed files with 2307 additions and 602 deletions

View File

@ -1,5 +1,5 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "3.5.0" current_version = "3.6.3"
commit = true commit = true
message = "Release v{new_version}" message = "Release v{new_version}"
tag = true tag = true

View File

@ -4,6 +4,7 @@ include =
tests/* tests/*
scripts/* scripts/*
branch = true branch = true
patch = subprocess
omit = omit =
qutebrowser/__main__.py qutebrowser/__main__.py
*/__init__.py */__init__.py

View File

@ -42,6 +42,7 @@ exclude = .*,__pycache__,resources.py
# W503: like break before binary operator # W503: like break before binary operator
# W504: line break after binary operator # W504: line break after binary operator
# FI18: __future__ import "annotations" missing # FI18: __future__ import "annotations" missing
# FI58: __future__ import "annotations" present
# PT004: fixture '{name}' does not return anything, add leading underscore # PT004: fixture '{name}' does not return anything, add leading underscore
# PT011: pytest.raises(ValueError) is too broad, set the match parameter or use a more specific exception # PT011: pytest.raises(ValueError) is too broad, set the match parameter or use a more specific exception
# PT012: pytest.raises() block should contain a single simple statement # PT012: pytest.raises() block should contain a single simple statement
@ -54,7 +55,7 @@ ignore =
D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D412,D413, D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D412,D413,
A003, A003,
W503, W504, W503, W504,
FI18, FI18,FI58,
PT004, PT004,
PT011, PT011,
PT012 PT012

View File

@ -17,8 +17,6 @@ jobs:
matrix: matrix:
include: include:
- testenv: bleeding - testenv: bleeding
image: "archlinux-webengine-unstable-qt6"
- testenv: bleeding-qt5
image: "archlinux-webengine-unstable" image: "archlinux-webengine-unstable"
container: container:
image: "qutebrowser/ci:${{ matrix.image }}" image: "qutebrowser/ci:${{ matrix.image }}"
@ -33,14 +31,13 @@ jobs:
- /home/runner/work/_temp/:/home/runner/work/_temp/ - /home/runner/work/_temp/:/home/runner/work/_temp/
options: --privileged --tty options: --privileged --tty
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: Set up problem matchers - name: Set up problem matchers
run: "python scripts/dev/ci/problemmatchers.py py3 ${{ runner.temp }}" run: "python scripts/dev/ci/problemmatchers.py py3 ${{ runner.temp }}"
- name: Upgrade 3rd party assets - name: Upgrade 3rd party assets
run: "tox exec -e ${{ matrix.testenv }} -- python scripts/dev/update_3rdparty.py --gh-token ${{ secrets.GITHUB_TOKEN }} --modern-pdfjs" run: "tox exec -e ${{ matrix.testenv }} -- python scripts/dev/update_3rdparty.py --gh-token ${{ secrets.GITHUB_TOKEN }} --modern-pdfjs"
if: "endsWith(matrix.image, '-qt6')"
- name: Run tox - name: Run tox
run: dbus-run-session tox -e ${{ matrix.testenv }} run: dbus-run-session tox -e ${{ matrix.testenv }}
- name: Gather info - name: Gather info
@ -51,7 +48,7 @@ jobs:
shell: bash shell: bash
if: failure() if: failure()
- name: Upload screenshots - name: Upload screenshots
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}" name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
path: | path: |

View File

@ -27,7 +27,6 @@ jobs:
- testenv: vulture - testenv: vulture
- testenv: misc - testenv: misc
- testenv: pyroma - testenv: pyroma
- testenv: check-manifest
- testenv: eslint - testenv: eslint
- testenv: shellcheck - testenv: shellcheck
args: "-f gcc" # For problem matchers args: "-f gcc" # For problem matchers
@ -35,20 +34,20 @@ jobs:
- testenv: actionlint - testenv: actionlint
- testenv: package - testenv: package
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/cache@v4 - uses: actions/cache@v5
with: with:
path: | path: |
.mypy_cache .mypy_cache
.tox .tox
~/.cache/pip ~/.cache/pip
key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('scripts/dev/pylint_checkers/qute_pylint/*.py') }}" key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('scripts/dev/pylint_checkers/qute_pylint/*.py') }}"
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: '3.10' python-version: '3.10'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: '22.x' node-version: '22.x'
if: "matrix.testenv == 'eslint'" if: "matrix.testenv == 'eslint'"
@ -91,14 +90,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- testenv: py-qt5 - testenv: py
image: archlinux-webengine image: archlinux-webengine
- testenv: py-qt5 - testenv: py
image: archlinux-webengine-unstable image: archlinux-webengine-unstable
- testenv: py
image: archlinux-webengine-qt6
- testenv: py
image: archlinux-webengine-unstable-qt6
container: container:
image: "qutebrowser/ci:${{ matrix.image }}" image: "qutebrowser/ci:${{ matrix.image }}"
env: env:
@ -111,7 +106,7 @@ jobs:
- /home/runner/work/_temp/:/home/runner/work/_temp/ - /home/runner/work/_temp/:/home/runner/work/_temp/
options: --privileged --tty options: --privileged --tty
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: Set up problem matchers - name: Set up problem matchers
@ -126,7 +121,7 @@ jobs:
shell: bash shell: bash
if: failure() if: failure()
- name: Upload screenshots - name: Upload screenshots
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}" name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
path: | path: |
@ -191,37 +186,41 @@ jobs:
- testenv: py313-pyqt68 - testenv: py313-pyqt68
os: ubuntu-24.04 os: ubuntu-24.04
python: "3.13" python: "3.13"
### PyQt 6.8 (Python 3.14) ### PyQt 6.8 (Python 3.13)
- testenv: py314-pyqt68 - testenv: py313-pyqt68
os: ubuntu-24.04
python: "3.14-dev"
### PyQt 6.9 (Python 3.13)
- testenv: py313-pyqt69
os: ubuntu-24.04 os: ubuntu-24.04
python: "3.13" python: "3.13"
### macOS Ventura ### PyQt 6.9 (Python 3.14)
- testenv: py313-pyqt69 - testenv: py314-pyqt69
os: macos-13 os: ubuntu-24.04
python: "3.13" python: "3.14"
args: "tests/unit" # Only run unit tests on macOS ### PyQt 6.10 (Python 3.14)
- testenv: py314-pyqt610
os: ubuntu-24.04
python: "3.14"
### macOS Sonoma (M1 runner) ### macOS Sonoma (M1 runner)
- testenv: py313-pyqt69 - testenv: py314-pyqt610
os: macos-14 os: macos-14
python: "3.13" python: "3.14"
args: "tests/unit" # Only run unit tests on macOS
### macOS Sequoia (Intel runner)
- testenv: py314-pyqt610
os: macos-15-intel
python: "3.14"
args: "tests/unit" # Only run unit tests on macOS args: "tests/unit" # Only run unit tests on macOS
### Windows ### Windows
- testenv: py313-pyqt69 - testenv: py314-pyqt610
os: windows-2022 os: windows-2022
python: "3.13" python: "3.14"
- testenv: py313-pyqt69 - testenv: py314-pyqt610
os: windows-2025 os: windows-2025
python: "3.13" python: "3.14"
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/cache@v4 - uses: actions/cache@v5
with: with:
path: | path: |
.mypy_cache .mypy_cache
@ -229,7 +228,7 @@ jobs:
~/.cache/pip ~/.cache/pip
key: "${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}" key: "${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "${{ matrix.python }}" python-version: "${{ matrix.python }}"
- name: Set up problem matchers - name: Set up problem matchers
@ -270,7 +269,7 @@ jobs:
shell: bash shell: bash
if: failure() if: failure()
- name: Upload screenshots - name: Upload screenshots
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.testenv }}-${{ matrix.os }}" name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.testenv }}-${{ matrix.os }}"
path: | path: |
@ -286,16 +285,16 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: javascript, python languages: javascript, python
queries: +security-extended queries: +security-extended
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
irc: irc:
timeout-minutes: 2 timeout-minutes: 2

View File

@ -15,11 +15,9 @@ jobs:
image: image:
- archlinux-webengine - archlinux-webengine
- archlinux-webengine-unstable - archlinux-webengine-unstable
- archlinux-webengine-unstable-qt6
- archlinux-webengine-qt6
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: '3.x' python-version: '3.x'
- run: pip install jinja2 - run: pip install jinja2

View File

@ -14,36 +14,45 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: macos-13 - os: macos-15-intel
toxenv: build-release toxenv: build-release
name: macos-intel name: macos-intel
- os: macos-14 - os: macos-14
toxenv: build-release toxenv: build-release
name: macos-apple-silicon name: macos-apple-silicon
- os: windows-2019 - os: windows-latest
toxenv: build-release toxenv: build-release
name: windows name: windows
- os: macos-13 - os: macos-15-intel
args: --debug args: --debug
toxenv: build-release toxenv: build-release
name: macos-debug-intel name: macos-debug-intel
- os: macos-14 - os: macos-14
toxenv: build-release toxenv: build-release
name: macos-debug-apple-silicon name: macos-debug-apple-silicon
- os: windows-2019 - os: windows-latest
args: --debug args: --debug
toxenv: build-release toxenv: build-release
name: windows-debug name: windows-debug
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
timeout-minutes: 45 timeout-minutes: 45
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "3.13" python-version: "3.13"
- name: Install nsis
if: "matrix.os == 'windows-latest'"
run: |
irm get.scoop.sh | iex
scoop update
scoop bucket add extras
scoop install nsis
Add-Content $env:GITHUB_PATH "C:\Users\runneradmin\scoop\shims"
shell: pwsh
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install -U pip python -m pip install -U pip
@ -63,7 +72,7 @@ jobs:
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash shell: bash
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: "qutebrowser-nightly-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.name }}" name: "qutebrowser-nightly-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.name }}"
path: | path: |

View File

@ -18,11 +18,11 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python 3.9 - name: Set up Python 3.9
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: '3.9' python-version: '3.9'
- name: Recompile requirements - name: Recompile requirements
@ -41,7 +41,7 @@ jobs:
- name: Run qutebrowser smoke test - name: Run qutebrowser smoke test
run: "xvfb-run .venv/bin/python3 -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ':later 500 quit'" run: "xvfb-run .venv/bin/python3 -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ':later 500 quit'"
- name: Create pull request - name: Create pull request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v8
with: with:
committer: qutebrowser bot <bot@qutebrowser.org> committer: qutebrowser bot <bot@qutebrowser.org>
author: qutebrowser bot <bot@qutebrowser.org> author: qutebrowser bot <bot@qutebrowser.org>

View File

@ -12,11 +12,12 @@ on:
- 'patch' - 'patch'
- 'minor' - 'minor'
- 'major' - 'major'
- 'reupload' # reupload last release
# FIXME do we want a possibility to do prereleases here? # FIXME do we want a possibility to do prereleases here?
python_version: python_version:
description: 'Python version' description: 'Python version'
required: true required: true
default: '3.13' default: '3.14'
type: choice type: choice
options: options:
- '3.9' - '3.9'
@ -24,18 +25,20 @@ on:
- '3.11' - '3.11'
- '3.12' - '3.12'
- '3.13' - '3.13'
- '3.14'
jobs: jobs:
prepare: prepare:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
timeout-minutes: 5 timeout-minutes: 5
outputs: outputs:
version: ${{ steps.bump.outputs.version }} version: ${{ steps.bump.outputs.version }}
release_id: ${{ steps.create-release.outputs.id }} version_x: ${{ steps.bump.outputs.version_x }}
release_id: ${{ inputs.release_type == 'reupload' && steps.find-release.outputs.result || steps.create-release.outputs.id }}
permissions: permissions:
contents: write # To push release commit/tag contents: write # To push release commit/tag
steps: steps:
- name: Find release branch - name: Find release branch
uses: actions/github-script@v7 uses: actions/github-script@v8
id: find-branch id: find-branch
with: with:
script: | script: |
@ -59,9 +62,9 @@ jobs:
console.log(`sorted: ${sorted}`); console.log(`sorted: ${sorted}`);
return sorted.at(-1); return sorted.at(-1);
result-encoding: string result-encoding: string
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
# Doesn't really matter what we prepare the release with, but let's # Doesn't really matter what we prepare the release with, but let's
# use the same version for consistency. # use the same version for consistency.
@ -75,7 +78,7 @@ jobs:
git config --global user.name "qutebrowser bot" git config --global user.name "qutebrowser bot"
git config --global user.email "bot@qutebrowser.org" git config --global user.email "bot@qutebrowser.org"
- name: Switch to release branch - name: Switch to release branch
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ steps.find-branch.outputs.result }} ref: ${{ steps.find-branch.outputs.result }}
- name: Import GPG Key - name: Import GPG Key
@ -83,9 +86,9 @@ jobs:
gpg --import <<< "${{ secrets.QUTEBROWSER_BOT_GPGKEY }}" gpg --import <<< "${{ secrets.QUTEBROWSER_BOT_GPGKEY }}"
- name: Bump version - name: Bump version
id: bump id: bump
run: "tox -e update-version -- ${{ github.event.inputs.release_type }}" run: "tox -e update-version -- ${{ inputs.release_type }}"
- name: Check milestone - name: Check milestone
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
const milestones = await github.paginate(github.rest.issues.listMilestones, { const milestones = await github.paginate(github.rest.issues.listMilestones, {
@ -100,35 +103,60 @@ jobs:
core.setFailed(`Found open milestone ${milestone.title} with ${milestone.open_issues} open and ${milestone.closed_issues} closed issues!`); core.setFailed(`Found open milestone ${milestone.title} with ${milestone.open_issues} open and ${milestone.closed_issues} closed issues!`);
} }
- name: Push release commit/tag - name: Push release commit/tag
if: ${{ inputs.release_type != 'reupload' }}
run: | run: |
git push origin ${{ steps.find-branch.outputs.result }} git push origin ${{ steps.find-branch.outputs.result }}
git push origin v${{ steps.bump.outputs.version }} git push origin v${{ steps.bump.outputs.version }}
- name: Cherry-pick release commit - name: Cherry-pick release commit
if: ${{ github.event.inputs.release_type == 'patch' }} if: ${{ inputs.release_type == 'patch' }}
run: | run: |
git fetch origin main
git checkout main git checkout main
git cherry-pick -x v${{ steps.bump.outputs.version }} git cherry-pick -x v${{ steps.bump.outputs.version }}
git push origin main git push origin main
git checkout v${{ steps.bump.outputs.version_x }} git checkout v${{ steps.bump.outputs.version_x }}
- name: Create release branch - name: Create release branch
if: ${{ github.event.inputs.release_type != 'patch' }} if: ${{ inputs.release_type == 'minor' || inputs.release_type == 'major' }}
run: | run: |
git checkout -b v${{ steps.bump.outputs.version_x }} git checkout -b v${{ steps.bump.outputs.version_x }}
git push --set-upstream origin v${{ steps.bump.outputs.version_x }} git push --set-upstream origin v${{ steps.bump.outputs.version_x }}
- name: Create GitHub draft release - name: Create GitHub draft release
if: ${{ inputs.release_type != 'reupload' }}
id: create-release id: create-release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{ steps.bump.outputs.version }} tag_name: v${{ steps.bump.outputs.version }}
draft: true draft: true
body: "*Release artifacts for this release are currently being uploaded...*" body: "*Release artifacts for this release are currently being uploaded...*"
- name: Find GitHub draft release
if: ${{ inputs.release_type == 'reupload' }}
id: find-release
uses: actions/github-script@v8
with:
script: |
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: context.repo.owner,
repo: context.repo.repo,
});
const names = releases.map(release => release.name);
console.log(`releases: ${names}`);
const release = releases.find(release => release.tag_name === "v${{ steps.bump.outputs.version }}");
if (release === undefined) {
core.setFailed(`No release found with tag v${{ steps.bump.outputs.version }}!`);
}
if (!release.draft) {
core.setFailed(`Release ${release.tag_name} is not a draft release!`);
}
return release.id;
result-encoding: string
release: release:
strategy: strategy:
matrix: matrix:
include: include:
- os: macos-13 - os: macos-14-large # Intel
- os: macos-14 - os: macos-14 # Apple Silicon
- os: windows-2019 - os: windows-2022
- os: ubuntu-24.04 - os: ubuntu-24.04
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
timeout-minutes: 45 timeout-minutes: 45
@ -136,13 +164,13 @@ jobs:
permissions: permissions:
contents: write # To upload release artifacts contents: write # To upload release artifacts
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
ref: v${{ needs.prepare.outputs.version }} ref: v${{ inputs.release_type == 'reupload' && needs.prepare.outputs.version_x || needs.prepare.outputs.version }}
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ github.event.inputs.python_version }} python-version: ${{ inputs.python_version }}
- name: Import GPG Key - name: Import GPG Key
if: ${{ startsWith(matrix.os, 'ubuntu-') }} if: ${{ startsWith(matrix.os, 'ubuntu-') }}
run: | run: |
@ -167,7 +195,7 @@ jobs:
# FIXME consider switching to trusted publishers: # FIXME consider switching to trusted publishers:
# https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/
- name: Build and upload release - name: Build and upload release
run: "tox -e build-release -- --upload --no-confirm" run: "tox -e build-release -- --upload --no-confirm ${{ inputs.release_type == 'reupload' && '--reupload' || '' }}"
env: env:
TWINE_USERNAME: __token__ TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.QUTEBROWSER_BOT_PYPI_TOKEN }} TWINE_PASSWORD: ${{ secrets.QUTEBROWSER_BOT_PYPI_TOKEN }}
@ -180,7 +208,7 @@ jobs:
contents: write # To change release contents: write # To change release
steps: steps:
- name: Publish final release - name: Publish final release
uses: actions/github-script@v7 uses: actions/github-script@v8
with: with:
script: | script: |
await github.rest.repos.updateRelease({ await github.rest.repos.updateRelease({

View File

@ -15,8 +15,124 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes. // `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities. // `Security` to invite users to upgrade in case of vulnerabilities.
[[v3.6.4]]
v3.6.4 (unreleased)
-------------------
Fixed
~~~~~
- datalist dropdowns not opening correctly on Wayland/Sway (#8831).
This was caused by an old workaround for a different QtWebEngine issue,
which is now disabled for QtWebEngine 6.6.3 and newer.
[[v3.6.3]]
v3.6.3 (2025-11-30)
-------------------
Fixed
~~~~~
- New `qt.workarounds.disable_accessibility` setting, which disables Chromium
accessibility support. By default, is it set to `auto`, which only disables
accessibility on Qt versions with known issues. This works around a bug in Qt
6.10.1 causing frequent segfaults (#8797).
[[v3.6.2]]
v3.6.2 (2025-11-27)
-------------------
Changed
~~~~~~~
* Windows and macOS releases now ship with Qt 6.10.1, which include
security patches up to Chromium 142.0.7444.162.
Fixed
~~~~~
- The version info now includes the Wayland compositor name if wayland-client is
available under a different name than `libwayland-client.so` (#8771).
- The list of Chromium extensions in `--version` / `:version` now uses the
correct Chromium data profile, also fixing a crash with Qt 6.10.1 (#8785).
- With Qt 6.10.1, `qt.workarounds.disable_hangouts_extension` now doesn't apply
on private profiles, avoiding a Qt bug leading to a crash (#8785).
[[v3.6.1]]
v3.6.1 (2025-11-03)
-------------------
Fixed
~~~~~
- A regression in v3.6.0 where the page didn't have keyboard focus after closing
the completion, so e.g. typing in an input field after hinting didn't work.
(#8750)
[[v3.6.0]]
v3.6.0 (2025-10-24)
-------------------
Added
~~~~~
- The `:version` info now shows additional information:
* The X11 window manager / Wayland compositor name (mostly useful for
bug/crash reports).
* Loaded WebExtensions (partial support landed in QtWebEngine 6.10, no
official qutebrowser support yet).
- Support for hinting elements which are part of an (open) shadow DOM.
Changed
~~~~~~~
- The `qutedmenu` userscript now sorts history by the last access time.
- Hardware accelerated 2D canvas is now enabled by default on Qt 6.8.2+,
as graphic glitches with e.g. PDF.js and Google Sheets should be fixed
nowadays. If you still run into issues, please report them and set
`qt.workarounds.disable_accelerated_2d_canvas` to `always` to disable it
again.
- Changes to binary releases:
* Windows and macOS releases are now built with Qt 6.10.0, which is based
on Chromium 134.0.6998.208 with security patches up to 140.0.7339.207.
* Windows and macOS releases are now built with Python 3.14.
* Windows releases are now built on Windows Server 2022 (previously 2019),
which might break compatibility with older Windows releases (untested).
* If using `mkvenv.py` on Linux, note that Qt now requires glibc v2.34 (v2.28
previously). This is available down to Ubuntu 22.04 LTS and Debian Bookworm
(oldstable), so this should not affect most users of desktop distributions.
Fixed
~~~~~
- Fixed crash if two new downloads start while a download prompt is already open
(#8674).
- Fixed exception when closing a qutebrowser window while a download prompt is
still open.
- Hopefully proper fix for some web pages jumping to the top when the statusbar
is hidden (#8223).
- Fix for the page header being shown on YouTube after the fullscreen
notification was hidden (#8625).
- Fix for videos losing keyboard focus when the fullscreen notification shows
(#8174).
- The workaround for microphone/camera permissions not being requested with
QtWebEngine 6.9 on Google Meet, Zoom, or other pages using the new
`<permission>` element now got extended to Qt 6.9.1+ as it's still not fixed
upstream. (#8612)
- The package version for Jinja 3.3+ is now correctly displayed in `:version`.
- Fixed crash with Qt 6.10 (and possibly older Qt versions) when navigating
from a `qute://` page to a web page, e.g. when searching on `qute://start`.
- On Wayland with Qt <= 6.9, `EGL_PLATFORM=wayland` is now set by qutebrowser to
get hardware rendering. Qt 6.10 includes an equivalent fix (#8637).
- Added workaround for per-domain User-Agent header not being used on redirects
(#8679).
- Added site-specific quirk for gitlab.gnome.org agressively blocking old
Chromium versions (and thus QtWebEngine) (#8509).
- Using `:config-list-remove` with an invalid value for the respective option
type now corrently displays an error instead of crashing.
[[v3.5.1]] [[v3.5.1]]
v3.5.1 (unreleased) v3.5.1 (2025-06-05)
------------------- -------------------
Deprecated Deprecated

View File

@ -121,8 +121,6 @@ Currently, the following tox environments are available:
* `pyroma`: Check packaging practices with * `pyroma`: Check packaging practices with
https://pypi.python.org/pypi/pyroma/[pyroma]. https://pypi.python.org/pypi/pyroma/[pyroma].
* `eslint`: Run https://eslint.org/[ESLint] javascript checker. * `eslint`: Run https://eslint.org/[ESLint] javascript checker.
* `check-manifest`: Check MANIFEST.in completeness with
https://github.com/mgedmin/check-manifest[check-manifest].
* `mkvenv`: Bootstrap a virtualenv for testing. * `mkvenv`: Bootstrap a virtualenv for testing.
* `misc`: Run `scripts/misc_checks.py` to check for: * `misc`: Run `scripts/misc_checks.py` to check for:
- untracked git files - untracked git files
@ -604,6 +602,7 @@ Info pages:
- chrome://device-log/ (QtWebEngine >= 6.3) - chrome://device-log/ (QtWebEngine >= 6.3)
- chrome://gpu/ - chrome://gpu/
- chrome://sandbox/ (Linux only) - chrome://sandbox/ (Linux only)
- chrome://qt/ (QtWebEngine >= 6.7)
Misc. / Debugging pages: Misc. / Debugging pages:
@ -614,6 +613,7 @@ Misc. / Debugging pages:
- chrome://ukm/ (QtWebEngine >= 5.15.3) - chrome://ukm/ (QtWebEngine >= 5.15.3)
- chrome://user-actions/ (QtWebEngine >= 5.15.3) - chrome://user-actions/ (QtWebEngine >= 5.15.3)
- chrome://webrtc-logs/ (QtWebEngine >= 5.15.3) - chrome://webrtc-logs/ (QtWebEngine >= 5.15.3)
- chrome://extensions/ (QtWebEngine >= 6.10)
Internals pages: Internals pages:
@ -804,7 +804,8 @@ qutebrowser release
**Automatic release via GitHub Actions (starting with v3.0.0):** **Automatic release via GitHub Actions (starting with v3.0.0):**
* Double check Python version in `.github/workflows/release.yml` * Double check Python version in `.github/workflows/release.yml`
* Run the `release` workflow on the `main` branch, e.g. via `gh workflow run release -f release_type=major` (`release_type` can be `major`, `minor` or `patch`; you can also override `python_version`) * Run the `release` workflow on the `main` branch, e.g. via `gh workflow run release -f release_type=minor` (`release_type` can be `major`, `minor` or `patch`; you can also override `python_version`)
* Consider running `gh run watch` or `gh run view --web` to watch the progress
**Manual release:** **Manual release:**

View File

@ -61,7 +61,7 @@ Why Python?::
point, I wasn't comfortable with C++ so that wasn't an alternative. point, I wasn't comfortable with C++ so that wasn't an alternative.
But isn't Python too slow for a browser?:: But isn't Python too slow for a browser?::
https://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[It's generally less of a problem than one would expect.] https://www.infoworld.com/article/2303031/van-rossum-python-is-not-too-slow-2.html[It's generally less of a problem than one would expect.]
Most of the heavy lifting of qutebrowser is done by Qt and Most of the heavy lifting of qutebrowser is done by Qt and
QtWebKit/QtWebEngine in C++, with the QtWebKit/QtWebEngine in C++, with the
https://wiki.python.org/moin/GlobalInterpreterLock[GIL] released. https://wiki.python.org/moin/GlobalInterpreterLock[GIL] released.
@ -141,7 +141,7 @@ The comma prefix is used to make sure user-defined bindings don't conflict with
the built-in ones. the built-in ones.
+ +
Note that you might need an additional package (e.g. Note that you might need an additional package (e.g.
https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on https://archlinux.org/packages/extra/any/yt-dlp/[yt-dlp] on
Archlinux) to play web videos with mpv. Archlinux) to play web videos with mpv.
+ +
There is a very useful script for mpv, which emulates "unique application" There is a very useful script for mpv, which emulates "unique application"

View File

@ -179,7 +179,7 @@ customizable for a given <<patterns,URL patterns>>.
[source,python] [source,python]
---- ----
config.set('content.images', False, '*://example.com/') config.set('content.images', False, '*://example.com/*')
---- ----
Alternatively, you can use `with config.pattern(...) as p:` to get a shortcut Alternatively, you can use `with config.pattern(...) as p:` to get a shortcut
@ -187,7 +187,7 @@ similar to `c.` which is scoped to the given domain:
[source,python] [source,python]
---- ----
with config.pattern('*://example.com/') as p: with config.pattern('*://example.com/*') as p:
p.content.images = False p.content.images = False
---- ----

View File

@ -302,6 +302,7 @@
|<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine. |<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine.
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling. |<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|<<qt.workarounds.disable_accelerated_2d_canvas,qt.workarounds.disable_accelerated_2d_canvas>>|Disable accelerated 2d canvas to avoid graphical glitches. |<<qt.workarounds.disable_accelerated_2d_canvas,qt.workarounds.disable_accelerated_2d_canvas>>|Disable accelerated 2d canvas to avoid graphical glitches.
|<<qt.workarounds.disable_accessibility,qt.workarounds.disable_accessibility>>|Disable accessibility to avoid crashes on Qt 6.10.1.
|<<qt.workarounds.disable_hangouts_extension,qt.workarounds.disable_hangouts_extension>>|Disable the Hangouts extension. |<<qt.workarounds.disable_hangouts_extension,qt.workarounds.disable_hangouts_extension>>|Disable the Hangouts extension.
|<<qt.workarounds.locale,qt.workarounds.locale>>|Work around locale parsing issues in QtWebEngine 5.15.3. |<<qt.workarounds.locale,qt.workarounds.locale>>|Work around locale parsing issues in QtWebEngine 5.15.3.
|<<qt.workarounds.remove_service_workers,qt.workarounds.remove_service_workers>>|Delete the QtWebEngine Service Worker directory on every start. |<<qt.workarounds.remove_service_workers,qt.workarounds.remove_service_workers>>|Delete the QtWebEngine Service Worker directory on every start.
@ -2770,6 +2771,7 @@ Valid values:
* +ua-google+ * +ua-google+
* +ua-googledocs+ * +ua-googledocs+
* +ua-gnome-gitlab+
* +js-whatsapp-web+ * +js-whatsapp-web+
* +js-discord+ * +js-discord+
* +js-string-replaceall+ * +js-string-replaceall+
@ -3990,11 +3992,29 @@ Type: <<types,String>>
Valid values: Valid values:
* +always+: Disable accelerated 2d canvas * +always+: Disable accelerated 2d canvas
* +auto+: Disable on Qt6 < 6.6.0, enable otherwise * +auto+: Disable on Qt versions with known issues, enable otherwise
* +never+: Enable accelerated 2d canvas * +never+: Enable accelerated 2d canvas
Default: +pass:[auto]+ Default: +pass:[auto]+
[[qt.workarounds.disable_accessibility]]
=== qt.workarounds.disable_accessibility
Disable accessibility to avoid crashes on Qt 6.10.1.
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
* +always+: Disable renderer accessibility
* +auto+: Disable on Qt versions with known issues, enable otherwise
* +never+: Enable renderer accessibility
Default: +pass:[auto]+
[[qt.workarounds.disable_hangouts_extension]] [[qt.workarounds.disable_hangouts_extension]]
=== qt.workarounds.disable_hangouts_extension === qt.workarounds.disable_hangouts_extension
Disable the Hangouts extension. Disable the Hangouts extension.

View File

@ -44,6 +44,11 @@
</content_rating> </content_rating>
<releases> <releases>
<!-- Add new releases here --> <!-- Add new releases here -->
<release version='3.6.3' date='2025-11-30'/>
<release version='3.6.2' date='2025-11-27'/>
<release version='3.6.1' date='2025-11-03'/>
<release version='3.6.0' date='2025-10-24'/>
<release version='3.5.1' date='2025-06-05'/>
<release version='3.5.0' date='2025-04-12'/> <release version='3.5.0' date='2025-04-12'/>
<release version="3.4.0" date="2024-12-14"/> <release version="3.4.0" date="2024-12-14"/>
<release version="3.3.1" date="2024-10-12"/> <release version="3.3.1" date="2024-10-12"/>

View File

@ -1,9 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
build==1.2.2.post1
check-manifest==0.50
importlib_metadata==8.7.0
packaging==25.0
pyproject_hooks==1.2.0
tomli==2.2.1
zipp==3.22.0

View File

@ -1 +0,0 @@
check-manifest

View File

@ -1,74 +1,73 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
annotated-types==0.7.0 annotated-types==0.7.0
anyio==4.9.0 anyio==4.12.0
autocommand==2.2.2 autocommand==2.2.2
backports.tarfile==1.2.0 backports.tarfile==1.2.0
bracex==2.5.post1 bracex==2.6
build==1.2.2.post1 build==1.3.0
bump-my-version==1.1.4 bump-my-version==1.2.5
certifi==2025.4.26 certifi==2025.11.12
cffi==1.17.1 cffi==2.0.0
charset-normalizer==3.4.2 charset-normalizer==3.4.4
click==8.1.8 click==8.1.8
cryptography==45.0.3 cryptography==46.0.3
docutils==0.21.2 docutils==0.22.3
exceptiongroup==1.3.0 exceptiongroup==1.3.1
github3.py==4.0.1 github3.py==4.0.1
h11==0.16.0 h11==0.16.0
httpcore==1.0.9 httpcore==1.0.9
httpx==0.28.1 httpx==0.28.1
hunter==3.7.0 hunter==3.9.0
id==1.5.0 id==1.5.0
idna==3.10 idna==3.11
importlib_metadata==8.7.0 importlib_metadata==8.7.0
importlib_resources==6.5.2 importlib_resources==6.5.2
inflect==7.3.1 inflect==7.3.1
jaraco.classes==3.4.0 jaraco.classes==3.4.0
jaraco.collections==5.1.0 jaraco.collections==5.1.0
jaraco.context==6.0.1 jaraco.context==6.0.1
jaraco.functools==4.1.0 jaraco.functools==4.0.1
jaraco.text==3.12.1 jaraco.text==3.12.1
jeepney==0.9.0 jeepney==0.9.0
keyring==25.6.0 keyring==25.7.0
manhole==1.8.1 manhole==1.8.1
markdown-it-py==3.0.0 markdown-it-py==3.0.0
mdurl==0.1.2 mdurl==0.1.2
more-itertools==10.7.0 more-itertools==10.8.0
nh3==0.2.21 nh3==0.3.2
packaging==25.0 packaging==25.0
platformdirs==4.3.8 platformdirs==4.4.0
prompt_toolkit==3.0.51 prompt_toolkit==3.0.52
pycparser==2.22 pycparser==2.23
pydantic==2.11.5 pydantic==2.12.5
pydantic-settings==2.9.1 pydantic-settings==2.11.0
pydantic_core==2.33.2 pydantic_core==2.41.5
Pygments==2.19.1 Pygments==2.19.2
PyJWT==2.10.1 PyJWT==2.10.1
Pympler==1.1 Pympler==1.1
pyproject_hooks==1.2.0 pyproject_hooks==1.2.0
PyQt-builder==1.18.2 PyQt-builder==1.19.1
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-dotenv==1.1.0 python-dotenv==1.2.1
questionary==2.1.0 questionary==2.1.1
readme_renderer==44.0 readme_renderer==44.0
requests==2.32.3 requests==2.32.5
requests-toolbelt==1.0.0 requests-toolbelt==1.0.0
rfc3986==2.0.0 rfc3986==2.0.0
rich==14.0.0 rich==14.2.0
rich-click==1.8.9 rich-click==1.9.4
SecretStorage==3.3.3 SecretStorage==3.3.3
sip==6.12.0 sip==6.14.0
six==1.17.0 six==1.17.0
sniffio==1.3.1 tomli==2.3.0
tomli==2.2.1 tomlkit==0.13.3
tomlkit==0.13.2 twine==6.2.0
twine==6.1.0
typeguard==4.3.0 typeguard==4.3.0
typing-inspection==0.4.1 typing-inspection==0.4.2
typing_extensions==4.14.0 typing_extensions==4.15.0
uritemplate==4.2.0 uritemplate==4.2.0
# urllib3==2.4.0 # urllib3==2.6.2
wcmatch==10.0 wcmatch==10.1
wcwidth==0.2.13 wcwidth==0.2.14
zipp==3.22.0 zipp==3.23.0

View File

@ -1,10 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==25.3.0 attrs==25.4.0
flake8==7.2.0 flake8==7.3.0
flake8-bugbear==24.12.12 flake8-bugbear==24.12.12
flake8-builtins==2.5.0 flake8-builtins==3.0.0
flake8-comprehensions==3.16.0 flake8-comprehensions==3.17.0
flake8-debugger==4.1.2 flake8-debugger==4.1.2
flake8-deprecated==2.2.1 flake8-deprecated==2.2.1
flake8-docstrings==1.7.0 flake8-docstrings==1.7.0
@ -12,12 +12,12 @@ flake8-future-import==0.4.7
flake8-plugin-utils==1.3.3 flake8-plugin-utils==1.3.3
flake8-pytest-style==2.1.0 flake8-pytest-style==2.1.0
flake8-string-format==0.3.0 flake8-string-format==0.3.0
flake8-tidy-imports==4.11.0 flake8-tidy-imports==4.12.0
flake8-tuple==0.4.1 flake8-tuple==0.4.1
mccabe==0.7.0 mccabe==0.7.0
pep8-naming==0.15.1 pep8-naming==0.15.1
pycodestyle==2.13.0 pycodestyle==2.14.0
pydocstyle==6.3.0 pydocstyle==6.3.0
pyflakes==3.3.2 pyflakes==3.4.0
six==1.17.0 six==1.17.0
snowballstemmer==3.0.1 snowballstemmer==3.0.1

View File

@ -1,19 +1,20 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
chardet==5.2.0 chardet==5.2.0
diff_cover==9.3.2 diff_cover==10.0.0
Jinja2==3.1.6 Jinja2==3.1.6
lxml==5.4.0 librt==0.7.3
MarkupSafe==3.0.2 lxml==6.0.2
mypy==1.16.0 MarkupSafe==3.0.3
mypy==1.19.0
mypy_extensions==1.1.0 mypy_extensions==1.1.0
pathspec==0.12.1 pathspec==0.12.1
pluggy==1.6.0 pluggy==1.6.0
Pygments==2.19.1 Pygments==2.19.2
PyQt5-stubs==5.15.6.0 PyQt5-stubs==5.15.6.0
tomli==2.2.1 tomli==2.3.0
types-colorama==0.4.15.20240311 types-colorama==0.4.15.20250801
types-docutils==0.21.0.20250604 types-docutils==0.22.3.20251115
types-Pygments==2.19.0.20250516 types-Pygments==2.19.0.20251121
types-PyYAML==6.0.12.20250516 types-PyYAML==6.0.12.20250915
typing_extensions==4.14.0 typing_extensions==4.15.0

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.17.4 altgraph==0.17.5
importlib_metadata==8.7.0 importlib_metadata==8.7.0
packaging==25.0 packaging==25.0
pyinstaller==6.14.0 pyinstaller==6.17.0
pyinstaller-hooks-contrib==2025.4 pyinstaller-hooks-contrib==2025.10
zipp==3.22.0 zipp==3.23.0

View File

@ -1,26 +1,28 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==3.3.10 astroid==3.3.11
certifi==2025.4.26 certifi==2025.11.12
cffi==1.17.1 cffi==2.0.0
charset-normalizer==3.4.2 charset-normalizer==3.4.4
cryptography==45.0.3 cryptography==46.0.3
dill==0.4.0 dill==0.4.0
github3.py==4.0.1 github3.py==4.0.1
idna==3.10 idna==3.11
isort==6.0.1 importlib_metadata==8.7.0
isort==6.1.0
mccabe==0.7.0 mccabe==0.7.0
pefile==2024.8.26 pefile==2024.8.26
platformdirs==4.3.8 platformdirs==4.4.0
pycparser==2.22 pycparser==2.23
PyJWT==2.10.1 PyJWT==2.10.1
pylint==3.3.7 pylint==3.3.9
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.32.3 requests==2.32.5
six==1.17.0 six==1.17.0
tomli==2.2.1 tomli==2.3.0
tomlkit==0.13.2 tomlkit==0.13.3
typing_extensions==4.14.0 typing_extensions==4.15.0
uritemplate==4.2.0 uritemplate==4.2.0
# urllib3==2.4.0 # urllib3==2.6.2
zipp==3.23.0

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.2 # rq.filter: == 5.15.2 PyQt5==5.15.2 # rq.filter: == 5.15.2
PyQt5_sip==12.17.0 PyQt5_sip==12.17.1
PyQtWebEngine==5.15.2 # rq.filter: == 5.15.2 PyQtWebEngine==5.15.2 # rq.filter: == 5.15.2

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.11 # rq.filter: < 5.16 PyQt5==5.15.11 # rq.filter: < 5.16
PyQt5-Qt5==5.15.17 PyQt5-Qt5==5.15.18
PyQt5_sip==12.17.0 PyQt5_sip==12.17.1
PyQtWebEngine==5.15.7 # rq.filter: < 5.16 PyQtWebEngine==5.15.7 # rq.filter: < 5.16
PyQtWebEngine-Qt5==5.15.17 PyQtWebEngine-Qt5==5.15.18

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.11 PyQt5==5.15.11
PyQt5-Qt5==5.15.17 PyQt5-Qt5==5.15.18
PyQt5_sip==12.17.0 PyQt5_sip==12.17.1
PyQtWebEngine==5.15.7 PyQtWebEngine==5.15.7
PyQtWebEngine-Qt5==5.15.17 PyQtWebEngine-Qt5==5.15.18

View File

@ -0,0 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.10.1
PyQt6-Qt6==6.10.1
PyQt6-WebEngine==6.10.0
PyQt6-WebEngine-Qt6==6.10.1
PyQt6_sip==13.10.2
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -0,0 +1,8 @@
PyQt6 >= 6.10, < 6.11
PyQt6-Qt6 >= 6.10, < 6.11
PyQt6-WebEngine >= 6.10, < 6.11
PyQt6-WebEngine-Qt6 >= 6.10, < 6.11
# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2025-October/046347.html
#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.9.0 PyQt6==6.9.1
PyQt6-Qt6==6.9.1 PyQt6-Qt6==6.9.2
PyQt6-WebEngine==6.9.0 PyQt6-WebEngine==6.9.0
PyQt6-WebEngine-Qt6==6.9.1 PyQt6-WebEngine-Qt6==6.9.2
PyQt6_sip==13.10.2 PyQt6_sip==13.10.2

View File

@ -1,7 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.9.0 PyQt6==6.10.1
PyQt6-Qt6==6.9.1 PyQt6-Qt6==6.10.1
PyQt6-WebEngine==6.9.0 PyQt6-WebEngine==6.10.0
PyQt6-WebEngine-Qt6==6.9.1 PyQt6-WebEngine-Qt6==6.10.1
PyQt6_sip==13.10.2 PyQt6_sip==13.10.2
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -2,3 +2,7 @@ PyQt6
PyQt6-Qt6 PyQt6-Qt6
PyQt6-WebEngine PyQt6-WebEngine
PyQt6-WebEngine-Qt6 PyQt6-WebEngine-Qt6
# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2025-October/046347.html
#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -1,7 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.9.0 PyQt6==6.10.1
PyQt6-Qt6==6.9.1 PyQt6-Qt6==6.10.1
PyQt6-WebEngine==6.9.0 PyQt6-WebEngine==6.10.0
PyQt6-WebEngine-Qt6==6.9.1 PyQt6-WebEngine-Qt6==6.10.1
PyQt6_sip==13.10.2 PyQt6_sip==13.10.2
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -2,3 +2,7 @@ PyQt6
PyQt6-Qt6 PyQt6-Qt6
PyQt6-WebEngine PyQt6-WebEngine
PyQt6-WebEngine-Qt6 PyQt6-WebEngine-Qt6
# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2025-October/046347.html
#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -1,17 +1,18 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
build==1.2.2.post1 build==1.3.0
certifi==2025.4.26 certifi==2025.11.12
charset-normalizer==3.4.2 charset-normalizer==3.4.4
docutils==0.21.2 check-manifest==0.51
idna==3.10 docutils==0.22.3
idna==3.11
importlib_metadata==8.7.0 importlib_metadata==8.7.0
packaging==25.0 packaging==25.0
Pygments==2.19.1 Pygments==2.19.2
pyproject_hooks==1.2.0 pyproject_hooks==1.2.0
pyroma==4.2 pyroma==5.0.1
requests==2.32.3 requests==2.32.5
tomli==2.2.1 tomli==2.3.0
trove-classifiers==2025.5.9.12 trove-classifiers==2025.12.1.14
urllib3==2.4.0 urllib3==2.6.2
zipp==3.22.0 zipp==3.23.0

View File

@ -1 +1,2 @@
pyroma pyroma
check-manifest

View File

@ -2,17 +2,17 @@
alabaster==0.7.16 alabaster==0.7.16
babel==2.17.0 babel==2.17.0
certifi==2025.4.26 certifi==2025.11.12
charset-normalizer==3.4.2 charset-normalizer==3.4.4
docutils==0.21.2 docutils==0.21.2
idna==3.10 idna==3.11
imagesize==1.4.1 imagesize==1.4.1
importlib_metadata==8.7.0 importlib_metadata==8.7.0
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.2 MarkupSafe==3.0.3
packaging==25.0 packaging==25.0
Pygments==2.19.1 Pygments==2.19.2
requests==2.32.3 requests==2.32.5
snowballstemmer==3.0.1 snowballstemmer==3.0.1
Sphinx==7.4.7 Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0 sphinxcontrib-applehelp==2.0.0
@ -21,6 +21,6 @@ sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1 sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0 sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0 sphinxcontrib-serializinghtml==2.0.0
tomli==2.2.1 tomli==2.3.0
urllib3==2.4.0 urllib3==2.6.2
zipp==3.22.0 zipp==3.23.0

View File

@ -1,23 +1,23 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==25.3.0 attrs==25.4.0
autocommand==2.2.2 autocommand==2.2.2
backports.tarfile==1.2.0 backports.tarfile==1.2.0
beautifulsoup4==4.13.4 beautifulsoup4==4.14.3
blinker==1.9.0 blinker==1.9.0
certifi==2025.4.26 certifi==2025.11.12
charset-normalizer==3.4.2 charset-normalizer==3.4.4
cheroot==10.0.1 cheroot==11.1.2
click==8.1.8 click==8.1.8
coverage==7.8.2 coverage==7.10.7
exceptiongroup==1.3.0 exceptiongroup==1.3.1
execnet==2.1.1 execnet==2.1.2
filelock==3.18.0 filelock==3.19.1
Flask==3.1.1 Flask==3.1.2
gherkin-official==29.0.0 gherkin-official==29.0.0
hunter==3.7.0 hunter==3.9.0
hypothesis==6.135.0 hypothesis==6.141.1
idna==3.10 idna==3.11
importlib_metadata==8.7.0 importlib_metadata==8.7.0
importlib_resources==6.5.2 importlib_resources==6.5.2
inflect==7.3.1 inflect==7.3.1
@ -25,43 +25,43 @@ iniconfig==2.1.0
itsdangerous==2.2.0 itsdangerous==2.2.0
jaraco.collections==5.1.0 jaraco.collections==5.1.0
jaraco.context==6.0.1 jaraco.context==6.0.1
jaraco.functools==4.1.0 jaraco.functools==4.0.1
jaraco.text==3.12.1 jaraco.text==3.12.1
# Jinja2==3.1.6 # Jinja2==3.1.6
Mako==1.3.10 Mako==1.3.10
manhole==1.8.1 manhole==1.8.1
# MarkupSafe==3.0.2 # MarkupSafe==3.0.3
more-itertools==10.7.0 more-itertools==10.8.0
packaging==25.0 packaging==25.0
parse==1.20.2 parse==1.20.2
parse_type==0.6.4 parse_type==0.6.6
pillow==11.2.1 pillow==11.3.0
platformdirs==4.3.8 platformdirs==4.4.0
pluggy==1.6.0 pluggy==1.6.0
py-cpuinfo==9.0.0 py-cpuinfo==9.0.0
Pygments==2.19.1 Pygments==2.19.2
pytest==8.4.0 pytest==8.4.2
pytest-bdd==8.1.0 pytest-bdd==8.1.0
pytest-benchmark==5.1.0 pytest-benchmark==5.2.3
pytest-cov==6.1.1 pytest-cov==7.0.0
pytest-instafail==0.5.0 pytest-instafail==0.5.0
pytest-mock==3.14.1 pytest-mock==3.15.1
pytest-qt==4.4.0 pytest-qt==4.5.0
pytest-repeat==0.9.4 pytest-repeat==0.9.4
pytest-rerunfailures==15.1 pytest-rerunfailures==16.0.1
pytest-xdist==3.7.0 pytest-xdist==3.8.0
pytest-xvfb==3.1.1 pytest-xvfb==3.1.1
PyVirtualDisplay==3.0 PyVirtualDisplay==3.0
requests==2.32.3 requests==2.32.5
requests-file==2.1.0 requests-file==3.0.1
six==1.17.0 six==1.17.0
sortedcontainers==2.4.0 sortedcontainers==2.4.0
soupsieve==2.7 soupsieve==2.8
tldextract==5.3.0 tldextract==5.3.0
tomli==2.2.1 tomli==2.3.0
typeguard==4.3.0 typeguard==4.3.0
typing_extensions==4.14.0 typing_extensions==4.15.0
urllib3==2.4.0 urllib3==2.6.2
vulture==2.14 vulture==2.14
Werkzeug==3.1.3 Werkzeug==3.1.4
zipp==3.22.0 zipp==3.23.0

View File

@ -1,19 +1,19 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
cachetools==6.0.0 cachetools==6.2.3
chardet==5.2.0 chardet==5.2.0
colorama==0.4.6 colorama==0.4.6
distlib==0.3.9 distlib==0.4.0
filelock==3.18.0 filelock==3.19.1
packaging==25.0 packaging==25.0
pip==25.1.1 pip==25.3
platformdirs==4.3.8 platformdirs==4.4.0
pluggy==1.6.0 pluggy==1.6.0
pyproject-api==1.9.1 pyproject-api==1.9.1
setuptools==80.9.0 setuptools==80.9.0
tomli==2.2.1 tomli==2.3.0
tox==4.26.0 ; python_full_version!="3.14.0b1" tox==4.30.3 ; python_full_version!="3.14.0b1"
typing_extensions==4.14.0 typing_extensions==4.15.0
virtualenv==20.31.2 virtualenv==20.35.4
wheel==0.45.1 wheel==0.45.1
tox @ git+https://github.com/tox-dev/tox ; python_full_version=="3.14.0b1" tox @ git+https://github.com/tox-dev/tox ; python_full_version=="3.14.0b1"

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
tomli==2.2.1 tomli==2.3.0
vulture==2.14 vulture==2.14

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
pathspec==0.12.1 pathspec==0.12.1
PyYAML==6.0.2 PyYAML==6.0.3
yamllint==1.37.1 yamllint==1.37.1

View File

@ -143,7 +143,7 @@ no_entries_found() {
# expected to write the username of that entry to the $username variable and # expected to write the username of that entry to the $username variable and
# the corresponding password to $password # the corresponding password to $password
# shellcheck disable=SC2317 # shellcheck disable=SC2329
reset_backend() { reset_backend() {
init() { true ; } init() { true ; }
query_entries() { true ; } query_entries() { true ; }
@ -199,7 +199,8 @@ choose_entry_zenity() {
} }
choose_entry_zenity_radio() { choose_entry_zenity_radio() {
zenity_helper() { # shellcheck disable=SC2317 # shellcheck disable=SC2329
zenity_helper() {
awk '{ print $0 ; print $0 }' \ awk '{ print $0 ; print $0 }' \
| zenity --list --radiolist \ | zenity --list --radiolist \
--title "qutebrowser password fill" \ --title "qutebrowser password fill" \
@ -279,7 +280,7 @@ pass_backend() {
# ======================================================= # =======================================================
# backend: secret # backend: secret
# shellcheck disable=SC2317 # shellcheck disable=SC2329
secret_backend() { secret_backend() {
init() { init() {
return return

View File

@ -23,7 +23,7 @@ create_menu() {
done < "$QUTE_CONFIG_DIR"/bookmarks/urls done < "$QUTE_CONFIG_DIR"/bookmarks/urls
# Finally history # Finally history
printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')" printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory ORDER BY last_atime DESC')"
} }
get_selection() { get_selection() {

12
pyproject.toml Normal file
View File

@ -0,0 +1,12 @@
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.check-manifest]
ignore = [
"qutebrowser/git-commit-id",
"qutebrowser/html/doc",
"qutebrowser/html/doc/*",
"qutebrowser/html/doc/img/cheatsheet-*.png",
"*/__pycache__",
]

View File

@ -1,4 +1,5 @@
[pytest] [pytest]
pythonpath = .
log_level = NOTSET log_level = NOTSET
addopts = --strict-markers --strict-config --instafail --benchmark-columns=Min,Max,Median addopts = --strict-markers --strict-config --instafail --benchmark-columns=Min,Max,Median
testpaths = tests testpaths = tests
@ -42,8 +43,8 @@ markers =
qt6_only: Tests which should only run with Qt 6 qt6_only: Tests which should only run with Qt 6
qt5_xfail: Tests which fail with Qt 5 qt5_xfail: Tests which fail with Qt 5
qt6_xfail: Tests which fail with Qt 6 qt6_xfail: Tests which fail with Qt 6
qt69_ci_flaky: Tests which are flaky with Qt 6.9 on CI qt69_ci_flaky: Tests which are flaky with Qt 6.9+ on CI
qt69_ci_skip: Tests which should be skipped with Qt 6.9 on CI qt69_ci_skip: Tests which should be skipped with Qt 6.9+ on CI
qt_log_level_fail = WARNING qt_log_level_fail = WARNING
qt_log_ignore = qt_log_ignore =
# GitHub Actions # GitHub Actions
@ -91,6 +92,9 @@ qt_log_ignore =
^Unable to detect GPU vendor\.$ ^Unable to detect GPU vendor\.$
# Qt 5 on CI with WebKit # Qt 5 on CI with WebKit
^qglx_findConfig: Failed to finding matching FBConfig for QSurfaceFormat\(version 2\.0, options QFlags<QSurfaceFormat::FormatOption>\(\), depthBufferSize -1, redBufferSize 1, greenBufferSize 1, blueBufferSize 1, alphaBufferSize -1, stencilBufferSize -1, samples -1, swapBehavior QSurfaceFormat::SingleBuffer, swapInterval 1, colorSpace QSurfaceFormat::DefaultColorSpace, profile QSurfaceFormat::NoProfile\)$ ^qglx_findConfig: Failed to finding matching FBConfig for QSurfaceFormat\(version 2\.0, options QFlags<QSurfaceFormat::FormatOption>\(\), depthBufferSize -1, redBufferSize 1, greenBufferSize 1, blueBufferSize 1, alphaBufferSize -1, stencilBufferSize -1, samples -1, swapBehavior QSurfaceFormat::SingleBuffer, swapInterval 1, colorSpace QSurfaceFormat::DefaultColorSpace, profile QSurfaceFormat::NoProfile\)$
# Qt 6.8+ debug build
# https://github.com/qutebrowser/qutebrowser/issues/8069#issuecomment-2017644465
^QObject::connect: Connecting from COMPAT signal \(QWebEnginePage::featurePermissionRequest(ed|Canceled)\(QUrl,QWebEnginePage::Feature\)\)
xfail_strict = true xfail_strict = true
filterwarnings = filterwarnings =
error error
@ -98,8 +102,6 @@ filterwarnings =
# https://github.com/cucumber/gherkin/commit/2f4830093149eae7ff7bd82f683b3d3bb7320d39 # https://github.com/cucumber/gherkin/commit/2f4830093149eae7ff7bd82f683b3d3bb7320d39
# https://github.com/pytest-dev/pytest-bdd/issues/752 # https://github.com/pytest-dev/pytest-bdd/issues/752
ignore:'maxsplit' is passed as positional argument:DeprecationWarning:gherkin.gherkin_line ignore:'maxsplit' is passed as positional argument:DeprecationWarning:gherkin.gherkin_line
# https://github.com/pytest-dev/pytest-mock/issues/468
ignore:'asyncio\.iscoroutinefunction' is deprecated and slated for removal:DeprecationWarning:pytest_mock.plugin
# https://github.com/ionelmc/pytest-benchmark/issues/283 # https://github.com/ionelmc/pytest-benchmark/issues/283
ignore:FileType is deprecated\. Simply open files after parsing arguments\.:PendingDeprecationWarning:pytest_benchmark.plugin ignore:FileType is deprecated\. Simply open files after parsing arguments\.:PendingDeprecationWarning:pytest_benchmark.plugin
faulthandler_timeout = 90 faulthandler_timeout = 90

View File

@ -14,7 +14,7 @@ __copyright__ = "Copyright 2013-{} Florian Bruhin (The Compiler)".format(_year)
__license__ = "GPL-3.0-or-later" __license__ = "GPL-3.0-or-later"
__maintainer__ = __author__ __maintainer__ = __author__
__email__ = "mail@qutebrowser.org" __email__ = "mail@qutebrowser.org"
__version__ = "3.5.0" __version__ = "3.6.3"
__version_info__ = tuple(int(part) for part in __version__.split('.')) __version_info__ = tuple(int(part) for part in __version__.split('.'))
__description__ = "A keyboard-driven, vim-like browser based on Python and Qt." __description__ = "A keyboard-driven, vim-like browser based on Python and Qt."

View File

@ -29,7 +29,7 @@ if TYPE_CHECKING:
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
urlutils, message, jinja, version) urlutils, message, jinja)
from qutebrowser.misc import miscwidgets, objects, sessions from qutebrowser.misc import miscwidgets, objects, sessions
from qutebrowser.browser import eventfilter, inspector from qutebrowser.browser import eventfilter, inspector
from qutebrowser.qt import sip from qutebrowser.qt import sip
@ -1177,37 +1177,6 @@ class AbstractTab(QWidget):
navigation.url.errorString())) navigation.url.errorString()))
navigation.accepted = False navigation.accepted = False
# WORKAROUND for QtWebEngine >= 6.2 not allowing form requests from
# qute:// to outside domains.
needs_load_workarounds = (
objects.backend == usertypes.Backend.QtWebEngine and
version.qtwebengine_versions().webengine >= utils.VersionNumber(6, 2)
)
if (
needs_load_workarounds and
self.url() == QUrl("qute://start/") and
navigation.navigation_type == navigation.Type.form_submitted and
navigation.url.matches(
QUrl(config.val.url.searchengines['DEFAULT']),
urlutils.FormatOption.REMOVE_QUERY)
):
log.webview.debug(
"Working around qute://start loading issue for "
f"{navigation.url.toDisplayString()}")
navigation.accepted = False
self.load_url(navigation.url)
if (
needs_load_workarounds and
self.url() == QUrl("qute://bookmarks/") and
navigation.navigation_type == navigation.Type.back_forward
):
log.webview.debug(
"Working around qute://bookmarks loading issue for "
f"{navigation.url.toDisplayString()}")
navigation.accepted = False
self.load_url(navigation.url)
@pyqtSlot(bool) @pyqtSlot(bool)
def _on_load_finished(self, ok: bool) -> None: def _on_load_finished(self, ok: bool) -> None:
assert self._widget is not None assert self._widget is not None

View File

@ -10,7 +10,7 @@ from qutebrowser.qt.gui import QKeyEvent
from qutebrowser.qt.widgets import QWidget from qutebrowser.qt.widgets import QWidget
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import log, message, usertypes, qtutils from qutebrowser.utils import log, message, usertypes, qtutils, version, utils
from qutebrowser.keyinput import modeman, keyutils from qutebrowser.keyinput import modeman, keyutils
@ -55,16 +55,16 @@ class ChildEventFilter(QObject):
# - This is a child event filter on a tab (self._widget is not None) # - This is a child event filter on a tab (self._widget is not None)
# - We find an old existing child which is a QQuickWidget and is # - We find an old existing child which is a QQuickWidget and is
# currently focused. # currently focused.
# - We're using QtWebEngine >= 6.4 (older versions are not affected) # - We're using an affected QtWebEngine version
children = [ children = [
c for c in self._widget.findChildren( c for c in self._widget.findChildren(
QWidget, "", Qt.FindChildOption.FindDirectChildrenOnly) QWidget, "", Qt.FindChildOption.FindDirectChildrenOnly)
if c is not child and if c is not child and
c.hasFocus() and c.hasFocus() and
c.metaObject() is not None and c.metaObject() is not None and
c.metaObject().className() == "QQuickWidget" c.metaObject().className() == "QQuickWidget" # Qt 6.4+
] ]
if children: if children and version.qtwebengine_versions().webengine < utils.VersionNumber(6, 6, 3):
log.misc.debug("Focusing new child") log.misc.debug("Focusing new child")
child.setFocus() child.setFocus()

View File

@ -123,25 +123,24 @@ def data_for_url(url: QUrl) -> tuple[str, bytes]:
path = url.path() path = url.path()
host = url.host() host = url.host()
query = url.query()
# A url like "qute:foo" is split as "scheme:path", not "scheme:host". # A url like "qute:foo" is split as "scheme:path", not "scheme:host".
log.misc.debug("url: {}, path: {}, host {}".format( log.misc.debug("url: {}, path: {}, host {}".format(
url.toDisplayString(), path, host)) url.toDisplayString(), path, host))
if not path or not host:
new_url = QUrl()
new_url.setScheme('qute')
# When path is absent, e.g. qute://help (with no trailing slash)
if host:
new_url.setHost(host)
# When host is absent, e.g. qute:help
else:
new_url.setHost(path)
if not host:
# Redirect qute:help -> qute://help/
new_url = QUrl(url)
new_url.setHost(path)
new_url.setPath('/') new_url.setPath('/')
if query: if not new_url.host(): # Valid path but not valid host
new_url.setQuery(query) raise UrlInvalidError(f"Invalid host (from path): {path!r}")
if new_url.host(): # path was a valid host raise Redirect(new_url)
raise Redirect(new_url)
if not path:
# Redirect qute://help -> qute://help/
new_url = QUrl(url)
new_url.setPath('/')
raise Redirect(new_url)
try: try:
handler = _HANDLERS[host] handler = _HANDLERS[host]

View File

@ -417,6 +417,37 @@ def _init_profile(profile: QWebEngineProfile) -> None:
lambda url: profile.clearVisitedLinks([url])) lambda url: profile.clearVisitedLinks([url]))
_global_settings.init_settings() _global_settings.init_settings()
_maybe_disable_hangouts_extension(profile)
def _maybe_disable_hangouts_extension(profile: QWebEngineProfile) -> None:
"""Disable the Hangouts extension for Qt 6.10+."""
if not config.val.qt.workarounds.disable_hangouts_extension:
return
if machinery.IS_QT6: # mypy
try:
ext_manager = profile.extensionManager()
except AttributeError:
return # added in QtWebEngine 6.10
qtwe_versions = version.qtwebengine_versions(avoid_init=True)
if (
qtwe_versions.webengine == utils.VersionNumber(6, 10, 1)
and profile.isOffTheRecord()
):
# WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/8785
log.misc.warning(
"Not disabling Hangouts extension on private profile to avoid "
"QtWebEngine crash with Qt 6.10.1")
return
assert ext_manager is not None # mypy
for info in ext_manager.extensions():
if info.id() == pakjoy.HANGOUTS_EXT_ID:
log.misc.debug(f"Disabling extension: {info.name()}")
# setExtensionEnabled(info, False) seems to segfault
ext_manager.unloadExtension(info)
def _clear_webengine_permissions_json(): def _clear_webengine_permissions_json():
@ -438,14 +469,18 @@ def _clear_webengine_permissions_json():
) )
def default_qt_profile() -> QWebEngineProfile:
"""Get the default profile from Qt."""
if machinery.IS_QT6:
return QWebEngineProfile("Default")
else:
return QWebEngineProfile.defaultProfile()
def _init_default_profile(): def _init_default_profile():
"""Init the default QWebEngineProfile.""" """Init the default QWebEngineProfile."""
global default_profile global default_profile
default_profile = default_qt_profile()
if machinery.IS_QT6:
default_profile = QWebEngineProfile("Default")
else:
default_profile = QWebEngineProfile.defaultProfile()
assert not default_profile.isOffTheRecord() assert not default_profile.isOffTheRecord()
assert parsed_user_agent is None # avoid earlier profile initialization assert parsed_user_agent is None # avoid earlier profile initialization
@ -506,7 +541,21 @@ def _init_site_specific_quirks():
# "{qt_key}/{qt_version} " # "{qt_key}/{qt_version} "
# "{upstream_browser_key}/{upstream_browser_version_short} " # "{upstream_browser_key}/{upstream_browser_version_short} "
# "Safari/{webkit_version}") # "Safari/{webkit_version}")
firefox_ua = "Mozilla/5.0 ({os_info}; rv:136.0) Gecko/20100101 Firefox/136.0" firefox_ua = "Mozilla/5.0 ({os_info}; rv:145.0) Gecko/20100101 Firefox/145.0"
# Needed for gitlab.gnome.org which blocks old Chromium versions outright,
# except when QtWebEngine/... is in the UA.
#
# We could further modify the UA to just "qutebrowser" or something so we don't get
# Anubis at all, but it looks like their Anubis triggers to more than just
# Mozilla/5.0 (also AppleWebKit/... and Chromium/... possibly?), so at that point
# I'm not sure if we can strip down the UA so much without breaking
# something in GitLab as well.
not_mozilla_ua = (
"Mozilla/5.0 ({os_info}) AppleWebKit/{webkit_version} (KHTML, like Gecko) "
"{qt_key}/{qt_version} {upstream_browser_key}/{upstream_browser_version_short} "
"Safari/{webkit_version}"
)
def maybe_newer_chrome_ua(at_least_version): def maybe_newer_chrome_ua(at_least_version):
"""Return a new UA if our current chrome version isn't at least at_least_version.""" """Return a new UA if our current chrome version isn't at least at_least_version."""
@ -528,6 +577,7 @@ def _init_site_specific_quirks():
# to keep your account secure" error. # to keep your account secure" error.
# https://github.com/qutebrowser/qutebrowser/issues/5182 # https://github.com/qutebrowser/qutebrowser/issues/5182
("ua-google", "https://accounts.google.com/*", firefox_ua), ("ua-google", "https://accounts.google.com/*", firefox_ua),
("ua-gnome-gitlab", "https://gitlab.gnome.org/*", not_mozilla_ua),
] ]
for name, pattern, ua in user_agents: for name, pattern, ua in user_agents:

View File

@ -13,7 +13,7 @@ import html as html_utils
from typing import cast, Union, Optional from typing import cast, Union, Optional
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl, from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
QObject, QByteArray) QObject, QByteArray, QTimer)
from qutebrowser.qt.network import QAuthenticator from qutebrowser.qt.network import QAuthenticator
from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory
@ -940,6 +940,10 @@ class _WebEnginePermissions(QObject):
notif = miscwidgets.FullscreenNotification(self._widget) notif = miscwidgets.FullscreenNotification(self._widget)
notif.set_timeout(timeout) notif.set_timeout(timeout)
notif.show() notif.show()
# Restore keyboard focus to the tab. Setting a NoFocus policy
# for FullscreenNotification doesn't seem to work.
if self._widget.isVisible():
self._widget.setFocus()
@pyqtSlot(QUrl, 'QWebEnginePage::Feature') @pyqtSlot(QUrl, 'QWebEnginePage::Feature')
def _on_feature_permission_requested(self, url, feature): def _on_feature_permission_requested(self, url, feature):
@ -1619,6 +1623,7 @@ class WebEngineTab(browsertab.AbstractTab):
def _on_navigation_request(self, navigation): def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation) super()._on_navigation_request(navigation)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103778
local_schemes = {"qute", "file"} local_schemes = {"qute", "file"}
qtwe_ver = version.qtwebengine_versions().webengine qtwe_ver = version.qtwebengine_versions().webengine
if ( if (
@ -1631,7 +1636,6 @@ class WebEngineTab(browsertab.AbstractTab):
(utils.VersionNumber(6, 2) <= qtwe_ver < utils.VersionNumber(6, 2, 5) or (utils.VersionNumber(6, 2) <= qtwe_ver < utils.VersionNumber(6, 2, 5) or
utils.VersionNumber(6, 3) <= qtwe_ver < utils.VersionNumber(6, 3, 1)) utils.VersionNumber(6, 3) <= qtwe_ver < utils.VersionNumber(6, 3, 1))
): ):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103778
log.webview.debug( log.webview.debug(
"Working around blocked request from local page " "Working around blocked request from local page "
f"{self.url().toDisplayString()}" f"{self.url().toDisplayString()}"
@ -1639,6 +1643,51 @@ class WebEngineTab(browsertab.AbstractTab):
navigation.accepted = False navigation.accepted = False
self.load_url(navigation.url) self.load_url(navigation.url)
# WORKAROUND for QtWebEngine >= 6.2 not allowing form requests from
# qute:// to outside domains.
if (
qtwe_ver >= utils.VersionNumber(6, 2) and
self.url() == QUrl("qute://start/") and
navigation.navigation_type == navigation.Type.form_submitted and
navigation.url.matches(
QUrl(config.val.url.searchengines['DEFAULT']),
urlutils.FormatOption.REMOVE_QUERY)
):
log.webview.debug(
"Working around qute://start loading issue for "
f"{navigation.url.toDisplayString()}")
navigation.accepted = False
# Using QTimer.singleShot as WORKAROUND for this crashing otherwise
# with QtWebEngine 6.10: https://bugreports.qt.io/browse/QTBUG-140543
QTimer.singleShot(0, functools.partial(self.load_url, navigation.url))
# WORKAROUND for QtWebEngine 6.2 - 6.5 blocking back/forward navigation too
if (
utils.VersionNumber(6, 6) > qtwe_ver >= utils.VersionNumber(6, 2) and
self.url() == QUrl("qute://bookmarks/") and
navigation.navigation_type == navigation.Type.back_forward
):
log.webview.debug(
"Working around qute://bookmarks loading issue for "
f"{navigation.url.toDisplayString()}")
navigation.accepted = False
self.load_url(navigation.url)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-140515
ua_setting = "content.headers.user_agent"
if (
navigation.accepted
and config.instance.get(ua_setting, navigation.url, fallback=False)
is not usertypes.UNSET
and navigation.navigation_type == usertypes.NavigationRequest.Type.redirect
and navigation.is_main_frame
and utils.VersionNumber(6, 5) <= qtwe_ver < utils.VersionNumber(6, 10, 1)
):
navigation.accepted = False
# Using QTimer.singleShot as WORKAROUND for this crashing otherwise
# with QtWebEngine 6.10: https://bugreports.qt.io/browse/QTBUG-140543
QTimer.singleShot(0, functools.partial(self.load_url, navigation.url))
if not navigation.accepted or not navigation.is_main_frame: if not navigation.accepted or not navigation.is_main_frame:
return return

View File

@ -333,8 +333,13 @@ class Config(QObject):
pattern, hide_userconfig=hide_userconfig) pattern, hide_userconfig=hide_userconfig)
self.changed.emit(opt.name) self.changed.emit(opt.name)
log.config.debug("Config option changed: {} = {}".format(
opt.name, value)) if pattern is not None:
log.config.debug("Config option changed: {} = {} for {}".format(
opt.name, value, pattern))
else:
log.config.debug("Config option changed: {} = {}".format(
opt.name, value))
def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None: def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None:
"""Make sure the given option may be set in autoconfig.yml.""" """Make sure the given option may be set in autoconfig.yml."""

View File

@ -356,9 +356,8 @@ class ConfigCommands:
raise cmdutils.CommandError(":config-list-remove can only be used " raise cmdutils.CommandError(":config-list-remove can only be used "
"for lists") "for lists")
converted = opt.typ.valtype.from_str(value)
with self._handle_config_error(): with self._handle_config_error():
converted = opt.typ.valtype.from_str(value)
option_value = self._config.get_mutable_obj(option) option_value = self._config.get_mutable_obj(option)
if converted not in option_value: if converted not in option_value:

View File

@ -391,7 +391,7 @@ qt.workarounds.disable_accelerated_2d_canvas:
name: String name: String
valid_values: valid_values:
- always: Disable accelerated 2d canvas - always: Disable accelerated 2d canvas
- auto: Disable on Qt6 < 6.6.0, enable otherwise - auto: Disable on Qt versions with known issues, enable otherwise
- never: Enable accelerated 2d canvas - never: Enable accelerated 2d canvas
default: auto default: auto
backend: QtWebEngine backend: QtWebEngine
@ -422,6 +422,19 @@ qt.workarounds.disable_hangouts_extension:
disabled to avoid crashes on Qt 6.5.0 to 6.5.3 if dark mode is enabled, disabled to avoid crashes on Qt 6.5.0 to 6.5.3 if dark mode is enabled,
as well as on Qt 6.6.0. as well as on Qt 6.6.0.
qt.workarounds.disable_accessibility:
type:
name: String
valid_values:
- always: Disable renderer accessibility
- auto: Disable on Qt versions with known issues, enable otherwise
- never: Enable renderer accessibility
default: auto
backend: QtWebEngine
restart: true
desc: >-
Disable accessibility to avoid crashes on Qt 6.10.1.
## auto_save ## auto_save
auto_save.interval: auto_save.interval:
@ -658,6 +671,7 @@ content.site_specific_quirks.skip:
valid_values: valid_values:
- ua-google - ua-google
- ua-googledocs - ua-googledocs
- ua-gnome-gitlab
- js-whatsapp-web - js-whatsapp-web
- js-discord - js-discord
- js-string-replaceall - js-string-replaceall
@ -772,14 +786,14 @@ content.headers.user_agent:
# Vim-protip: Place your cursor below this comment and run # Vim-protip: Place your cursor below this comment and run
# :r!python scripts/dev/ua_fetch.py # :r!python scripts/dev/ua_fetch.py
- - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
- Chrome 135 macOS - Chrome 142 macOS
- - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/135.0.0.0 Safari/537.36" like Gecko) Chrome/142.0.0.0 Safari/537.36"
- Chrome 135 Win10 - Chrome 142 Win10
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like - - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/135.0.0.0 Safari/537.36" Gecko) Chrome/142.0.0.0 Safari/537.36"
- Chrome 135 Linux - Chrome 142 Linux
supports_pattern: true supports_pattern: true
desc: | desc: |
User agent to send. User agent to send.
@ -1602,6 +1616,7 @@ fileselect.single_file.command:
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"] - ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
- ['["xterm", "-e", "fff", "-p", "{}"]', "fff in xterm"] - ['["xterm", "-e", "fff", "-p", "{}"]', "fff in xterm"]
- ['["xterm", "-e", "lf", "-selection-path", "{}"]', "lf in xterm"] - ['["xterm", "-e", "lf", "-selection-path", "{}"]', "lf in xterm"]
- ['["xterm", "-e", "yazi", "--chooser-file", "{}"]', "yazi in xterm"]
default: ['xterm', '-e', 'ranger', '--choosefile={}'] default: ['xterm', '-e', 'ranger', '--choosefile={}']
desc: >- desc: >-
Command (and arguments) to use for selecting a single file in forms. Command (and arguments) to use for selecting a single file in forms.
@ -1622,6 +1637,7 @@ fileselect.multiple_files.command:
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"] - ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
- ['["xterm", "-e", "fff", "-p", "{}"]', "fff in xterm"] - ['["xterm", "-e", "fff", "-p", "{}"]', "fff in xterm"]
- ['["xterm", "-e", "lf", "-selection-path", "{}"]', "lf in xterm"] - ['["xterm", "-e", "lf", "-selection-path", "{}"]', "lf in xterm"]
- ['["xterm", "-e", "yazi", "--chooser-file", "{}"]', "yazi in xterm"]
default: ['xterm', '-e', 'ranger', '--choosefiles={}'] default: ['xterm', '-e', 'ranger', '--choosefiles={}']
desc: >- desc: >-
Command (and arguments) to use for selecting multiple files in forms. Command (and arguments) to use for selecting multiple files in forms.
@ -1641,6 +1657,7 @@ fileselect.folder.command:
- ['["xterm", "-e", "ranger", "--choosedir={}"]', "Ranger in xterm"] - ['["xterm", "-e", "ranger", "--choosedir={}"]', "Ranger in xterm"]
- ['["xterm", "-e", "vifm", "--choose-dir", "{}"]', "vifm in xterm"] - ['["xterm", "-e", "vifm", "--choose-dir", "{}"]', "vifm in xterm"]
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"] - ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
- ['["xterm", "-e", "yazi", "--cwd-file", "{}"]', "yazi in xterm"]
default: ['xterm', '-e', 'ranger', '--choosedir={}'] default: ['xterm', '-e', 'ranger', '--choosedir={}']
desc: >- desc: >-
Command (and arguments) to use for selecting a single folder in forms. Command (and arguments) to use for selecting a single folder in forms.

View File

@ -156,12 +156,12 @@ def _qtwebengine_features( # noqa: C901
if versions.webengine >= utils.VersionNumber(6, 7): if versions.webengine >= utils.VersionNumber(6, 7):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-132681 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-132681
# TODO adjust if fixed in Qt 6.8.2/.3 or 6.9.0/.1 # TODO adjust if fixed in Qt 6.9.2+
disabled_features.append('DocumentPictureInPictureAPI') disabled_features.append('DocumentPictureInPictureAPI')
if versions.webengine == utils.VersionNumber(6, 9): if utils.VersionNumber(6, 9) <= versions.webengine < utils.VersionNumber(6, 10, 1):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-135787 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-135787
# TODO adjust if still present in 6.9.1 # and https://bugreports.qt.io/browse/QTBUG-141096
disabled_features.append('PermissionElement') disabled_features.append('PermissionElement')
if not config.val.input.media_keys: if not config.val.input.media_keys:
@ -356,7 +356,21 @@ _WEBENGINE_SETTINGS: dict[str, dict[Any, Optional[_SettingValueType]]] = {
'qt.workarounds.disable_accelerated_2d_canvas': { 'qt.workarounds.disable_accelerated_2d_canvas': {
'always': '--disable-accelerated-2d-canvas', 'always': '--disable-accelerated-2d-canvas',
'never': None, 'never': None,
'auto': lambda _versions: '--disable-accelerated-2d-canvas' if machinery.IS_QT6 else None, 'auto': lambda versions: '--disable-accelerated-2d-canvas'
if machinery.IS_QT6
and versions.webengine
and versions.webengine < utils.VersionNumber(6, 8, 2)
else None,
},
'qt.workarounds.disable_accessibility': {
'always': '--disable-renderer-accessibility',
'never': None,
# WORKAROUND for https://qt-project.atlassian.net/browse/QTBUG-142320
'auto': lambda versions: '--disable-renderer-accessibility'
if machinery.IS_QT6
and versions.webengine
and versions.webengine == utils.VersionNumber(6, 10, 1)
else None,
}, },
} }

View File

@ -92,7 +92,7 @@ li {
the required packages for pdf.js are also installed. the required packages for pdf.js are also installed.
<br/> <br/>
The package is named The package is named
<a href="https://archlinux.org/packages/community/any/pdfjs/"><b>pdfjs</b></a> on Archlinux <a href="https://archlinux.org/packages/extra/any/pdfjs-legacy/"><b>pdfjs-legacy</b></a> on Archlinux
and <a href="https://packages.debian.org/bullseye/libjs-pdf"><b>libjs-pdf</b></a> on Debian. and <a href="https://packages.debian.org/bullseye/libjs-pdf"><b>libjs-pdf</b></a> on Debian.
</li> </li>

View File

@ -191,36 +191,51 @@ window._qutebrowser.webelem = (function() {
} }
} }
funcs.find_css = (selector, only_visible) => { // Recursively finds elements from DOM that have a shadowRoot
let elems; // and returns the shadow roots in a list
function find_shadow_roots(container = document) {
const roots = [];
try { for (const elem of container.querySelectorAll("*")) {
elems = document.querySelectorAll(selector); if (elem.shadowRoot) {
} catch (ex) { roots.push(elem.shadowRoot, ...find_shadow_roots(elem.shadowRoot));
return {"success": false, "error": ex.toString()};
}
const subelem_frames = window.frames;
const out = [];
for (let i = 0; i < elems.length; ++i) {
if (!only_visible || is_visible(elems[i])) {
out.push(serialize_elem(elems[i]));
} }
} }
// Recurse into frames and add them return roots;
for (let i = 0; i < subelem_frames.length; i++) { }
if (iframe_same_domain(subelem_frames[i])) {
const frame = subelem_frames[i]; funcs.find_css = (selector, only_visible) => {
const subelems = frame.document. // Find all places where we need to look for elements:
querySelectorAll(selector); const containers = [[document, null]];
for (let elem_num = 0; elem_num < subelems.length; ++elem_num) { // Same-domain iframes
if (!only_visible || for (const frame of Array.from(window.frames)) {
is_visible(subelems[elem_num], frame)) { if (iframe_same_domain(frame)) {
out.push(serialize_elem(subelems[elem_num], frame)); containers.push([frame.document, frame]);
} }
}
// Open shadow roots
for (const root of find_shadow_roots()) {
containers.push([root, null]);
}
// Then find elements in all of them
const elems = [];
for (const [container, frame] of containers) {
try {
for (const elem of container.querySelectorAll(selector)) {
elems.push([elem, frame]);
} }
} catch (ex) {
return {"success": false, "error": ex.toString()};
}
}
// Finally, filter by visibility
const out = [];
for (const [elem, frame] of elems) {
if (!only_visible || is_visible(elem, frame)) {
out.push(serialize_elem(elem, frame));
} }
} }

View File

@ -562,7 +562,7 @@ class MainWindow(QWidget):
self._completion.on_clear_completion_selection) self._completion.on_clear_completion_selection)
self.status.cmd.hide_completion.connect( self.status.cmd.hide_completion.connect(
self._completion.hide) self._completion.hide)
self.status.cmd.hide_cmd.connect(self.tabbed_browser.on_release_focus) self.status.release_focus.connect(self.tabbed_browser.on_release_focus)
def _set_decoration(self, hidden): def _set_decoration(self, hidden):
"""Set the visibility of the window decoration via Qt.""" """Set the visibility of the window decoration via Qt."""

View File

@ -342,9 +342,9 @@ class PromptContainer(QWidget):
"""Leave KEY_MODE whenever a prompt is aborted.""" """Leave KEY_MODE whenever a prompt is aborted."""
try: try:
modeman.leave(self._win_id, key_mode, 'aborted', maybe=True) modeman.leave(self._win_id, key_mode, 'aborted', maybe=True)
except objreg.RegistryUnavailableError: except (objreg.RegistryUnavailableError, RuntimeError):
# window was deleted: ignore # window was deleted: ignore
pass log.prompt.debug(f"Ignoring leaving {key_mode} as window was deleted")
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def _on_prompt_done(self, key_mode): def _on_prompt_done(self, key_mode):
@ -654,6 +654,12 @@ class FilenamePrompt(_BasePrompt):
"""A prompt for a filename.""" """A prompt for a filename."""
# Note: This *must* be a class variable! If it's not, for unknown reasons,
# we get a segfault in Qt/PyQt in QFileInfoGatherer::getInfo() if we have
# nested download prompts (i.e. trigger a download while a download prompt
# is open already).
_null_icon_provider = NullIconProvider()
def __init__(self, question, parent=None): def __init__(self, question, parent=None):
super().__init__(question, parent) super().__init__(question, parent)
self._init_texts(question) self._init_texts(question)
@ -753,7 +759,7 @@ class FilenamePrompt(_BasePrompt):
self._file_model = QFileSystemModel(self) self._file_model = QFileSystemModel(self)
# avoid icon and mime type lookups, they are slow in Qt6 # avoid icon and mime type lookups, they are slow in Qt6
self._file_model.setIconProvider(NullIconProvider()) self._file_model.setIconProvider(self._null_icon_provider)
self._file_view.setModel(self._file_model) self._file_view.setModel(self._file_model)
self._file_view.clicked.connect(self._insert_path) self._file_view.clicked.connect(self._insert_path)

View File

@ -140,10 +140,12 @@ class StatusBar(QWidget):
moved: Emitted when the statusbar has moved, so the completion widget moved: Emitted when the statusbar has moved, so the completion widget
can move to the right position. can move to the right position.
arg: The new position. arg: The new position.
release_focus: Emitted just before the statusbar is hidden.
""" """
resized = pyqtSignal('QRect') resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint') moved = pyqtSignal('QPoint')
release_focus = pyqtSignal()
STYLESHEET = _generate_stylesheet() STYLESHEET = _generate_stylesheet()
@ -365,6 +367,7 @@ class StatusBar(QWidget):
def _hide_cmd_widget(self): def _hide_cmd_widget(self):
"""Show temporary text instead of command widget.""" """Show temporary text instead of command widget."""
log.statusbar.debug("Hiding cmd widget") log.statusbar.debug("Hiding cmd widget")
self.release_focus.emit()
self._stack.setCurrentWidget(self.txt) self._stack.setCurrentWidget(self.txt)
self.maybe_hide() self.maybe_hide()

View File

@ -422,6 +422,37 @@ class _BackendProblemChecker:
raise utils.Unreachable raise utils.Unreachable
def _force_wayland_hardware_acceleration(self) -> None:
"""Set environment variable so hardware acceleration works on Wayland.
Set EGL_PLATFORM=wayland to force ANGLE to obtain EGL display connection
for wayland platform. Otherwise, the display connection for
EGL_DEFAULT_DISPLAY may belong to a platform which Nvidia's EGL driver
doesn't support. In case of unsupported platform, EGL may fallback to
Mesa software renderer (LLVMPipe) disabling hardware acceleration in
Chromium.
Equivalent to:
https://codereview.qt-project.org/c/qt/qtwebengine/+/663568
"""
if objects.qapp.platformName() != 'wayland':
return
versions = version.qtwebengine_versions(avoid_init=True)
if versions.webengine >= utils.VersionNumber(6, 10):
# Qt workaround is active
return
egl_platform_var = "EGL_PLATFORM"
egl_platform = os.environ.get(egl_platform_var)
if not egl_platform:
os.environ[egl_platform_var] = "wayland"
elif egl_platform != "wayland":
log.init.warning(
f"{egl_platform_var} environment variable is set to {egl_platform!r}. "
"This may break hardware rendering on Wayland."
)
def _assert_backend(self, backend: usertypes.Backend) -> None: def _assert_backend(self, backend: usertypes.Backend) -> None:
assert objects.backend == backend, objects.backend assert objects.backend == backend, objects.backend
@ -433,6 +464,7 @@ class _BackendProblemChecker:
self._handle_ssl_support() self._handle_ssl_support()
self._handle_serviceworker_nuking() self._handle_serviceworker_nuking()
self._check_software_rendering() self._check_software_rendering()
self._force_wayland_hardware_acceleration()
self._confirm_chromium_version_changes() self._confirm_chromium_version_changes()
else: else:
self._assert_backend(usertypes.Backend.QtWebKit) self._assert_backend(usertypes.Backend.QtWebKit)

View File

@ -48,8 +48,8 @@ def parse_fatal_stacktrace(text):
lines = [ lines = [
r'(?P<type>Fatal Python error|Windows fatal exception): (?P<msg>.*)', r'(?P<type>Fatal Python error|Windows fatal exception): (?P<msg>.*)',
r' *', r' *',
r'(Current )?[Tt]hread [^ ]* \(most recent call first\): *', r'(Current )?[Tt]hread .* \(most recent call first\): *',
r' File ".*", line \d+ in (?P<func>.*)', r' (File ".*", line \d+ in (?P<func>.*)|<no Python frame>)',
] ]
m = re.search('\n'.join(lines), text) m = re.search('\n'.join(lines), text)
if m is None: if m is None:
@ -58,7 +58,7 @@ def parse_fatal_stacktrace(text):
else: else:
msg = m.group('msg') msg = m.group('msg')
typ = m.group('type') typ = m.group('type')
func = m.group('func') func = m.group('func') or ''
if typ == 'Windows fatal exception': if typ == 'Windows fatal exception':
msg = 'Windows ' + msg msg = 'Windows ' + msg
return msg, func return msg, func

View File

@ -23,6 +23,7 @@ import datetime
from typing import NoReturn from typing import NoReturn
try: try:
import tkinter import tkinter
import tkinter.messagebox
except ImportError: except ImportError:
tkinter = None # type: ignore[assignment] tkinter = None # type: ignore[assignment]

View File

@ -26,6 +26,7 @@ instead of crashing.
""" """
import os import os
import sys
import shutil import shutil
import pathlib import pathlib
import dataclasses import dataclasses
@ -35,9 +36,13 @@ from collections.abc import Iterator
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.misc import binparsing, objects from qutebrowser.misc import binparsing, objects
from qutebrowser.qt import core
from qutebrowser.utils import qtutils, standarddir, version, utils, log, message from qutebrowser.utils import qtutils, standarddir, version, utils, log, message
from qutebrowser.qt.webenginecore import QWebEngineProfile
HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome"
HANGOUTS_EXT_ID = "nkeimhogjdpnpccoofpliimaahmaaome"
HANGOUTS_MARKER = f"// Extension ID: {HANGOUTS_EXT_ID}".encode("utf-8")
HANGOUTS_IDS = [ HANGOUTS_IDS = [
# Linux # Linux
47222, # QtWebEngine 6.9 Beta 3 47222, # QtWebEngine 6.9 Beta 3
@ -57,7 +62,11 @@ PAK_VERSION = 5
RESOURCES_ENV_VAR = "QTWEBENGINE_RESOURCES_PATH" RESOURCES_ENV_VAR = "QTWEBENGINE_RESOURCES_PATH"
DISABLE_ENV_VAR = "QUTE_DISABLE_PAKJOY" DISABLE_ENV_VAR = "QUTE_DISABLE_PAKJOY"
CACHE_DIR_NAME = "webengine_resources_pak_quirk" CACHE_DIR_NAME = "webengine_resources_pak_quirk"
PAK_FILENAME = "qtwebengine_resources.pak" PAK_FILENAME = (
"qtwebengine_resources.debug.pak"
if core.QLibraryInfo.isDebugBuild()
else "qtwebengine_resources.pak"
)
TARGET_URL = b"https://*.google.com/*" TARGET_URL = b"https://*.google.com/*"
REPLACEMENT_URL = b"https://qute.invalid/*" REPLACEMENT_URL = b"https://qute.invalid/*"
@ -222,7 +231,7 @@ def copy_webengine_resources() -> Optional[pathlib.Path]:
) )
# https://github.com/qutebrowser/qutebrowser/issues/8257 # https://github.com/qutebrowser/qutebrowser/issues/8257
or config.val.qt.workarounds.disable_hangouts_extension or config.val.qt.workarounds.disable_hangouts_extension
): ) or hasattr(QWebEngineProfile, "extensionManager"): # Qt 6.10+
# No patching needed # No patching needed
return None return None
@ -303,3 +312,16 @@ def patch_webengine() -> Iterator[None]:
del os.environ[RESOURCES_ENV_VAR] del os.environ[RESOURCES_ENV_VAR]
else: else:
os.environ[RESOURCES_ENV_VAR] = old_value os.environ[RESOURCES_ENV_VAR] = old_value
def main() -> None:
with open(sys.argv[1], "rb") as f:
parser = PakParser(f)
print(parser.manifest.decode("utf-8"))
print()
print(f"entry: {parser.manifest_entry}")
print(f"URL offset: {parser.find_patch_offset()}")
if __name__ == "__main__":
main()

325
qutebrowser/misc/wmname.py Normal file
View File

@ -0,0 +1,325 @@
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Utilities to get the name of the window manager (X11) / compositor (Wayland)."""
from typing import NewType
from collections.abc import Iterator
import ctypes
import socket
import struct
import pathlib
import dataclasses
import contextlib
import ctypes.util
class Error(Exception):
"""Base class for errors in this module."""
class _WaylandDisplayStruct(ctypes.Structure):
pass
_WaylandDisplay = NewType("_WaylandDisplay", "ctypes._Pointer[_WaylandDisplayStruct]")
def _load_library(name: str) -> ctypes.CDLL:
lib = ctypes.util.find_library(name)
if lib is None:
raise Error(f"{name} library not found")
try:
return ctypes.CDLL(lib)
except OSError as e:
raise Error(f"Failed to load {name} library: {e}")
def _pid_from_fd(fd: int) -> int:
"""Get the process ID from a file descriptor using SO_PEERCRED.
https://stackoverflow.com/a/35827184
"""
if not hasattr(socket, "SO_PEERCRED"):
raise Error("Missing socket.SO_PEERCRED")
# struct ucred {
# pid_t pid;
# uid_t uid;
#  gid_t gid;
# }; // where all of those are integers
ucred_format = "3i"
ucred_size = struct.calcsize(ucred_format)
try:
sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
except OSError as e:
raise Error(f"Error creating socket for fd {fd}: {e}")
try:
ucred = sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, ucred_size)
except OSError as e:
raise Error(f"Error getting SO_PEERCRED for fd {fd}: {e}")
finally:
sock.close()
pid, _uid, _gid = struct.unpack(ucred_format, ucred)
return pid
def _process_name_from_pid(pid: int) -> str:
"""Get the process name from a PID by reading /proc/[pid]/cmdline."""
proc_path = pathlib.Path(f"/proc/{pid}/cmdline")
try:
return proc_path.read_text(encoding="utf-8").replace("\0", " ").strip()
except OSError as e:
raise Error(f"Error opening {proc_path}: {e}")
@contextlib.contextmanager
def _wayland_display(wayland_client: ctypes.CDLL) -> Iterator[_WaylandDisplay]:
"""Context manager to connect to a Wayland display."""
wayland_client.wl_display_connect.argtypes = [ctypes.c_char_p] # name
wayland_client.wl_display_connect.restype = ctypes.POINTER(_WaylandDisplayStruct)
wayland_client.wl_display_disconnect.argtypes = [
ctypes.POINTER(_WaylandDisplayStruct)
]
wayland_client.wl_display_disconnect.restype = None
display = wayland_client.wl_display_connect(None)
if not display:
raise Error("Can't connect to display")
try:
yield display
finally:
wayland_client.wl_display_disconnect(display)
def _wayland_get_fd(wayland_client: ctypes.CDLL, display: _WaylandDisplay) -> int:
"""Get the file descriptor for the Wayland display."""
wayland_client.wl_display_get_fd.argtypes = [ctypes.POINTER(_WaylandDisplayStruct)]
wayland_client.wl_display_get_fd.restype = ctypes.c_int
fd = wayland_client.wl_display_get_fd(display)
if fd < 0:
raise Error(f"Failed to get Wayland display file descriptor: {fd}")
return fd
def wayland_compositor_name() -> str:
"""Get the name of the running Wayland compositor.
Approach based on:
https://stackoverflow.com/questions/69302630/wayland-client-get-compositor-name
"""
wayland_client = _load_library("wayland-client")
with _wayland_display(wayland_client) as display:
fd = _wayland_get_fd(wayland_client, display)
pid = _pid_from_fd(fd)
process_name = _process_name_from_pid(pid)
return process_name
@dataclasses.dataclass
class _X11Atoms:
NET_SUPPORTING_WM_CHECK: int
NET_WM_NAME: int
UTF8_STRING: int
class _X11DisplayStruct(ctypes.Structure):
pass
_X11Display = NewType("_X11Display", "ctypes._Pointer[_X11DisplayStruct]")
_X11Window = NewType("_X11Window", int)
@contextlib.contextmanager
def _x11_open_display(xlib: ctypes.CDLL) -> Iterator[_X11Display]:
"""Open a connection to the X11 display."""
xlib.XOpenDisplay.argtypes = [ctypes.c_char_p]
xlib.XOpenDisplay.restype = ctypes.POINTER(_X11DisplayStruct)
xlib.XCloseDisplay.argtypes = [ctypes.POINTER(_X11DisplayStruct)]
xlib.XCloseDisplay.restype = None
display = xlib.XOpenDisplay(None)
if not display:
raise Error("Cannot open display")
try:
yield display
finally:
xlib.XCloseDisplay(display)
def _x11_intern_atom(
xlib: ctypes.CDLL, display: _X11Display, name: bytes, only_if_exists: bool = True
) -> int:
"""Call xlib's XInternAtom function."""
xlib.XInternAtom.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_char_p, # Atom name
ctypes.c_int, # Only if exists (bool)
]
xlib.XInternAtom.restype = ctypes.c_ulong
atom = xlib.XInternAtom(display, name, only_if_exists)
if atom == 0:
raise Error(f"Failed to intern atom: {name!r}")
return atom
@contextlib.contextmanager
def _x11_get_window_property(
xlib: ctypes.CDLL,
display: _X11Display,
*,
window: _X11Window,
prop: int,
req_type: int,
length: int,
offset: int = 0,
delete: bool = False,
) -> Iterator[tuple["ctypes._Pointer[ctypes.c_ubyte]", ctypes.c_ulong]]:
"""Call xlib's XGetWindowProperty function."""
ret_actual_type = ctypes.c_ulong()
ret_actual_format = ctypes.c_int()
ret_nitems = ctypes.c_ulong()
ret_bytes_after = ctypes.c_ulong()
ret_prop = ctypes.POINTER(ctypes.c_ubyte)()
xlib.XGetWindowProperty.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_ulong, # Window
ctypes.c_ulong, # Property
ctypes.c_long, # Offset
ctypes.c_long, # Length
ctypes.c_int, # Delete (bool)
ctypes.c_ulong, # Required type (Atom)
ctypes.POINTER(ctypes.c_ulong), # return: Actual type (Atom)
ctypes.POINTER(ctypes.c_int), # return: Actual format
ctypes.POINTER(ctypes.c_ulong), # return: Number of items
ctypes.POINTER(ctypes.c_ulong), # return: Bytes after
ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte)), # return: Property value
]
xlib.XGetWindowProperty.restype = ctypes.c_int
result = xlib.XGetWindowProperty(
display,
window,
prop,
offset,
length,
delete,
req_type,
ctypes.byref(ret_actual_type),
ctypes.byref(ret_actual_format),
ctypes.byref(ret_nitems),
ctypes.byref(ret_bytes_after),
ctypes.byref(ret_prop),
)
if result != 0:
raise Error(f"XGetWindowProperty for {prop} failed: {result}")
if not ret_prop:
raise Error(f"Property {prop} is NULL")
if ret_actual_type.value != req_type:
raise Error(
f"Expected type {req_type}, got {ret_actual_type.value} for property {prop}"
)
if ret_bytes_after.value != 0:
raise Error(
f"Expected no bytes after property {prop}, got {ret_bytes_after.value}"
)
try:
yield ret_prop, ret_nitems
finally:
xlib.XFree(ret_prop)
def _x11_get_wm_window(
xlib: ctypes.CDLL, display: _X11Display, *, atoms: _X11Atoms
) -> _X11Window:
"""Get the _NET_SUPPORTING_WM_CHECK window."""
xlib.XDefaultScreen.argtypes = [ctypes.POINTER(_X11DisplayStruct)]
xlib.XDefaultScreen.restype = ctypes.c_int
xlib.XRootWindow.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_int, # Screen number
]
xlib.XRootWindow.restype = ctypes.c_ulong
screen = xlib.XDefaultScreen(display)
root_window = xlib.XRootWindow(display, screen)
with _x11_get_window_property(
xlib,
display,
window=root_window,
prop=atoms.NET_SUPPORTING_WM_CHECK,
req_type=33, # XA_WINDOW
length=1,
) as (prop, _nitems):
win = ctypes.cast(prop, ctypes.POINTER(ctypes.c_ulong)).contents.value
return _X11Window(win)
def _x11_get_wm_name(
xlib: ctypes.CDLL,
display: _X11Display,
*,
atoms: _X11Atoms,
wm_window: _X11Window,
) -> str:
"""Get the _NET_WM_NAME property of the window manager."""
with _x11_get_window_property(
xlib,
display,
window=wm_window,
prop=atoms.NET_WM_NAME,
req_type=atoms.UTF8_STRING,
length=1024, # somewhat arbitrary
) as (prop, nitems):
if nitems.value <= 0:
raise Error(f"{nitems.value} items found in _NET_WM_NAME property")
wm_name = ctypes.string_at(prop, nitems.value).decode("utf-8")
if not wm_name:
raise Error("Window manager name is empty")
return wm_name
def x11_wm_name() -> str:
"""Get the name of the running X11 window manager."""
xlib = _load_library("X11")
with _x11_open_display(xlib) as display:
atoms = _X11Atoms(
NET_SUPPORTING_WM_CHECK=_x11_intern_atom(
xlib, display, b"_NET_SUPPORTING_WM_CHECK"
),
NET_WM_NAME=_x11_intern_atom(xlib, display, b"_NET_WM_NAME"),
UTF8_STRING=_x11_intern_atom(xlib, display, b"UTF8_STRING"),
)
wm_window = _x11_get_wm_window(xlib, display, atoms=atoms)
return _x11_get_wm_name(xlib, display, atoms=atoms, wm_window=wm_window)
if __name__ == "__main__":
try:
wayland_name = wayland_compositor_name()
print(f"Wayland compositor name: {wayland_name}")
except Error as e:
print(f"Wayland error: {e}")
try:
x11_name = x11_wm_name()
print(f"X11 window manager name: {x11_name}")
except Error as e:
print(f"X11 error: {e}")

View File

@ -44,7 +44,7 @@ except ImportError: # pragma: no cover
import qutebrowser import qutebrowser
from qutebrowser.utils import (log, utils, standarddir, usertypes, message, resources, from qutebrowser.utils import (log, utils, standarddir, usertypes, message, resources,
qtutils) qtutils)
from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf, wmname
from qutebrowser.browser import pdfjs from qutebrowser.browser import pdfjs
from qutebrowser.config import config from qutebrowser.config import config
if TYPE_CHECKING: if TYPE_CHECKING:
@ -322,8 +322,8 @@ class ModuleInfo:
except (ImportError, ValueError): except (ImportError, ValueError):
self._installed = False self._installed = False
return return
else:
self._installed = True self._installed = True
for attribute_name in self._version_attributes: for attribute_name in self._version_attributes:
if hasattr(module, attribute_name): if hasattr(module, attribute_name):
@ -332,6 +332,13 @@ class ModuleInfo:
self._version = str(version) self._version = str(version)
break break
if self._version is None:
try:
self._version = importlib.metadata.version(self.name)
except importlib.metadata.PackageNotFoundError:
log.misc.debug(f"{self.name} not found")
self._version = None
self._initialized = True self._initialized = True
def get_version(self) -> Optional[str]: def get_version(self) -> Optional[str]:
@ -372,7 +379,7 @@ class ModuleInfo:
version = self.get_version() version = self.get_version()
if version is None: if version is None:
return f'{self.name}: yes' return f'{self.name}: unknown'
text = f'{self.name}: {version}' text = f'{self.name}: {version}'
if self.is_outdated(): if self.is_outdated():
@ -383,7 +390,7 @@ class ModuleInfo:
def _create_module_info() -> dict[str, ModuleInfo]: def _create_module_info() -> dict[str, ModuleInfo]:
packages = [ packages = [
('colorama', ['VERSION', '__version__']), ('colorama', ['VERSION', '__version__']),
('jinja2', ['__version__']), ('jinja2', []),
('pygments', ['__version__']), ('pygments', ['__version__']),
('yaml', ['__version__']), ('yaml', ['__version__']),
('adblock', ['__version__'], "0.3.2"), ('adblock', ['__version__'], "0.3.2"),
@ -552,6 +559,7 @@ class WebEngineVersions:
118: '118.0.5993.220', # 2024-01-25, Qt 6.7 118: '118.0.5993.220', # 2024-01-25, Qt 6.7
122: '122.0.6261.171', # 2024-04-15, Qt 6.8 122: '122.0.6261.171', # 2024-04-15, Qt 6.8
130: '130.0.6723.192', # 2025-01-06, Qt 6.9 130: '130.0.6723.192', # 2025-01-06, Qt 6.9
134: '134.0.6998.208', # 2025-04-16, Qt 6.10
} }
# Dates based on https://chromereleases.googleblog.com/ # Dates based on https://chromereleases.googleblog.com/
@ -645,6 +653,12 @@ class WebEngineVersions:
## Qt 6.9 ## Qt 6.9
utils.VersionNumber(6, 9): (_BASES[130], '133.0.6943.141'), # 2025-02-25 utils.VersionNumber(6, 9): (_BASES[130], '133.0.6943.141'), # 2025-02-25
utils.VersionNumber(6, 9, 1): (_BASES[130], '136.0.7103.114'), # 2025-05-13 utils.VersionNumber(6, 9, 1): (_BASES[130], '136.0.7103.114'), # 2025-05-13
utils.VersionNumber(6, 9, 2): (_BASES[130], '139.0.7258.67'), # 2025-07-29
utils.VersionNumber(6, 9, 3): (_BASES[130], '140.0.7339.207'), # 2025-09-22
## Qt 6.10
utils.VersionNumber(6, 10): (_BASES[134], '140.0.7339.207'), # 2025-09-22
utils.VersionNumber(6, 10, 1): (_BASES[134], '142.0.7444.162'), # 2025-11-11
} }
def __post_init__(self) -> None: def __post_init__(self) -> None:
@ -913,6 +927,51 @@ def _backend() -> str:
raise utils.Unreachable(objects.backend) raise utils.Unreachable(objects.backend)
def _webengine_extensions() -> Sequence[str]:
"""Get a list of WebExtensions enabled in QtWebEngine."""
lines: list[str] = []
if (
objects.backend == usertypes.Backend.QtWebEngine
and machinery.IS_QT6 # mypy; TODO early return once Qt 5 is dropped
):
from qutebrowser.browser.webengine import webenginesettings
lines.append("WebExtensions:")
if webenginesettings.default_profile:
profile = webenginesettings.default_profile
elif "avoid-chromium-init" in objects.debug_flags:
lines[0] += " unknown (avoiding init)"
return lines
else:
profile = webenginesettings.default_qt_profile()
try:
ext_manager = profile.extensionManager()
except AttributeError:
# Added in QtWebEngine 6.10
return []
assert ext_manager is not None # mypy
if not ext_manager.extensions():
lines[0] += " none"
for info in ext_manager.extensions():
tags = [
("[x]" if info.isEnabled() else "[ ]") + " enabled",
("[x]" if info.isLoaded() else "[ ]") + " loaded",
("[x]" if info.isInstalled() else "[ ]") + " installed",
]
lines.append(f" {info.name()} ({info.id()})")
lines.append(f" {' '.join(tags)}")
lines.append(f" {info.path()}")
url = info.actionPopupUrl()
if url.isValid():
lines.append(f" {url.toDisplayString()}")
lines.append("")
return lines
def _uptime() -> datetime.timedelta: def _uptime() -> datetime.timedelta:
time_delta = datetime.datetime.now() - objects.qapp.launch_time time_delta = datetime.datetime.now() - objects.qapp.launch_time
# Round off microseconds # Round off microseconds
@ -962,13 +1021,15 @@ def version_info() -> str:
if QSslSocket.supportsSsl() else 'no'), if QSslSocket.supportsSsl() else 'no'),
] ]
lines += _webengine_extensions()
if objects.qapp: if objects.qapp:
style = objects.qapp.style() style = objects.qapp.style()
assert style is not None assert style is not None
metaobj = style.metaObject() metaobj = style.metaObject()
assert metaobj is not None assert metaobj is not None
lines.append('Style: {}'.format(metaobj.className())) lines.append('Style: {}'.format(metaobj.className()))
lines.append('Platform plugin: {}'.format(objects.qapp.platformName())) lines.append('Qt Platform: {}'.format(gui_platform_info()))
lines.append('OpenGL: {}'.format(opengl_info())) lines.append('OpenGL: {}'.format(opengl_info()))
importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__)) importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__))
@ -1137,6 +1198,19 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
old_context.makeCurrent(old_surface) old_context.makeCurrent(old_surface)
def gui_platform_info() -> str:
"""Get the Qt GUI platform name, optionally with the WM/compositor name."""
info = objects.qapp.platformName()
try:
if info == "xcb":
info += f" ({wmname.x11_wm_name()})"
elif info in ["wayland", "wayland-egl"]:
info += f" ({wmname.wayland_compositor_name()})"
except wmname.Error as e:
info += f" (Error: {e})"
return info
def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None: def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None:
"""Pastebin the version and log the url to messages.""" """Pastebin the version and log the url to messages."""
def _yank_url(url: str) -> None: def _yank_url(url: str) -> None:

View File

@ -3,9 +3,9 @@
adblock==0.6.0 adblock==0.6.0
colorama==0.4.6 colorama==0.4.6
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.2 MarkupSafe==3.0.3
Pygments==2.19.1 Pygments==2.19.2
PyYAML==6.0.2 PyYAML==6.0.3
# Unpinned due to recompile_requirements.py limitations # Unpinned due to recompile_requirements.py limitations
pyobjc-core ; sys_platform=="darwin" pyobjc-core ; sys_platform=="darwin"
pyobjc-framework-Cocoa ; sys_platform=="darwin" pyobjc-framework-Cocoa ; sys_platform=="darwin"

View File

@ -6,6 +6,7 @@
"""Build a new release.""" """Build a new release."""
from __future__ import annotations
import os import os
import sys import sys
@ -20,7 +21,8 @@ import platform
import collections import collections
import dataclasses import dataclasses
import re import re
from typing import Optional import http
from typing import Optional, TYPE_CHECKING
from collections.abc import Iterable from collections.abc import Iterable
try: try:
@ -28,6 +30,12 @@ try:
except ImportError: except ImportError:
pass pass
if TYPE_CHECKING:
import github3
import github3.repos.release
import requests
REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT)) sys.path.insert(0, str(REPO_ROOT))
@ -126,7 +134,7 @@ def _smoke_test_run(
return subprocess.run(argv, check=True, capture_output=True) return subprocess.run(argv, check=True, capture_output=True)
def smoke_test(executable: pathlib.Path, debug: bool) -> None: def smoke_test(executable: pathlib.Path, debug_build: bool) -> None:
"""Try starting the given qutebrowser executable.""" """Try starting the given qutebrowser executable."""
stdout_whitelist = [] stdout_whitelist = []
stderr_whitelist = [ stderr_whitelist = [
@ -176,6 +184,10 @@ def smoke_test(executable: pathlib.Path, debug: bool) -> None:
# Qt 6.9 on macOS # Qt 6.9 on macOS
r'Compositor returned null texture', r'Compositor returned null texture',
# Qt 6.10
(r'\[.*:ERROR:service_utils.cc\([0-9]*\)\] '
r'Skia Graphite backend = "" not found - falling back to Ganesh!'),
]) ])
elif IS_WINDOWS: elif IS_WINDOWS:
stderr_whitelist.extend([ stderr_whitelist.extend([
@ -184,10 +196,27 @@ def smoke_test(executable: pathlib.Path, debug: bool) -> None:
(r'\[.*:ERROR:dxva_video_decode_accelerator_win.cc\(\d+\)\] ' (r'\[.*:ERROR:dxva_video_decode_accelerator_win.cc\(\d+\)\] '
r'DXVAVDA fatal error: could not LoadLibrary: .*: The specified ' r'DXVAVDA fatal error: could not LoadLibrary: .*: The specified '
r'module could not be found. \(0x7E\)'), r'module could not be found. \(0x7E\)'),
# Qt 6.10
(r'\[.*:ERROR:direct_composition_support.cc\([0-9]*\)\] '
r'GetGpuDriverOverlayInfo: Failed to retrieve video device'),
(r'\[.*:ERROR:direct_composition_support.cc\([0-9]*\)\] QueryInterface '
r'to IDCompositionDevice4 failed: No such interface supported '
r'\(0x80004002\)'),
]) ])
proc = _smoke_test_run(executable) try:
if debug: proc = _smoke_test_run(executable)
except subprocess.CalledProcessError as e:
print(f"Smoke test failed: {e}, running with --debug")
smoke_test_debug(
executable,
original_stdout=e.stdout.decode("utf-8"),
original_stderr=e.stderr.decode("utf-8"),
issue_description=str(e),
)
return
if debug_build:
print("Skipping output check for debug build") print("Skipping output check for debug build")
return return
@ -196,48 +225,64 @@ def smoke_test(executable: pathlib.Path, debug: bool) -> None:
if stdout or stderr: if stdout or stderr:
print("Unexpected output, running with --debug") print("Unexpected output, running with --debug")
proc = _smoke_test_run(executable, '--debug') smoke_test_debug(
debug_stdout = proc.stdout.decode('utf-8') executable,
debug_stderr = proc.stderr.decode('utf-8') original_stdout=stdout,
original_stderr=stderr,
issue_description="Unexpected output",
)
lines = [
"Unexpected output!", def smoke_test_debug(
executable: pathlib.Path,
*,
original_stdout: str,
original_stderr: str,
issue_description: str,
) -> None:
"""Run smoke test in debug mode to get more output."""
proc = _smoke_test_run(executable, '--debug')
debug_stdout = proc.stdout.decode('utf-8')
debug_stderr = proc.stderr.decode('utf-8')
lines = [
issue_description,
"",
]
if original_stdout:
lines += [
"stdout",
"------",
"",
original_stdout,
"",
]
if original_stderr:
lines += [
"stderr",
"------",
"",
original_stderr,
"",
]
if debug_stdout:
lines += [
"debug rerun stdout",
"------------------",
"",
debug_stdout,
"",
]
if debug_stderr:
lines += [
"debug rerun stderr",
"------------------",
"",
debug_stderr,
"", "",
] ]
if stdout:
lines += [
"stdout",
"------",
"",
stdout,
"",
]
if stderr:
lines += [
"stderr",
"------",
"",
stderr,
"",
]
if debug_stdout:
lines += [
"debug rerun stdout",
"------------------",
"",
debug_stdout,
"",
]
if debug_stderr:
lines += [
"debug rerun stderr",
"------------------",
"",
debug_stderr,
"",
]
raise Exception("\n".join(lines)) # pylint: disable=broad-exception-raised raise Exception("\n".join(lines)) # pylint: disable=broad-exception-raised
def verify_windows_exe(exe_path: pathlib.Path) -> None: def verify_windows_exe(exe_path: pathlib.Path) -> None:
@ -293,7 +338,7 @@ def build_mac(
dist_path = pathlib.Path("dist") dist_path = pathlib.Path("dist")
utils.print_title("Running pre-dmg smoke test") utils.print_title("Running pre-dmg smoke test")
smoke_test(_mac_bin_path(dist_path), debug=debug) smoke_test(_mac_bin_path(dist_path), debug_build=debug)
if skip_packaging: if skip_packaging:
return [] return []
@ -316,7 +361,7 @@ def build_mac(
subprocess.run(['hdiutil', 'attach', dmg_path, subprocess.run(['hdiutil', 'attach', dmg_path,
'-mountpoint', tmp_path], check=True) '-mountpoint', tmp_path], check=True)
try: try:
smoke_test(_mac_bin_path(tmp_path), debug=debug) smoke_test(_mac_bin_path(tmp_path), debug_build=debug)
finally: finally:
print("Waiting 10s for dmg to be detachable...") print("Waiting 10s for dmg to be detachable...")
time.sleep(10) time.sleep(10)
@ -375,7 +420,7 @@ def _build_windows_single(
verify_windows_exe(exe_path) verify_windows_exe(exe_path)
utils.print_title("Running smoke test") utils.print_title("Running smoke test")
smoke_test(exe_path, debug=debug) smoke_test(exe_path, debug_build=debug)
if skip_packaging: if skip_packaging:
return [] return []
@ -551,11 +596,36 @@ def read_github_token(
return token return token
def _github_find_release(
gh: github3.GitHub, tag: str, experimental: bool
) -> github3.repos.release.Release:
if experimental:
repo = gh.repository('qutebrowser', 'experiments')
else:
repo = gh.repository('qutebrowser', 'qutebrowser')
assert repo is not None
for release in repo.releases():
if release.tag_name == tag:
return release
releases = ", ".join(r.tag_name for r in repo.releases())
raise Exception( # pylint: disable=broad-exception-raised
f"No release found for {tag!r} in {repo.full_name}, found: {releases}")
def _github_assets(
release: github3.repos.release.Release, artifact: Artifact
) -> list[github3.repos.release.Asset]:
return [asset for asset in release.assets() if asset.name == artifact.path.name]
def github_upload( def github_upload(
artifacts: list[Artifact], artifacts: list[Artifact],
tag: str, tag: str,
gh_token: str, gh_token: str,
experimental: bool, experimental: bool,
skip_if_exists: bool,
) -> None: ) -> None:
"""Upload the given artifacts to GitHub. """Upload the given artifacts to GitHub.
@ -564,35 +634,25 @@ def github_upload(
tag: The name of the release tag tag: The name of the release tag
gh_token: The GitHub token to use gh_token: The GitHub token to use
experimental: Upload to the experiments repo experimental: Upload to the experiments repo
skip_if_exists: Skip uploading artifacts that already exist
""" """
# pylint: disable=broad-exception-raised
import github3 import github3
import github3.exceptions import github3.exceptions
utils.print_title("Uploading to github...") utils.print_title("Uploading to github...")
gh = github3.login(token=gh_token) gh = github3.login(token=gh_token)
assert gh is not None
if experimental: release = _github_find_release(gh=gh, tag=tag, experimental=experimental)
repo = gh.repository('qutebrowser', 'experiments')
else:
repo = gh.repository('qutebrowser', 'qutebrowser')
release = None # to satisfy pylint
for release in repo.releases():
if release.tag_name == tag:
break
else:
releases = ", ".join(r.tag_name for r in repo.releases())
raise Exception(
f"No release found for {tag!r} in {repo.full_name}, found: {releases}")
for artifact in artifacts: for artifact in artifacts:
if _github_assets(release, artifact) and skip_if_exists:
print(f"Artifact {artifact.path.name} already exists, skipping")
continue
while True: while True:
print(f"Uploading {artifact.path}") print(f"Uploading {artifact.path}")
assets = [asset for asset in release.assets() if (assets := _github_assets(release, artifact)):
if asset.name == artifact.path.name]
if assets:
print(f"Assets already exist: {assets}") print(f"Assets already exist: {assets}")
if utils.ON_CI: if utils.ON_CI:
@ -620,9 +680,7 @@ def github_upload(
print("Retrying!") print("Retrying!")
assets = [asset for asset in release.assets() if (assets := _github_assets(release, artifact)):
if asset.name == artifact.path.name]
if assets:
stray_asset = assets[0] stray_asset = assets[0]
print(f"Deleting stray asset {stray_asset.name}") print(f"Deleting stray asset {stray_asset.name}")
stray_asset.delete() stray_asset.delete()
@ -630,12 +688,29 @@ def github_upload(
break break
def pypi_upload(artifacts: list[Artifact], experimental: bool) -> None: def check_pypi_exists(version: str) -> bool:
"""Check whether the given version exists on PyPI."""
response = requests.get(
f"https://pypi.org/pypi/qutebrowser/{version}/json", timeout=30
)
if response.status_code == http.HTTPStatus.NOT_FOUND:
return False
response.raise_for_status()
return bool(response.json()["urls"])
def pypi_upload(
artifacts: list[Artifact], experimental: bool, skip_if_exists: bool
) -> None:
"""Upload the given artifacts to PyPI using twine.""" """Upload the given artifacts to PyPI using twine."""
utils.print_title("Uploading to PyPI...")
if skip_if_exists and check_pypi_exists(qutebrowser.__version__):
print(f"Version {qutebrowser.__version__} already exists on PyPI, skipping")
return
# https://blog.pypi.org/posts/2023-05-23-removing-pgp/ # https://blog.pypi.org/posts/2023-05-23-removing-pgp/
artifacts = [a for a in artifacts if a.mimetype != 'application/pgp-signature'] artifacts = [a for a in artifacts if a.mimetype != 'application/pgp-signature']
utils.print_title("Uploading to PyPI...")
if experimental: if experimental:
run_twine('upload', artifacts, "-r", "testpypi") run_twine('upload', artifacts, "-r", "testpypi")
else: else:
@ -661,6 +736,8 @@ def main() -> None:
nargs='?') nargs='?')
parser.add_argument('--upload', action='store_true', required=False, parser.add_argument('--upload', action='store_true', required=False,
help="Toggle to upload the release to GitHub.") help="Toggle to upload the release to GitHub.")
parser.add_argument('--reupload', action='store_true', required=False,
help="Skip uploading artifacts that already exist.")
parser.add_argument('--no-confirm', action='store_true', required=False, parser.add_argument('--no-confirm', action='store_true', required=False,
help="Skip confirmation before uploading.") help="Skip confirmation before uploading.")
parser.add_argument('--skip-packaging', action='store_true', required=False, parser.add_argument('--skip-packaging', action='store_true', required=False,
@ -720,9 +797,16 @@ def main() -> None:
assert gh_token is not None assert gh_token is not None
github_upload( github_upload(
artifacts, version_tag, gh_token=gh_token, experimental=args.experimental) artifacts,
version_tag,
gh_token=gh_token,
experimental=args.experimental,
skip_if_exists=args.reupload,
)
if upload_to_pypi: if upload_to_pypi:
pypi_upload(artifacts, experimental=args.experimental) pypi_upload(
artifacts, experimental=args.experimental, skip_if_exists=args.reupload
)
else: else:
print() print()
utils.print_title("Artifacts") utils.print_title("Artifacts")

View File

@ -23,6 +23,7 @@
"Mako": "https://docs.makotemplates.org/en/latest/changelog.html", "Mako": "https://docs.makotemplates.org/en/latest/changelog.html",
"hypothesis": "https://hypothesis.readthedocs.io/en/latest/changes.html", "hypothesis": "https://hypothesis.readthedocs.io/en/latest/changes.html",
"mypy": "https://github.com/python/mypy/blob/master/CHANGELOG.md", "mypy": "https://github.com/python/mypy/blob/master/CHANGELOG.md",
"librt": "https://github.com/mypyc/librt/commits/master/",
"types-PyYAML": "https://github.com/python/typeshed/commits/main/stubs/PyYAML", "types-PyYAML": "https://github.com/python/typeshed/commits/main/stubs/PyYAML",
"types-colorama": "https://github.com/python/typeshed/commits/main/stubs/colorama", "types-colorama": "https://github.com/python/typeshed/commits/main/stubs/colorama",
"types-docutils": "https://github.com/python/typeshed/commits/main/stubs/docutils", "types-docutils": "https://github.com/python/typeshed/commits/main/stubs/docutils",
@ -101,7 +102,6 @@
"h11": "https://h11.readthedocs.io/en/latest/changes.html", "h11": "https://h11.readthedocs.io/en/latest/changes.html",
"httpcore": "https://github.com/encode/httpcore/blob/master/CHANGELOG.md", "httpcore": "https://github.com/encode/httpcore/blob/master/CHANGELOG.md",
"httpx": "https://github.com/encode/httpx/blob/master/CHANGELOG.md", "httpx": "https://github.com/encode/httpx/blob/master/CHANGELOG.md",
"sniffio": "https://sniffio.readthedocs.io/en/latest/history.html",
"six": "https://github.com/benjaminp/six/blob/master/CHANGES", "six": "https://github.com/benjaminp/six/blob/master/CHANGES",
"altgraph": "https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst", "altgraph": "https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst",
"urllib3": "https://github.com/urllib3/urllib3/blob/main/CHANGES.rst", "urllib3": "https://github.com/urllib3/urllib3/blob/main/CHANGES.rst",
@ -127,7 +127,7 @@
"distlib": "https://github.com/pypa/distlib/blob/master/CHANGES.rst", "distlib": "https://github.com/pypa/distlib/blob/master/CHANGES.rst",
"py-cpuinfo": "https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog", "py-cpuinfo": "https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog",
"cheroot": "https://cheroot.cherrypy.dev/en/latest/history.html", "cheroot": "https://cheroot.cherrypy.dev/en/latest/history.html",
"certifi": "https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport", "certifi": "https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReport",
"chardet": "https://github.com/chardet/chardet/releases", "chardet": "https://github.com/chardet/chardet/releases",
"charset-normalizer": "https://github.com/Ousret/charset_normalizer/blob/master/CHANGELOG.md", "charset-normalizer": "https://github.com/Ousret/charset_normalizer/blob/master/CHANGELOG.md",
"idna": "https://github.com/kjd/idna/blob/master/HISTORY.rst", "idna": "https://github.com/kjd/idna/blob/master/HISTORY.rst",

View File

@ -10,21 +10,12 @@ RUN pacman -Su --noconfirm \
python-tox \ python-tox \
python-distlib \ python-distlib \
libxml2-legacy \ libxml2-legacy \
{% if qt6 %} qt6-base \
qt6-base \ qt6-declarative \
qt6-declarative \ qt6-webengine \
qt6-webengine \ python-pyqt6-webengine \
python-pyqt6-webengine \ pdfjs \
pdfjs \ python-pyqt6 \
python-pyqt6 \
{% else %}
qt5-base \
qt5-declarative \
openssl-1.1 \
qt5-webengine \
python-pyqtwebengine \
python-pyqt5 \
{% endif %}
xorg-xinit \ xorg-xinit \
xorg-server-xvfb \ xorg-server-xvfb \
ttf-bitstream-vera \ ttf-bitstream-vera \
@ -36,12 +27,7 @@ RUN useradd user -u 1001 && \
mkdir /home/user && \ mkdir /home/user && \
chown user:users /home/user chown user:users /home/user
{% if qt6 %} RUN python3 -c "from PyQt6 import QtWebEngineCore, QtWebEngineWidgets"
{% set pyqt_module = 'PyQt6' %}
{% else %}
{% set pyqt_module = 'PyQt5' %}
{% endif %}
RUN python3 -c "from {{ pyqt_module }} import QtWebEngineCore, QtWebEngineWidgets"
USER user USER user
WORKDIR /home/user WORKDIR /home/user
@ -49,4 +35,4 @@ RUN git config --global --add safe.directory /outside/.git
CMD git clone /outside qutebrowser.git && \ CMD git clone /outside qutebrowser.git && \
cd qutebrowser.git && \ cd qutebrowser.git && \
{{ python }} -m tox -e {% if qt6 %}py-qt6{% else %}py-qt5{% endif %} {{ python }} -m tox -e py-qt6

View File

@ -7,17 +7,14 @@
"""Generate Dockerfiles for qutebrowser's CI.""" """Generate Dockerfiles for qutebrowser's CI."""
import sys
import argparse import argparse
import jinja2 import jinja2
CONFIGS = { CONFIGS = {
'archlinux-webengine': {'unstable': False, 'qt6': False}, 'archlinux-webengine': {'unstable': False},
'archlinux-webengine-qt6': {'unstable': False, 'qt6': True}, 'archlinux-webengine-unstable': {'unstable': True},
'archlinux-webengine-unstable': {'unstable': True, 'qt6': False},
'archlinux-webengine-unstable-qt6': {'unstable': True, 'qt6': True},
} }

View File

@ -54,13 +54,14 @@ def show_commit():
git_args = ['git', 'show'] git_args = ['git', 'show']
if utils.ON_CI: if utils.ON_CI:
git_args.append("--color") git_args.append("--color")
git_args.append("--no-patch") # shows entire git tree on CI (shallow clone)
subprocess.run(git_args, check=True) subprocess.run(git_args, check=True)
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update release version.") parser = argparse.ArgumentParser(description="Update release version.")
parser.add_argument('bump', action="store", parser.add_argument('bump', action="store",
choices=["major", "minor", "patch"], choices=["major", "minor", "patch", "reupload"],
help="Update release version") help="Update release version")
parser.add_argument('--commands', action="store_true", parser.add_argument('--commands', action="store_true",
help="Only show commands to run post-release.") help="Only show commands to run post-release.")
@ -70,7 +71,8 @@ if __name__ == "__main__":
if not args.commands: if not args.commands:
verify_branch(args.bump) verify_branch(args.bump)
bump_version(args.bump) if args.bump != "reupload":
bump_version(args.bump)
show_commit() show_commit()
import qutebrowser import qutebrowser
@ -87,15 +89,16 @@ if __name__ == "__main__":
print(f"Outputs for {version} written to GitHub Actions output file") print(f"Outputs for {version} written to GitHub Actions output file")
else: else:
print("Run the following commands to create a new release:") print("Run the following commands to create a new release:")
print("* git push origin; git push origin v{v}".format(v=version)) if args.bump != 'reupload':
if args.bump == 'patch': print("* git push origin; git push origin v{v}".format(v=version))
print("* git checkout main && git cherry-pick -x v{v} && " if args.bump == 'patch':
"git push origin".format(v=version)) print("* git checkout main && git cherry-pick -x v{v} && "
else: "git push origin".format(v=version))
print("* git branch v{x} v{v} && git push --set-upstream origin v{x}" else:
.format(v=version, x=version_x)) print("* git branch v{x} v{v} && git push --set-upstream origin v{x}"
print("* Create new release via GitHub (required to upload release " .format(v=version, x=version_x))
"artifacts)") print("* Create new release via GitHub (required to upload release "
"artifacts)")
print("* Linux: git fetch && git checkout v{v} && " print("* Linux: git fetch && git checkout v{v} && "
"tox -e build-release -- --upload" "tox -e build-release -- --upload"
.format(v=version)) .format(v=version))

View File

@ -107,7 +107,7 @@ def get_lib_path(executable, name, required=True):
return data return data
elif prefix == 'ImportError': elif prefix == 'ImportError':
if required: if required:
wrapper = os.environ["QUTE_QT_WRAPPER"] wrapper = os.environ.get("QUTE_QT_WRAPPER", "unset")
raise Error( raise Error(
f"Could not import {name} with {executable}: {data} " f"Could not import {name} with {executable}: {data} "
f"(QUTE_QT_WRAPPER: {wrapper})" f"(QUTE_QT_WRAPPER: {wrapper})"

View File

@ -453,7 +453,6 @@ def install_dev_requirements(venv_dir: pathlib.Path) -> None:
utils.print_title("Installing dev dependencies") utils.print_title("Installing dev dependencies")
pip_install(venv_dir, pip_install(venv_dir,
'-r', str(requirements_file('dev')), '-r', str(requirements_file('dev')),
'-r', str(requirements_file('check-manifest')),
'-r', str(requirements_file('flake8')), '-r', str(requirements_file('flake8')),
'-r', str(requirements_file('mypy')), '-r', str(requirements_file('mypy')),
'-r', str(requirements_file('pyroma')), '-r', str(requirements_file('pyroma')),

View File

@ -9,7 +9,15 @@
import re import re
import ast import ast
import os import os
import os.path import sys
import pathlib
# Add repo root to path so we can import scripts. Prior to PEP517 support this
# was the default behavior for setuptools.
# https://github.com/pypa/setuptools/issues/3939#issuecomment-1573619382
# > If users want to import local modules they are recommended to explicitly add
# > the current directory to sys.path at the top of setup.py.
sys.path.append(".")
from scripts import setupcommon as common from scripts import setupcommon as common
@ -17,7 +25,7 @@ import setuptools
try: try:
BASEDIR = os.path.dirname(os.path.realpath(__file__)) BASEDIR = pathlib.Path(__file__).resolve().parent
except NameError: except NameError:
BASEDIR = None BASEDIR = None
@ -42,8 +50,8 @@ def _get_constant(name):
The value of the argument. The value of the argument.
""" """
field_re = re.compile(r'__{}__\s+=\s+(.*)'.format(re.escape(name))) field_re = re.compile(r'__{}__\s+=\s+(.*)'.format(re.escape(name)))
path = os.path.join(BASEDIR, 'qutebrowser', '__init__.py') init_path = BASEDIR / 'qutebrowser' / '__init__.py'
line = field_re.search(read_file(path)).group(1) line = field_re.search(read_file(init_path)).group(1)
value = ast.literal_eval(line) value = ast.literal_eval(line)
return value return value
@ -91,6 +99,6 @@ try:
) )
finally: finally:
if BASEDIR is not None: if BASEDIR is not None:
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id') git_commit_id_path = BASEDIR / 'qutebrowser' / 'git-commit-id'
if os.path.exists(path): if git_commit_id_path.exists():
os.remove(path) git_commit_id_path.unlink()

View File

@ -132,10 +132,10 @@ def _apply_platform_markers(config, item):
( (
config.webengine config.webengine
and version.qtwebengine_versions(avoid_init=True).webengine and version.qtwebengine_versions(avoid_init=True).webengine
== utils.VersionNumber(6, 9) > utils.VersionNumber(6, 9)
and testutils.ON_CI and testutils.ON_CI
), ),
"Flaky with QtWebEngine 6.9 on CI", "Flaky with QtWebEngine 6.9+ on CI",
), ),
( (
"qt69_ci_skip", # WORKAROUND: https://github.com/qutebrowser/qutebrowser/issues/8444#issuecomment-2569610110 "qt69_ci_skip", # WORKAROUND: https://github.com/qutebrowser/qutebrowser/issues/8444#issuecomment-2569610110
@ -143,10 +143,10 @@ def _apply_platform_markers(config, item):
( (
config.webengine config.webengine
and version.qtwebengine_versions(avoid_init=True).webengine and version.qtwebengine_versions(avoid_init=True).webengine
== utils.VersionNumber(6, 9) > utils.VersionNumber(6, 9)
and testutils.ON_CI and testutils.ON_CI
), ),
"Skipped with QtWebEngine 6.9 on CI", "Skipped with QtWebEngine 6.9+ on CI",
), ),
] ]

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<!-- target: hello.txt -->
<html>
<body>
<div id="host"></div>
<script>
const hostElement = document.getElementById('host');
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
const a = document.createElement('a');
a.href = '/data/hello.txt';
a.textContent = 'In shadow DOM';
shadowRoot.appendChild(a);
</script>
</body>
</html>

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Scrolling</title> <title>Scrolling</title>
<script>requestAnimationFrame(() => console.log('position_absolute loaded'))</script>
</head> </head>
<body style="position: absolute"> <body style="position: absolute">
<a href="/data/hello.txt" id="link">Just a link</a> <a href="/data/hello.txt" id="link">Just a link</a>

View File

@ -15,9 +15,11 @@
console.log("[PASS] Positions equal: " + old_position); console.log("[PASS] Positions equal: " + old_position);
} }
} }
requestAnimationFrame(() => console.log('simple loaded'))
</script> </script>
</head> </head>
<body onload="console.log('simple loaded')"> <body>
<a href="/data/hello.txt" id="link">Just a link</a> <a href="/data/hello.txt" id="link">Just a link</a>
<button>blub</button> <button>blub</button>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p> <p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>

View File

@ -103,3 +103,11 @@ Feature: Using completion
And I run :completion-item-focus next And I run :completion-item-focus next
And I run :cmd-set-text -s :set And I run :cmd-set-text -s :set
Then the completion model should be option Then the completion model should be option
Scenario: Page focus after using completion (#8750)
When I open data/insert_mode_settings/html/input.html
And I run :cmd-set-text :
And I run :mode-leave
And I run :click-element id qute-input
And I run :fake-key -g someinput
Then the javascript message "contents: someinput" should be logged

View File

@ -417,7 +417,7 @@ def update_documentation():
try: try:
subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL, subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, check=True) stderr=subprocess.DEVNULL, check=True)
except OSError: except (OSError, subprocess.CalledProcessError):
pytest.skip("Docs outdated and asciidoc unavailable!") pytest.skip("Docs outdated and asciidoc unavailable!")
update_script = os.path.join(script_path, 'asciidoc2html.py') update_script = os.path.join(script_path, 'asciidoc2html.py')

View File

@ -78,8 +78,8 @@ Feature: Downloading things from a website.
And I open data/downloads/issue1243.html And I open data/downloads/issue1243.html
And I hint with args "links download" and follow a And I hint with args "links download" and follow a
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
Then the error "Download error: No handler found for qute://" should be shown Then the error "Download error: Invalid host (from path): ''" should be shown
And "NotFoundError while handling qute://* URL" should be logged And "UrlInvalidError while handling qute://* URL" should be logged
Scenario: Downloading a data: link (issue 1214) Scenario: Downloading a data: link (issue 1214)
When I set downloads.location.suggestion to filename When I set downloads.location.suggestion to filename
@ -129,6 +129,14 @@ Feature: Downloading things from a website.
And I wait for "Download drip finished" in the log And I wait for "Download drip finished" in the log
Then the downloaded file drip should be 128 bytes big Then the downloaded file drip should be 128 bytes big
Scenario: Shutting down with a download question
When I set downloads.location.prompt to true
And I open data/downloads/download.bin without waiting
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
And I run :close
Then qutebrowser should quit
# (and no crash should happen)
Scenario: Downloading a file with spaces Scenario: Downloading a file with spaces
When I open data/downloads/download with spaces.bin without waiting When I open data/downloads/download with spaces.bin without waiting
And I wait until the download is finished And I wait until the download is finished
@ -669,6 +677,21 @@ Feature: Downloading things from a website.
Then the downloaded file download.bin should exist Then the downloaded file download.bin should exist
And the downloaded file download2.bin should not exist And the downloaded file download2.bin should not exist
@qt>=6.9
Scenario: Nested download prompts (#8674)
When I set downloads.location.prompt to true
And I open data/downloads/download.bin without waiting
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
And I open data/downloads/download.bin without waiting
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
And I open data/downloads/download.bin without waiting
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
And I run :prompt-accept
And I run :mode-leave
And I run :mode-leave
And I wait until the download is finished
Then the downloaded file download.bin should exist
@qtwebengine_skip # We can't get the UA from the page there @qtwebengine_skip # We can't get the UA from the page there
Scenario: user-agent when using :download Scenario: user-agent when using :download
When I open user-agent When I open user-agent

View File

@ -386,6 +386,13 @@ Feature: Various utility commands.
And I run :jseval console.log(window.navigator.userAgent) And I run :jseval console.log(window.navigator.userAgent)
Then the header User-Agent should be set to toaster Then the header User-Agent should be set to toaster
Scenario: User-agent header with redirect
When I run :set -u localhost content.headers.user_agent toaster
And I open redirect-to?url=headers without waiting
And I wait until headers is loaded
And I run :jseval console.log(window.navigator.userAgent)
Then the header User-Agent should be set to toaster
Scenario: User-agent header (JS) Scenario: User-agent header (JS)
When I set content.headers.user_agent to toaster When I set content.headers.user_agent to toaster
And I open about:blank And I open about:blank

View File

@ -327,6 +327,7 @@ Feature: Scrolling
Scenario: Relative scroll position with a position:absolute page Scenario: Relative scroll position with a position:absolute page
When I open data/scroll/position_absolute.html When I open data/scroll/position_absolute.html
And I wait for "* position_absolute loaded" in the log
And I run :scroll-to-perc 100 And I run :scroll-to-perc 100
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :scroll-page --bottom-navigate next 0 1 And I run :scroll-page --bottom-navigate next 0 1
@ -339,3 +340,11 @@ Feature: Scrolling
And I run :tab-next And I run :tab-next
And I run :jseval --world main checkAnchor() And I run :jseval --world main checkAnchor()
Then "[*] [PASS] Positions equal: *" should be logged Then "[*] [PASS] Positions equal: *" should be logged
Scenario: Showing/hiding statusbar (#2236, #8223)
When I set statusbar.show to never
And I run :scroll-to-perc 100
And I wait until the scroll position changed
And I run :cmd-set-text /
And I run :fake-key -g <Escape>
Then "Scroll position changed to Py*.QtCore.QPoint()" should not be logged

View File

@ -75,6 +75,7 @@ Feature: Saving and loading sessions
@qtwebkit_skip @qtwebkit_skip
Scenario: Scrolling (qtwebengine) Scenario: Scrolling (qtwebengine)
When I open data/scroll/simple.html When I open data/scroll/simple.html
And I wait for "* simple loaded" in the log
And I run :scroll-px 10 20 And I run :scroll-px 10 20
And I wait until the scroll position changed to 10/20 And I wait until the scroll position changed to 10/20
Then the session should look like: Then the session should look like:

View File

@ -49,11 +49,14 @@ def fresh_instance(quteproc):
# Qt6.8 by default will remember feature grants or denies. When we are # Qt6.8 by default will remember feature grants or denies. When we are
# on PyQt6.8 we disable that with the new API, otherwise restart the # on PyQt6.8 we disable that with the new API, otherwise restart the
# browser to make it forget previous prompts. # browser to make it forget previous prompts.
#
# Starting with Qt 6.10, QtWebEngine unconditionally persists some permissions;
# see https://bugreports.qt.io/browse/QTBUG-140194
if ( if (
qtutils.version_check("6.8", compiled=False) qtutils.version_check("6.8", compiled=False)
and PYQT_WEBENGINE_VERSION and PYQT_WEBENGINE_VERSION
and PYQT_WEBENGINE_VERSION < 0x60800 and PYQT_WEBENGINE_VERSION < 0x60800
): ) or qtutils.version_check("6.10", compiled=False):
quteproc.terminate() quteproc.terminate()
quteproc.start() quteproc.start()

View File

@ -239,7 +239,7 @@ def is_ignored_chromium_message(line):
# Qt 6.7 on GitHub Actions # Qt 6.7 on GitHub Actions
# [3456:5752:1111/103609.929:ERROR:block_files.cc(443)] Failed to open # [3456:5752:1111/103609.929:ERROR:block_files.cc(443)] Failed to open
# C:\Users\RUNNER~1\AppData\Local\Temp\qutebrowser-basedir-ruvn1lys\data\webengine\DawnCache\data_0 # C:\Users\RUNNER~1\AppData\Local\Temp\qutebrowser-basedir-ruvn1lys\data\webengine\DawnCache\data_0
"Failed to open *webengine*DawnCache*data_*", "Failed to open *webengine*Dawn*Cache*data_*",
# Qt 6.8 on GitHub Actions # Qt 6.8 on GitHub Actions
# [7072:3412:1209/220659.527:ERROR:simple_index_file.cc(322)] Failed to # [7072:3412:1209/220659.527:ERROR:simple_index_file.cc(322)] Failed to
@ -249,6 +249,28 @@ def is_ignored_chromium_message(line):
# Qt 6.9 Beta 3 on GitHub Actions # Qt 6.9 Beta 3 on GitHub Actions
# [978:1041:0311/070551.759339:ERROR:bus.cc(407)] # [978:1041:0311/070551.759339:ERROR:bus.cc(407)]
"Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory", "Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory",
# Qt 6.9 on GitHub Actions with Windows Server 2025
# [4348:7828:0605/123815.402:ERROR:shared_image_manager.cc(356)]
"SharedImageManager::ProduceMemory: Trying to Produce a Memory representation from a non-existent mailbox.",
# Qt 6.10 debug build
# "[453900:453973:0909/000324.265214:WARNING:viz_main_impl.cc(85)]"
"VizNullHypothesis is disabled (not a warning)",
# Qt 6.10 on Windows + GitHub Actions
# [1784:7100:1022/150433.690:ERROR:direct_composition_support.cc(225)]
"GetGpuDriverOverlayInfo: Failed to retrieve video device",
# [1784:7100:1022/150434.202:ERROR:direct_composition_support.cc(1122)]
"QueryInterface to IDCompositionDevice4 failed: No such interface supported (0x80004002)",
# Qt 6.10 on Windows + GitHub Actions
# [3508:6056:1103/172403.602:ERROR:cache_util_win.cc(20)]
"Unable to move the cache: The system cannot find the file specified. (0x2)",
# [3508:5516:1103/172403.608:ERROR:disk_cache.cc(216)]
"Unable to create cache",
# [3508:5516:1103/172403.608:ERROR:gpu_disk_cache.cc(711)]
"Gpu Cache Creation failed: -2",
] ]
return any(testutils.pattern_match(pattern=pattern, value=message) return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages) for pattern in ignored_messages)

View File

@ -398,7 +398,7 @@ class Process(QObject):
match = self._wait_for_match(spy, kwargs) match = self._wait_for_match(spy, kwargs)
if match is not None: if match is not None:
if message is not None: if message is not None:
self._log("----> found it") self._log(f"----> found it: {match.formatted_str()}")
return match return match
raise quteutils.Unreachable raise quteutils.Unreachable

View File

@ -21,6 +21,7 @@ import pytest
from qutebrowser.qt.core import QProcess, QPoint from qutebrowser.qt.core import QProcess, QPoint
from helpers import testutils from helpers import testutils
from end2end.fixtures import quteprocess
from qutebrowser.utils import qtutils, utils, version from qutebrowser.utils import qtutils, utils, version
@ -251,6 +252,7 @@ def test_optimize(request, quteproc_new, capfd, level):
def test_version(request): def test_version(request):
"""Test invocation with --version argument.""" """Test invocation with --version argument."""
args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config)
args.remove("--json-logging")
# can't use quteproc_new here because it's confused by # can't use quteproc_new here because it's confused by
# early process termination # early process termination
proc = QProcess() proc = QProcess()
@ -611,6 +613,26 @@ def test_service_worker_workaround(
assert not service_worker_dir.exists() assert not service_worker_dir.exists()
@pytest.mark.qt6_only
def test_disable_hangouts_extension_crash(
quteproc_new: quteprocess.QuteProc,
request: pytest.FixtureRequest,
webengine_versions: version.WebEngineVersions,
):
"""Make sure disabling the Hangouts extension doesn't crash."""
args = _base_args(request.config) + [
'--temp-basedir',
'-s', 'qt.workarounds.disable_hangouts_extension', 'true',
]
quteproc_new.start(args)
if webengine_versions.webengine == utils.VersionNumber(6, 10, 1):
line = quteproc_new.wait_for(message="Not disabling Hangouts extension *")
line.expected = True
quteproc_new.send_cmd(':quit')
quteproc_new.wait_for_quit()
@pytest.mark.parametrize('store', [True, False]) @pytest.mark.parametrize('store', [True, False])
def test_cookies_store(quteproc_new, request, short_tmpdir, store): def test_cookies_store(quteproc_new, request, short_tmpdir, store):
# Start test process # Start test process
@ -1039,11 +1061,13 @@ def test_restart(request, quteproc_new):
pid = int(line.message.removeprefix(prefix)) pid = int(line.message.removeprefix(prefix))
os.kill(pid, signal.SIGTERM) os.kill(pid, signal.SIGTERM)
try: # This often hangs on Windows for unknown reasons
# If the new process hangs, this will hang too. if not utils.is_windows:
# Still better than just ignoring it, so we can fix it if something is broken. try:
os.waitpid(pid, 0) # pid, options... positional-only :( # If the new process hangs, this will hang too.
except (ChildProcessError, PermissionError): # Still better than just ignoring it, so we can fix it if something is broken.
# Already gone. Even if not documented, Windows seems to raise PermissionError os.waitpid(pid, 0) # pid, options... positional-only :(
# here... except (ChildProcessError, PermissionError):
pass # Already gone. Even if not documented, Windows seems to raise PermissionError
# here...
pass

View File

@ -8,6 +8,7 @@ import sys
import logging import logging
import types import types
import enum import enum
import inspect
import textwrap import textwrap
import pytest import pytest
@ -440,7 +441,15 @@ class TestArgument:
def test_no_docstring_with_optimize(self, monkeypatch): def test_no_docstring_with_optimize(self, monkeypatch):
"""With -OO we'd get a warning on start, but no warning afterwards.""" """With -OO we'd get a warning on start, but no warning afterwards."""
monkeypatch.setattr(sys, 'flags', types.SimpleNamespace(optimize=2)) sys_flags_fake = types.SimpleNamespace(
**{
k: v
for k, v in inspect.getmembers(sys.flags)
if not k.startswith("_") and k not in {"count", "index"}
}
)
sys_flags_fake.optimize = 2
monkeypatch.setattr(sys, 'flags', sys_flags_fake)
@cmdutils.register() @cmdutils.register()
def fun(): def fun():

View File

@ -4,7 +4,6 @@
import string import string
import functools import functools
import itertools
import operator import operator
import pytest import pytest
@ -76,7 +75,7 @@ def test_match_benchmark(benchmark, tabbed_browser, qtbot, mode_manager, qapp,
@pytest.mark.parametrize('min_len', [0, 3]) @pytest.mark.parametrize('min_len', [0, 3])
@pytest.mark.parametrize('num_chars', [5, 9]) @pytest.mark.parametrize('num_chars', [5, 9])
@pytest.mark.parametrize('num_elements', itertools.chain(range(1, 26), [125])) @pytest.mark.parametrize('num_elements', [*range(1, 26), 125])
def test_scattered_hints_count(min_len, num_chars, num_elements): def test_scattered_hints_count(min_len, num_chars, num_elements):
"""Test scattered hints function. """Test scattered hints function.

View File

@ -17,6 +17,36 @@ from qutebrowser.utils import resources, urlmatch
from qutebrowser.misc import guiprocess from qutebrowser.misc import guiprocess
class TestDataForUrl:
@pytest.mark.parametrize(
"url, expected",
[
# QUrl.UrlFormattingOption.StripTrailingSlash
(QUrl("qute://abc/xyz/"), QUrl("qute://abc/xyz")),
# QUrl.UrlFormattingOption.NormalizePathSegments
(QUrl("qute://abc/uvw/../xyz"), QUrl("qute://abc/xyz")),
# Adding host trailing slash
(QUrl("qute://abc"), QUrl("qute://abc/")),
(QUrl("qute://abc?q=42"), QUrl("qute://abc/?q=42")),
# path -> host
(QUrl("qute:abc"), QUrl("qute://abc/")),
(QUrl("qute:abc?q=42"), QUrl("qute://abc/?q=42")),
],
ids=lambda url: url.toString(),
)
def test_redirects(self, url: QUrl, expected: QUrl) -> None:
with pytest.raises(qutescheme.Redirect) as exc:
qutescheme.data_for_url(url)
assert exc.value.url == expected
def test_invalid_redirect(self) -> None:
url = QUrl("qute:-")
with pytest.raises(
qutescheme.UrlInvalidError, match=r"Invalid host \(from path\): '-'"
):
qutescheme.data_for_url(url)
class TestJavascriptHandler: class TestJavascriptHandler:
"""Test the qute://javascript endpoint.""" """Test the qute://javascript endpoint."""

View File

@ -315,9 +315,10 @@ class TestAdd:
with pytest.raises(cmdutils.CommandError, match="Invalid value ''"): with pytest.raises(cmdutils.CommandError, match="Invalid value ''"):
commands.config_list_add('content.blocking.whitelist', '') commands.config_list_add('content.blocking.whitelist', '')
# FIXME test value conversion for :list-add like in test_dict_add_value_type def test_list_add_value_type(self, commands, config_stub):
# (once we have a List config option using a non-str type, or a way to commands.config_list_add("completion.web_history.exclude", "*")
# dynamically add new option definitions). value = config_stub.val.completion.web_history.exclude
assert value == [urlmatch.UrlPattern("*")]
@pytest.mark.parametrize('value', ['test1', 'test2']) @pytest.mark.parametrize('value', ['test1', 'test2'])
@pytest.mark.parametrize('temp', [True, False]) @pytest.mark.parametrize('temp', [True, False])
@ -410,9 +411,16 @@ class TestRemove:
match="#133742 is not in colors.completion.fg!"): match="#133742 is not in colors.completion.fg!"):
commands.config_list_remove('colors.completion.fg', '#133742') commands.config_list_remove('colors.completion.fg', '#133742')
# FIXME test value conversion for :list-remove like in test_dict_add_value_type def test_list_remove_value_type(self, commands, config_stub):
# (once we have a List config option using a non-str type, or a way to config_stub.val.completion.web_history.exclude = ["*"]
# dynamically add new option definitions). commands.config_list_remove("completion.web_history.exclude", "*")
assert not config_stub.val.completion.web_history.exclude
def test_list_remove_invalid_value(self, commands, config_stub):
with pytest.raises(
cmdutils.CommandError,
match="Invalid value '::' - Pattern without host"):
commands.config_list_remove("completion.web_history.exclude", "::")
@pytest.mark.parametrize('key', ['w', 'q']) @pytest.mark.parametrize('key', ['w', 'q'])
@pytest.mark.parametrize('temp', [True, False]) @pytest.mark.parametrize('temp', [True, False])

View File

@ -1629,7 +1629,24 @@ class TestDict:
none_ok=True) none_ok=True)
converted = d.to_py(val) converted = d.to_py(val)
expected = converted if converted else None expected = converted if converted else None
assert d.from_str(d.to_str(converted)) == expected to_str = d.to_str(converted)
# YAML keys have a max length of 1024 characters:
# https://yaml.org/spec/1.2.2/#example-single-pair-explicit-entry
# Due to characters being backslash-escaped in YAML, we can't easily control
# the input size (short of setting it to `1024 / len("\\uXXXX")) = 170`),
# so we instead skip the string round trip check if the end result turned out
# to be too long.
#
# yaml.safe_load('{"%s": false}' % ("a" * 1022)) -> works (1033 chars total)
# yaml.safe_load('{"%s": false}' % ("a" * 1023)) -> fails (1034 chars total)
# ^^ ^^^^^^^^^ = 11 chars "overhead"
#
# Since this only affects .from_str() which always has error handling
# for YAML errors (since a user could enter invalid values anyways), we
# don't handle this specially in configtypes.py.
if len(to_str) <= 1022 + len('{"": false}'):
assert d.from_str(to_str) == expected
@hypothesis.given(val=strategies.dictionaries(strategies.text(min_size=1), @hypothesis.given(val=strategies.dictionaries(strategies.text(min_size=1),
strategies.booleans())) strategies.booleans()))

View File

@ -12,7 +12,7 @@ import pytest
from qutebrowser.qt import machinery from qutebrowser.qt import machinery
from qutebrowser import qutebrowser from qutebrowser import qutebrowser
from qutebrowser.config import qtargs, configdata from qutebrowser.config import qtargs, configdata
from qutebrowser.utils import usertypes, version from qutebrowser.utils import usertypes, version, utils
@pytest.fixture @pytest.fixture
@ -52,6 +52,7 @@ def reduce_args(config_stub, version_patcher, monkeypatch):
config_stub.val.scrolling.bar = 'never' config_stub.val.scrolling.bar = 'never'
config_stub.val.qt.chromium.experimental_web_platform_features = 'never' config_stub.val.qt.chromium.experimental_web_platform_features = 'never'
config_stub.val.qt.workarounds.disable_accelerated_2d_canvas = 'never' config_stub.val.qt.workarounds.disable_accelerated_2d_canvas = 'never'
config_stub.val.qt.workarounds.disable_accessibility = 'never'
monkeypatch.setattr(qtargs.utils, 'is_mac', False) monkeypatch.setattr(qtargs.utils, 'is_mac', False)
# Avoid WebRTC pipewire feature # Avoid WebRTC pipewire feature
monkeypatch.setattr(qtargs.utils, 'is_linux', False) monkeypatch.setattr(qtargs.utils, 'is_linux', False)
@ -117,6 +118,14 @@ def test_no_webengine_available(monkeypatch, config_stub, parser, stubs):
assert args == [sys.argv[0]] assert args == [sys.argv[0]]
_XFAIL_FUTURE_QT = (
pytest.mark.xfail(
utils.VersionNumber(6, 11) not in version.WebEngineVersions._CHROMIUM_VERSIONS,
reason="Unknown security patch version for Qt 6.11 so far",
),
)
@pytest.mark.usefixtures('reduce_args') @pytest.mark.usefixtures('reduce_args')
class TestWebEngineArgs: class TestWebEngineArgs:
@ -158,12 +167,17 @@ class TestWebEngineArgs:
assert '--enable-in-process-stack-traces' not in args assert '--enable-in-process-stack-traces' not in args
@pytest.mark.parametrize( @pytest.mark.parametrize(
'qt6, value, has_arg', 'qt_version, qt6, value, has_arg',
[ [
(False, 'auto', False), ('5.15.2', False, 'auto', False),
(True, 'auto', True), ('6.5.3', True, 'auto', True),
(True, 'always', True), ('6.6.0', True, 'auto', True),
(True, 'never', False), ('6.7.0', True, 'auto', True),
('6.8.1', True, 'auto', True),
('6.8.2', True, 'auto', False),
('6.5.3', True, 'always', True),
('6.5.3', True, 'never', False),
('6.8.2', True, 'always', True),
], ],
) )
def test_accelerated_2d_canvas( def test_accelerated_2d_canvas(
@ -172,10 +186,12 @@ class TestWebEngineArgs:
version_patcher, version_patcher,
config_stub, config_stub,
monkeypatch, monkeypatch,
qt_version,
qt6, qt6,
value, value,
has_arg, has_arg,
): ):
version_patcher(qt_version)
config_stub.val.qt.workarounds.disable_accelerated_2d_canvas = value config_stub.val.qt.workarounds.disable_accelerated_2d_canvas = value
monkeypatch.setattr(machinery, 'IS_QT6', qt6) monkeypatch.setattr(machinery, 'IS_QT6', qt6)
@ -183,6 +199,40 @@ class TestWebEngineArgs:
args = qtargs.qt_args(parsed) args = qtargs.qt_args(parsed)
assert ('--disable-accelerated-2d-canvas' in args) == has_arg assert ('--disable-accelerated-2d-canvas' in args) == has_arg
@pytest.mark.parametrize(
"qt_version, qt6, value, has_arg",
[
("5.15.2", False, "auto", False),
# 6.8.5 is broken too, but commercial-only
("6.10.0", True, "always", True),
("6.10.0", True, "auto", False),
("6.10.1", True, "auto", True),
("6.10.1", True, "never", False),
("6.10.2", True, "always", True),
("6.10.2", True, "auto", False),
pytest.param("6.11.0", True, "always", True, marks=_XFAIL_FUTURE_QT),
pytest.param("6.11.0", True, "auto", False, marks=_XFAIL_FUTURE_QT),
],
)
def test_disable_accessibility(
self,
parser,
version_patcher,
config_stub,
monkeypatch,
qt_version,
qt6,
value,
has_arg,
):
version_patcher(qt_version)
config_stub.val.qt.workarounds.disable_accessibility = value
monkeypatch.setattr(machinery, 'IS_QT6', qt6)
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
assert ('--disable-renderer-accessibility' in args) == has_arg
@pytest.mark.parametrize('flags, args', [ @pytest.mark.parametrize('flags, args', [
([], []), ([], []),
(['--debug-flag', 'chromium'], ['--enable-logging', '--v=1']), (['--debug-flag', 'chromium'], ['--enable-logging', '--v=1']),
@ -463,9 +513,12 @@ class TestWebEngineArgs:
('6.8.3', "DocumentPictureInPictureAPI"), ('6.8.3', "DocumentPictureInPictureAPI"),
# Qt 6.9 # Qt 6.9
('6.9.0', "DocumentPictureInPictureAPI,PermissionElement"), ('6.9.0', "DocumentPictureInPictureAPI,PermissionElement"),
('6.9.1', "DocumentPictureInPictureAPI"), # tbd ('6.9.1', "DocumentPictureInPictureAPI,PermissionElement"),
# Qt 6.10
('6.10.0', "DocumentPictureInPictureAPI,PermissionElement"),
('6.10.1', "DocumentPictureInPictureAPI"),
]) ])
def test_disble_feature_workaround( def test_disable_feature_workaround(
self, parser, version_patcher, qt_version, disabled self, parser, version_patcher, qt_version, disabled
): ):
version_patcher(qt_version) version_patcher(qt_version)

View File

@ -551,6 +551,7 @@ KEYS = [
Key('MicVolumeUp', 'Microphone Volume Up', qtest=False), Key('MicVolumeUp', 'Microphone Volume Up', qtest=False),
Key('MicVolumeDown', 'Microphone Volume Down', qtest=False), Key('MicVolumeDown', 'Microphone Volume Down', qtest=False),
Key('Keyboard', 'Keyboard', qtest=False),
Key('New', qtest=False), Key('New', qtest=False),
Key('Open', qtest=False), Key('Open', qtest=False),

View File

@ -32,6 +32,31 @@ Thread 0x00007fa135ac7700 (most recent call first):
File "", line 1 in testfunc File "", line 1 in testfunc
""" """
VALID_CRASH_TEXT_PY314 = """
Fatal Python error: Segmentation fault
_
Current thread 0x00000001fe53e140 [CrBrowserMain] (most recent call first):
File "qutebrowser/app.py", line 126 in qt_mainloop
File "qutebrowser/app.py", line 116 in run
File "qutebrowser/qutebrowser.py", line 234 in main
File "__main__.py", line 15 in <module>
_
Current thread's C stack trace (most recent call first):
Binary file "...", at _Py_DumpStack+0x48 [0x10227cc9c]
<truncated rest of calls>
"""
VALID_CRASH_TEXT_PY314_NO_PY = """
Fatal Python error: Segmentation fault
_
Current thread 0x00007f0dc805cbc0 [qutebrowser] (most recent call first):
<no Python frame>
_
Current thread's C stack trace (most recent call first):
Binary file "/lib64/libpython3.14.so.1.0", at _Py_DumpStack+0x4c [0x7f0dc7b2127b]
<truncated rest of calls>
"""
WINDOWS_CRASH_TEXT = r""" WINDOWS_CRASH_TEXT = r"""
Windows fatal exception: access violation Windows fatal exception: access violation
_ _
@ -45,13 +70,32 @@ Hello world!
""" """
@pytest.mark.parametrize('text, typ, func', [ @pytest.mark.parametrize(
(VALID_CRASH_TEXT, 'Segmentation fault', 'testfunc'), "text, typ, func",
(VALID_CRASH_TEXT_THREAD, 'Segmentation fault', 'testfunc'), [
(VALID_CRASH_TEXT_EMPTY, 'Aborted', ''), pytest.param(VALID_CRASH_TEXT, "Segmentation fault", "testfunc", id="valid"),
(WINDOWS_CRASH_TEXT, 'Windows access violation', 'tabopen'), pytest.param(
(INVALID_CRASH_TEXT, '', ''), VALID_CRASH_TEXT_THREAD, "Segmentation fault", "testfunc", id="valid-thread"
]) ),
pytest.param(
VALID_CRASH_TEXT_PY314,
"Segmentation fault",
"qt mainloop",
id="valid-py314",
),
pytest.param(
VALID_CRASH_TEXT_PY314_NO_PY,
"Segmentation fault",
"",
id="valid-py314-no-py",
),
pytest.param(VALID_CRASH_TEXT_EMPTY, "Aborted", "", id="valid-empty"),
pytest.param(
WINDOWS_CRASH_TEXT, "Windows access violation", "tabopen", id="windows"
),
pytest.param(INVALID_CRASH_TEXT, "", "", id="invalid"),
],
)
def test_parse_fatal_stacktrace(text, typ, func): def test_parse_fatal_stacktrace(text, typ, func):
text = text.strip().replace('_', ' ') text = text.strip().replace('_', ' ')
assert crashdialog.parse_fatal_stacktrace(text) == (typ, func) assert crashdialog.parse_fatal_stacktrace(text) == (typ, func)

View File

@ -37,8 +37,8 @@ def signal_handler(qtbot, mocker, read_config_mock):
app=mocker.Mock(spec=QApplication), app=mocker.Mock(spec=QApplication),
quitter=mocker.Mock(spec=quitter.Quitter), quitter=mocker.Mock(spec=quitter.Quitter),
) )
yield signal_handler
return signal_handler signal_handler.deactivate()
def test_handlers_registered(signal_handler): def test_handlers_registered(signal_handler):

View File

@ -12,6 +12,7 @@ import shutil
import pytest import pytest
from qutebrowser.qt import machinery
from qutebrowser.misc import pakjoy, binparsing from qutebrowser.misc import pakjoy, binparsing
from qutebrowser.utils import utils, version, standarddir, usertypes from qutebrowser.utils import utils, version, standarddir, usertypes
@ -193,11 +194,19 @@ def read_patched_manifest():
return json_without_comments(reparsed.manifest) return json_without_comments(reparsed.manifest)
skip_if_incompatible = pytest.mark.skipif(
not machinery.IS_QT6
or version.qtwebengine_versions(avoid_init=True).webengine
>= utils.VersionNumber(6, 10),
reason="Only needed for Qt 6; Qt 6.10+ uses gzip storage for manifest",
)
@pytest.mark.usefixtures("affected_version") @pytest.mark.usefixtures("affected_version")
@skip_if_incompatible
class TestWithRealResourcesFile: class TestWithRealResourcesFile:
"""Tests that use the real pak file form the Qt installation.""" """Tests that use the real pak file form the Qt installation."""
@pytest.mark.qt6_only
def test_happy_path(self): def test_happy_path(self):
# Go through the full patching processes with the real resources file from # Go through the full patching processes with the real resources file from
# the current installation. Make sure our replacement string is in it # the current installation. Make sure our replacement string is in it
@ -257,7 +266,6 @@ class TestWithRealResourcesFile:
"Not applying quirks. Expected location: " "Not applying quirks. Expected location: "
) )
@pytest.mark.qt6_only
def test_hardcoded_ids(self): def test_hardcoded_ids(self):
"""Make sure we hardcoded the currently valid ID. """Make sure we hardcoded the currently valid ID.
@ -445,6 +453,7 @@ class TestWithConstructedResourcesFile:
def quirk_dir_path(self, tmp_path: pathlib.Path) -> pathlib.Path: def quirk_dir_path(self, tmp_path: pathlib.Path) -> pathlib.Path:
return tmp_path / "cache" / pakjoy.CACHE_DIR_NAME return tmp_path / "cache" / pakjoy.CACHE_DIR_NAME
@skip_if_incompatible
def test_patching(self, resources_path: pathlib.Path, quirk_dir_path: pathlib.Path): def test_patching(self, resources_path: pathlib.Path, quirk_dir_path: pathlib.Path):
"""Go through the full patching processes with a fake resources file.""" """Go through the full patching processes with a fake resources file."""
with pakjoy.patch_webengine(): with pakjoy.patch_webengine():
@ -457,13 +466,14 @@ class TestWithConstructedResourcesFile:
) )
assert pakjoy.RESOURCES_ENV_VAR not in os.environ assert pakjoy.RESOURCES_ENV_VAR not in os.environ
@pytest.mark.qt6_only @skip_if_incompatible
def test_explicitly_enabled(self, monkeypatch: pytest.MonkeyPatch, config_stub): def test_explicitly_enabled(self, monkeypatch: pytest.MonkeyPatch, config_stub):
patch_version(monkeypatch, utils.VersionNumber(6, 7)) # unaffected patch_version(monkeypatch, utils.VersionNumber(6, 7)) # unaffected
config_stub.val.qt.workarounds.disable_hangouts_extension = True config_stub.val.qt.workarounds.disable_hangouts_extension = True
with pakjoy.patch_webengine(): with pakjoy.patch_webengine():
assert pakjoy.RESOURCES_ENV_VAR in os.environ assert pakjoy.RESOURCES_ENV_VAR in os.environ
@skip_if_incompatible
def test_preset_env_var( def test_preset_env_var(
self, self,
resources_path: pathlib.Path, resources_path: pathlib.Path,

View File

@ -0,0 +1,313 @@
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import sys
import socket
import ctypes
import ctypes.util
import unittest.mock
from collections.abc import Iterator
import pytest
import pytest_mock
import pytestqt.qtbot
from qutebrowser.qt.widgets import QApplication, QWidget
from qutebrowser.misc import wmname
def test_load_libwayland_client():
"""Test loading the Wayland client library, which might or might not exist."""
try:
wmname._load_library("wayland-client")
except wmname.Error:
pass
def test_load_libwayland_client_error(mocker: pytest_mock.MockerFixture):
"""Test that an error in loading the Wayland client library raises an error."""
mocker.patch.object(ctypes.util, "find_library", return_value="libwayland-client.so.6")
mocker.patch("ctypes.CDLL", side_effect=OSError("Library not found"))
with pytest.raises(wmname.Error, match="Failed to load wayland-client"):
wmname._load_library("wayland-client")
@pytest.fixture
def sock() -> Iterator[socket.socket]:
"""Fixture to create a Unix domain socket."""
parent_sock, child_sock = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
yield parent_sock
parent_sock.close()
child_sock.close()
@pytest.mark.linux
def test_pid_from_fd(sock: socket.socket):
assert wmname._pid_from_fd(sock.fileno()) == os.getpid()
@pytest.mark.skipif(
not hasattr(socket, "SO_PEERCRED"), reason="socket.SO_PEERCRED not available"
)
def test_pid_from_fd_invalid():
"""Test that an invalid file descriptor raises an error."""
with pytest.raises(
wmname.Error,
match=r"Error creating socket for fd -1: \[Errno 9\] Bad file descriptor",
):
wmname._pid_from_fd(-1)
@pytest.mark.linux
def test_pid_from_fd_getsockopt_error(
sock: socket.socket, mocker: pytest_mock.MockerFixture
):
"""Test that an error in getsockopt raises an error."""
mocker.patch.object(
socket.socket, "getsockopt", side_effect=OSError("Mocked error")
)
with pytest.raises(wmname.Error, match="Error getting SO_PEERCRED for fd"):
wmname._pid_from_fd(sock.fileno())
def test_pid_from_fd_no_so_peercred(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delattr(socket, "SO_PEERCRED", raising=False)
with pytest.raises(wmname.Error, match=r"Missing socket\.SO_PEERCRED"):
wmname._pid_from_fd(-1)
@pytest.mark.linux
def test_process_name_from_pid():
"""Test getting the process name from a PID."""
pid = os.getpid()
name = wmname._process_name_from_pid(pid)
assert os.path.basename(name.split()[0]) == os.path.basename(sys.executable)
def test_process_name_from_pid_invalid():
"""Test that an invalid PID raises an error."""
with pytest.raises(wmname.Error, match=r"Error opening .proc.-1.cmdline"):
wmname._process_name_from_pid(-1)
@pytest.fixture
def libwayland_client_mock(mocker: pytest_mock.MockerFixture) -> None:
"""Mock the libwayland-client library."""
return mocker.Mock()
@pytest.fixture
def fake_wayland_display() -> wmname._WaylandDisplay:
return wmname._WaylandDisplay(ctypes.pointer(wmname._WaylandDisplayStruct()))
def test_wayland_display(
libwayland_client_mock: unittest.mock.Mock,
fake_wayland_display: wmname._WaylandDisplay,
):
"""Test getting the Wayland display."""
libwayland_client_mock.wl_display_connect.return_value = fake_wayland_display
with wmname._wayland_display(libwayland_client_mock):
pass
libwayland_client_mock.wl_display_connect.assert_called_once_with(None)
libwayland_client_mock.wl_display_disconnect.assert_called_once_with(
fake_wayland_display
)
def test_wayland_display_error(libwayland_client_mock: unittest.mock.Mock):
"""Test that an error in getting the Wayland display raises an error."""
libwayland_client_mock.wl_display_connect.return_value = ctypes.c_void_p(0)
with pytest.raises(wmname.Error, match="Can't connect to display"):
with wmname._wayland_display(libwayland_client_mock):
pass
libwayland_client_mock.wl_display_disconnect.assert_not_called() # Not called on error
def test_wayland_get_fd(
libwayland_client_mock: unittest.mock.Mock,
fake_wayland_display: wmname._WaylandDisplay,
):
"""Test getting the file descriptor from a Wayland display."""
libwayland_client_mock.wl_display_get_fd.return_value = 42
fd = wmname._wayland_get_fd(libwayland_client_mock, fake_wayland_display)
assert fd == 42
libwayland_client_mock.wl_display_get_fd.assert_called_once_with(
fake_wayland_display
)
def test_wayland_get_fd_error(
libwayland_client_mock: unittest.mock.Mock,
fake_wayland_display: wmname._WaylandDisplay,
):
"""Test that an error in getting the file descriptor raises an error."""
libwayland_client_mock.wl_display_get_fd.return_value = -1
with pytest.raises(
wmname.Error, match="Failed to get Wayland display file descriptor: -1"
):
wmname._wayland_get_fd(libwayland_client_mock, fake_wayland_display)
libwayland_client_mock.wl_display_get_fd.assert_called_once_with(
fake_wayland_display
)
def test_wayland_real():
"""Test getting the Wayland window manager name."""
try:
name = wmname.wayland_compositor_name()
except wmname.Error:
return
assert isinstance(name, str)
assert name
def test_load_xlib():
"""Test loading Xlib, which might or might not exist."""
try:
wmname._load_library("X11")
except wmname.Error:
pass
def test_load_xlib_not_found(monkeypatch: pytest.MonkeyPatch):
"""Test loading Xlib simulating a missing library."""
monkeypatch.setattr(ctypes.util, "find_library", lambda x: None)
with pytest.raises(wmname.Error, match="X11 library not found"):
wmname._load_library("X11")
def test_load_xlib_error(mocker: pytest_mock.MockerFixture):
"""Test that an error in loading Xlib raises an error."""
mocker.patch.object(ctypes.util, "find_library", return_value="libX11.so.6")
mocker.patch.object(ctypes, "CDLL", side_effect=OSError("Failed to load library"))
with pytest.raises(
wmname.Error, match="Failed to load X11 library: Failed to load library"
):
wmname._load_library("X11")
@pytest.fixture
def xlib_mock(mocker: pytest_mock.MockerFixture) -> None:
"""Mock the XLib library."""
return mocker.Mock()
@pytest.fixture
def fake_x11_display() -> wmname._X11Display:
return wmname._X11Display(ctypes.pointer(wmname._X11DisplayStruct()))
def test_x11_display(
xlib_mock: unittest.mock.Mock,
fake_x11_display: wmname._X11Display,
):
"""Test getting the X11 display."""
xlib_mock.XOpenDisplay.return_value = fake_x11_display
with wmname._x11_open_display(xlib_mock):
pass
xlib_mock.XOpenDisplay.assert_called_once_with(None)
xlib_mock.XCloseDisplay.assert_called_once_with(fake_x11_display)
def test_x11_display_error(xlib_mock: unittest.mock.Mock):
"""Test that an error in getting the X11 display raises an error."""
xlib_mock.XOpenDisplay.return_value = ctypes.c_void_p(0)
with pytest.raises(wmname.Error, match="Cannot open display"):
with wmname._x11_open_display(xlib_mock):
pass
xlib_mock.XCloseDisplay.assert_not_called() # Not called on error
def test_x11_intern_atom(
xlib_mock: unittest.mock.Mock,
fake_x11_display: wmname._X11Display,
):
"""Test getting an interned atom from X11."""
atom_name = b"_NET_WM_NAME"
atom = 12345
xlib_mock.XInternAtom.return_value = atom
result = wmname._x11_intern_atom(xlib_mock, fake_x11_display, atom_name)
assert result == atom
xlib_mock.XInternAtom.assert_called_once_with(
fake_x11_display,
atom_name,
True, # don't create if not found
)
def test_x11_intern_atom_error(
xlib_mock: unittest.mock.Mock,
fake_x11_display: wmname._X11Display,
):
"""Test that an error in getting an interned atom raises an error."""
xlib_mock.XInternAtom.return_value = 0
with pytest.raises(wmname.Error, match="Failed to intern atom: b'_NET_WM_NAME'"):
wmname._x11_intern_atom(xlib_mock, fake_x11_display, b"_NET_WM_NAME")
xlib_mock.XInternAtom.assert_called_once_with(
fake_x11_display,
b"_NET_WM_NAME",
True, # don't create if not found
)
def test_x11_get_wm_name(
qapp: QApplication,
qtbot: pytestqt.qtbot.QtBot,
) -> None:
"""Test getting a property from X11.
This is difficult to mock (as it involves a C layer via ctypes with return
arguments), so we instead try getting data from a real window.
"""
if qapp.platformName() != "xcb":
pytest.skip("This test only works on X11 (xcb) platforms")
w = QWidget()
qtbot.add_widget(w)
w.setWindowTitle("Test Window")
xlib = wmname._load_library("X11")
with wmname._x11_open_display(xlib) as display:
atoms = wmname._X11Atoms(
NET_SUPPORTING_WM_CHECK=-1,
NET_WM_NAME=wmname._x11_intern_atom(xlib, display, b"_NET_WM_NAME"),
UTF8_STRING=wmname._x11_intern_atom(xlib, display, b"UTF8_STRING"),
)
window = wmname._X11Window(int(w.winId()))
name = wmname._x11_get_wm_name(xlib, display, atoms=atoms, wm_window=window)
assert name == "Test Window"
def test_x11_real():
try:
name = wmname.x11_wm_name()
except wmname.Error:
return
assert isinstance(name, str)
assert name

View File

@ -15,17 +15,21 @@ import textwrap
import datetime import datetime
import dataclasses import dataclasses
import importlib.metadata import importlib.metadata
import unittest.mock
from typing import Any
import pytest import pytest
import pytest_mock
import hypothesis import hypothesis
import hypothesis.strategies import hypothesis.strategies
from qutebrowser.qt import machinery from qutebrowser.qt import machinery
from qutebrowser.qt.core import PYQT_VERSION_STR from qutebrowser.qt.core import PYQT_VERSION_STR, QUrl
from qutebrowser.qt.webenginecore import QWebEngineProfile
import qutebrowser import qutebrowser
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import version, usertypes, utils, standarddir from qutebrowser.utils import version, usertypes, utils, standarddir
from qutebrowser.misc import pastebin, objects, elf from qutebrowser.misc import pastebin, objects, elf, wmname
from qutebrowser.browser import pdfjs from qutebrowser.browser import pdfjs
try: try:
@ -620,39 +624,43 @@ def test_path_info(monkeypatch, equal):
assert pathinfo['system data'] == 'SYSTEM DATA PATH' assert pathinfo['system data'] == 'SYSTEM DATA PATH'
@pytest.fixture
def import_fake(stubs, monkeypatch):
"""Fixture to patch imports using ImportFake."""
fake = stubs.ImportFake(dict.fromkeys(version.MODULE_INFO, True), monkeypatch)
fake.patch()
return fake
class TestModuleVersions: class TestModuleVersions:
"""Tests for _module_versions() and ModuleInfo.""" """Tests for _module_versions() and ModuleInfo."""
@pytest.fixture
def import_fake(self, stubs, monkeypatch):
"""Fixture to patch imports using ImportFake."""
fake = stubs.ImportFake(dict.fromkeys(version.MODULE_INFO, True), monkeypatch)
fake.patch()
return fake
@pytest.fixture(autouse=True)
def importlib_metadata_mock(
self, mocker: pytest_mock.MockerFixture
) -> unittest.mock.Mock:
return mocker.patch("importlib.metadata.version", return_value="4.5.6")
def test_all_present(self, import_fake): def test_all_present(self, import_fake):
"""Test with all modules present in version 1.2.3.""" """Test with all modules present in a fixed version."""
expected = [] expected = []
for name in import_fake.modules: for name in import_fake.modules:
version.MODULE_INFO[name]._reset_cache() version.MODULE_INFO[name]._reset_cache()
if '__version__' not in version.MODULE_INFO[name]._version_attributes: if '__version__' not in version.MODULE_INFO[name]._version_attributes:
expected.append('{}: yes'.format(name)) expected.append(f"{name}: 4.5.6") # from importlib.metadata
else: else:
expected.append('{}: 1.2.3'.format(name)) expected.append(f"{name}: 1.2.3")
assert version._module_versions() == expected assert version._module_versions() == expected
@pytest.mark.parametrize('module, idx, expected', [ @pytest.mark.parametrize('module, expected', [
('colorama', 0, 'colorama: no'), ('colorama', 'colorama: no'),
('adblock', 4, 'adblock: no'), ('adblock', 'adblock: no'),
]) ])
def test_missing_module(self, module, idx, expected, import_fake): def test_missing_module(self, module, expected, import_fake):
"""Test with a module missing. """Test with a module missing.
Args: Args:
module: The name of the missing module. module: The name of the missing module.
idx: The index where the given text is expected.
expected: The expected text. expected: The expected text.
""" """
import_fake.modules[module] = False import_fake.modules[module] = False
@ -660,6 +668,7 @@ class TestModuleVersions:
mod_info = version.MODULE_INFO[module] mod_info = version.MODULE_INFO[module]
mod_info._reset_cache() mod_info._reset_cache()
idx = list(version.MODULE_INFO).index(module)
assert version._module_versions()[idx] == expected assert version._module_versions()[idx] == expected
for method_name, expected_result in [ for method_name, expected_result in [
@ -693,7 +702,16 @@ class TestModuleVersions:
assert not mod_info.is_usable() assert not mod_info.is_usable()
expected = f"adblock: {fake_version} (< {mod_info.min_version}, outdated)" expected = f"adblock: {fake_version} (< {mod_info.min_version}, outdated)"
assert version._module_versions()[4] == expected idx = list(version.MODULE_INFO).index("adblock")
assert version._module_versions()[idx] == expected
def test_importlib_not_found(self, importlib_metadata_mock: unittest.mock.Mock):
"""Test with no __version__ attribute and missing importlib.metadata."""
assert not version.MODULE_INFO["jinja2"]._version_attributes # sanity check
importlib_metadata_mock.side_effect = importlib.metadata.PackageNotFoundError
version.MODULE_INFO["jinja2"]._reset_cache()
idx = list(version.MODULE_INFO).index("jinja2")
assert version._module_versions()[idx] == "jinja2: unknown"
@pytest.mark.parametrize('attribute, expected_modules', [ @pytest.mark.parametrize('attribute, expected_modules', [
('VERSION', ['colorama']), ('VERSION', ['colorama']),
@ -722,17 +740,17 @@ class TestModuleVersions:
mod_info = version.MODULE_INFO[name] mod_info = version.MODULE_INFO[name]
if name in expected_modules: if name in expected_modules:
assert mod_info.get_version() == "1.2.3" assert mod_info.get_version() == "1.2.3"
expected.append('{}: 1.2.3'.format(name)) expected.append(f"{name}: 1.2.3")
else: else:
assert mod_info.get_version() is None assert mod_info.get_version() == "4.5.6" # from importlib.metadata
expected.append('{}: yes'.format(name)) expected.append(f"{name}: 4.5.6")
assert version._module_versions() == expected assert version._module_versions() == expected
@pytest.mark.parametrize('name, has_version', [ @pytest.mark.parametrize('name, has_version', [
('sip', False), ('sip', False),
('colorama', True), ('colorama', True),
('jinja2', True), # jinja2: removed in 3.3
('pygments', True), ('pygments', True),
('yaml', True), ('yaml', True),
('adblock', True), ('adblock', True),
@ -1136,13 +1154,7 @@ class TestChromiumVersion:
def test_prefers_saved_user_agent(self, monkeypatch, patch_no_api): def test_prefers_saved_user_agent(self, monkeypatch, patch_no_api):
webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87')) webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87'))
monkeypatch.setattr(QWebEngineProfile, "defaultProfile", lambda: 1/0)
class FakeProfile:
def defaultProfile(self):
raise AssertionError("Should not be called")
monkeypatch.setattr(webenginesettings, 'QWebEngineProfile', FakeProfile())
version.qtwebengine_versions() version.qtwebengine_versions()
def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub): def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub):
@ -1263,10 +1275,67 @@ class TestChromiumVersion:
assert versions.webengine == override assert versions.webengine == override
class FakeExtensionInfo:
def __init__(
self,
name: str,
*,
enabled: bool = False,
installed: bool = False,
loaded: bool = False,
action_popup_url: QUrl = QUrl(),
) -> None:
self._name = name
self.enabled = enabled
self.installed = installed
self.loaded = loaded
self.action_popup_url = action_popup_url
def isEnabled(self) -> bool:
return self.enabled
def isInstalled(self) -> bool:
return self.installed
def isLoaded(self) -> bool:
return self.loaded
def name(self) -> str:
return self._name
def actionPopupUrl(self) -> QUrl:
return self.action_popup_url
def path(self) -> str:
return f"{self._name}-path"
def id(self) -> str:
return f"{self._name}-id"
class FakeExtensionManager:
def __init__(self, extensions: list[FakeExtensionInfo]) -> None:
self._extensions = extensions
def extensions(self) -> list[FakeExtensionInfo]:
return self._extensions
class FakeExtensionProfile:
def __init__(self, ext_manager: FakeExtensionManager) -> None:
self._ext_manager = ext_manager
def extensionManager(self) -> FakeExtensionManager:
return self._ext_manager
@dataclasses.dataclass @dataclasses.dataclass
class VersionParams: class VersionParams:
name: str name: str
gui_platform: str = 'GUI_PLATFORM'
git_commit: bool = True git_commit: bool = True
frozen: bool = False frozen: bool = False
qapp: bool = True qapp: bool = True
@ -1287,6 +1356,8 @@ class VersionParams:
VersionParams('no-ssl', ssl_support=False), VersionParams('no-ssl', ssl_support=False),
VersionParams('no-autoconfig-loaded', autoconfig_loaded=False), VersionParams('no-autoconfig-loaded', autoconfig_loaded=False),
VersionParams('no-config-py-loaded', config_py_loaded=False), VersionParams('no-config-py-loaded', config_py_loaded=False),
VersionParams('xcb-platform', gui_platform='xcb'),
VersionParams('wayland-platform', gui_platform='wayland'),
], ids=lambda param: param.name) ], ids=lambda param: param.name)
def test_version_info(params, stubs, monkeypatch, config_stub): def test_version_info(params, stubs, monkeypatch, config_stub):
"""Test version.version_info().""" """Test version.version_info()."""
@ -1307,16 +1378,21 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
'QSslSocket': FakeQSslSocket('SSL VERSION', params.ssl_support), 'QSslSocket': FakeQSslSocket('SSL VERSION', params.ssl_support),
'platform.platform': lambda: 'PLATFORM', 'platform.platform': lambda: 'PLATFORM',
'platform.architecture': lambda: ('ARCHITECTURE', ''), 'platform.architecture': lambda: ('ARCHITECTURE', ''),
'wmname.x11_wm_name': lambda: 'X11 WM NAME',
'wmname.wayland_compositor_name': lambda: 'WAYLAND COMPOSITOR NAME',
'_os_info': lambda: ['OS INFO 1', 'OS INFO 2'], '_os_info': lambda: ['OS INFO 1', 'OS INFO 2'],
'_path_info': lambda: {'PATH DESC': 'PATH NAME'}, '_path_info': lambda: {'PATH DESC': 'PATH NAME'},
'objects.qapp': (stubs.FakeQApplication(style='STYLE', platform_name='PLATFORM') 'objects.qapp': (
if params.qapp else None), stubs.FakeQApplication(style='STYLE', platform_name=params.gui_platform)
if params.qapp
else None
),
'qtutils.library_path': (lambda _loc: 'QT PATH'), 'qtutils.library_path': (lambda _loc: 'QT PATH'),
'sql.version': lambda: 'SQLITE VERSION', 'sql.version': lambda: 'SQLITE VERSION',
'_uptime': lambda: datetime.timedelta(hours=1, minutes=23, seconds=45), '_uptime': lambda: datetime.timedelta(hours=1, minutes=23, seconds=45),
'config.instance.yaml_loaded': params.autoconfig_loaded, 'config.instance.yaml_loaded': params.autoconfig_loaded,
'machinery.INFO': machinery.SelectionInfo( 'machinery.INFO': machinery.SelectionInfo(
wrapper="QT WRAPPER", wrapper='QT WRAPPER',
reason=machinery.SelectionReason.fake reason=machinery.SelectionReason.fake
), ),
} }
@ -1324,11 +1400,23 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
version.opengl_info.cache_clear() version.opengl_info.cache_clear()
monkeypatch.setenv('QUTE_FAKE_OPENGL', 'VENDOR, 1.0 VERSION') monkeypatch.setenv('QUTE_FAKE_OPENGL', 'VENDOR, 1.0 VERSION')
if not params.qapp:
expected_gui_platform = None
elif params.gui_platform == 'GUI_PLATFORM':
expected_gui_platform = 'GUI_PLATFORM'
elif params.gui_platform == 'xcb':
expected_gui_platform = 'xcb (X11 WM NAME)'
elif params.gui_platform == 'wayland':
expected_gui_platform = 'wayland (WAYLAND COMPOSITOR NAME)'
else:
raise utils.Unreachable(params.gui_platform)
substitutions = { substitutions = {
'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '', 'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '',
'style': '\nStyle: STYLE' if params.qapp else '', 'style': '\nStyle: STYLE' if params.qapp else '',
'platform_plugin': ('\nPlatform plugin: PLATFORM' if params.qapp 'platform_plugin': (
else ''), f'\nQt Platform: {expected_gui_platform}' if params.qapp else ''
),
'opengl': '\nOpenGL: VENDOR, 1.0 VERSION' if params.qapp else '', 'opengl': '\nOpenGL: VENDOR, 1.0 VERSION' if params.qapp else '',
'qt': 'QT VERSION', 'qt': 'QT VERSION',
'frozen': str(params.frozen), 'frozen': str(params.frozen),
@ -1336,6 +1424,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
'python_path': 'EXECUTABLE PATH', 'python_path': 'EXECUTABLE PATH',
'uptime': "1:23:45", 'uptime': "1:23:45",
'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no", 'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no",
'webextensions': "", # overridden below if QtWebEngine is used
} }
patches['qtwebengine_versions'] = ( patches['qtwebengine_versions'] = (
@ -1358,6 +1447,19 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)' substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)'
else: else:
monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False) monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False)
if machinery.IS_QT6:
monkeypatch.setattr(
webenginesettings,
"default_profile",
FakeExtensionProfile(FakeExtensionManager([FakeExtensionInfo("ext1")])),
)
substitutions['webextensions'] = (
"\n"
"WebExtensions:\n"
" ext1 (ext1-id)\n"
" [ ] enabled [ ] loaded [ ] installed\n"
" ext1-path\n"
)
patches['objects.backend'] = usertypes.Backend.QtWebEngine patches['objects.backend'] = usertypes.Backend.QtWebEngine
substitutions['backend'] = 'QtWebEngine 1.2.3\n (source: faked)' substitutions['backend'] = 'QtWebEngine 1.2.3\n (source: faked)'
@ -1397,7 +1499,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
pdf.js: PDFJS VERSION pdf.js: PDFJS VERSION
sqlite: SQLITE VERSION sqlite: SQLITE VERSION
QtNetwork SSL: {ssl} QtNetwork SSL: {ssl}
{style}{platform_plugin}{opengl} {webextensions}{style}{platform_plugin}{opengl}
Platform: PLATFORM, ARCHITECTURE{linuxdist} Platform: PLATFORM, ARCHITECTURE{linuxdist}
Frozen: {frozen} Frozen: {frozen}
Imported from {import_path} Imported from {import_path}
@ -1482,6 +1584,108 @@ class TestOpenGLInfo:
assert str(info) == 'OpenGL ES' assert str(info) == 'OpenGL ES'
@pytest.mark.skipif(
not machinery.IS_QT6, reason="extensions are only available with Qt6"
)
class TestWebEngineExtensions:
def test_qtwebkit(self, monkeypatch: pytest.MonkeyPatch) -> None:
assert webenginesettings.default_profile is None # -> default_qt_profile() used
monkeypatch.setattr(version.objects, "backend", usertypes.Backend.QtWebKit)
monkeypatch.setattr(webenginesettings, "default_qt_profile", lambda: 1 / 0)
assert not version._webengine_extensions()
def test_avoid_chromium_init(self, monkeypatch: pytest.MonkeyPatch) -> None:
assert webenginesettings.default_profile is None # -> default_qt_profile() used
monkeypatch.setattr(version.objects, "backend", usertypes.Backend.QtWebEngine)
monkeypatch.setattr(objects, "debug_flags", {"avoid-chromium-init"})
monkeypatch.setattr(webenginesettings, "default_qt_profile", lambda: 1 / 0)
assert version._webengine_extensions() == [
"WebExtensions: unknown (avoiding init)"
]
def test_no_extension_manager(self, monkeypatch: pytest.MonkeyPatch) -> None:
assert webenginesettings.default_profile is None # -> default_qt_profile() used
monkeypatch.setattr(webenginesettings, "default_qt_profile", object)
assert not version._webengine_extensions()
@pytest.mark.parametrize("avoid_init", [True, False])
def test_preexisting_profile(self, monkeypatch: pytest.MonkeyPatch, avoid_init: bool) -> None:
"""Test that we use the pre-existing profile if available."""
monkeypatch.setattr(webenginesettings, "default_profile", FakeExtensionProfile(FakeExtensionManager([])))
if avoid_init:
monkeypatch.setattr(objects, "debug_flags", {"avoid-chromium-init"})
result = version._webengine_extensions()
assert result == ["WebExtensions: none"]
@pytest.mark.parametrize(
"extensions, expected",
[
pytest.param([], ["WebExtensions: none"], id="empty"),
pytest.param(
[FakeExtensionInfo("ext1")],
[
"WebExtensions:",
" ext1 (ext1-id)",
" [ ] enabled [ ] loaded [ ] installed",
" ext1-path",
"",
],
id="single",
),
pytest.param(
[
FakeExtensionInfo("ext1", enabled=True),
FakeExtensionInfo(
"ext2", enabled=True, loaded=True, installed=True
),
],
[
"WebExtensions:",
" ext1 (ext1-id)",
" [x] enabled [ ] loaded [ ] installed",
" ext1-path",
"",
" ext2 (ext2-id)",
" [x] enabled [x] loaded [x] installed",
" ext2-path",
"",
],
id="multiple",
),
pytest.param(
[
FakeExtensionInfo(
"ext", action_popup_url=QUrl("chrome-extension://ext")
)
],
[
"WebExtensions:",
" ext (ext-id)",
" [ ] enabled [ ] loaded [ ] installed",
" ext-path",
" chrome-extension://ext",
"",
],
id="with-url",
),
],
)
def test_extensions(
self,
monkeypatch: pytest.MonkeyPatch,
extensions: list[FakeExtensionInfo],
expected: list[str],
) -> None:
monkeypatch.setattr(
webenginesettings,
"default_profile",
FakeExtensionProfile(FakeExtensionManager(extensions)),
)
assert version._webengine_extensions() == expected
@pytest.fixture @pytest.fixture
def pbclient(stubs): def pbclient(stubs):
http_stub = stubs.HTTPPostStub() http_stub = stubs.HTTPPostStub()
@ -1536,6 +1740,46 @@ def test_pastebin_version_error(pbclient, caplog, message_mock, monkeypatch):
assert msg.text == "Failed to pastebin version info: test" assert msg.text == "Failed to pastebin version info: test"
@pytest.mark.parametrize("platform, expected", [
("windows", "windows"),
("xcb", "xcb (X11 WM NAME)"),
("wayland", "wayland (WAYLAND COMPOSITOR NAME)"),
("wayland-egl", "wayland-egl (WAYLAND COMPOSITOR NAME)"),
])
def test_gui_platform_info(
platform: str, expected: str, monkeypatch: pytest.MonkeyPatch, stubs: Any
) -> None:
monkeypatch.setattr(
version.objects,
"qapp",
stubs.FakeQApplication(platform_name=platform, style="STYLE"),
)
monkeypatch.setattr(version.wmname, "x11_wm_name", lambda: "X11 WM NAME")
monkeypatch.setattr(
version.wmname, "wayland_compositor_name", lambda: "WAYLAND COMPOSITOR NAME"
)
assert version.gui_platform_info() == expected
@pytest.mark.parametrize("platform", ["xcb", "wayland", "wayland-egl"])
def test_gui_platform_info_error(
platform: str,
monkeypatch: pytest.MonkeyPatch,
mocker: pytest_mock.MockerFixture,
stubs: Any,
) -> None:
monkeypatch.setattr(
version.objects,
"qapp",
stubs.FakeQApplication(platform_name=platform, style="STYLE"),
)
mocker.patch.object(wmname, "x11_wm_name", side_effect=wmname.Error("fake error"))
mocker.patch.object(
wmname, "wayland_compositor_name", side_effect=wmname.Error("fake error")
)
assert version.gui_platform_info() == f"{platform} (Error: fake error)"
def test_uptime(monkeypatch, qapp): def test_uptime(monkeypatch, qapp):
"""Test _uptime runs and check if microseconds are dropped.""" """Test _uptime runs and check if microseconds are dropped."""
monkeypatch.setattr(objects, 'qapp', qapp) monkeypatch.setattr(objects, 'qapp', qapp)

19
tox.ini
View File

@ -4,7 +4,7 @@
# and then run "tox" from this directory. # and then run "tox" from this directory.
[tox] [tox]
envlist = py39-pyqt515-cov,mypy-pyqt5,misc,vulture,flake8,pylint,pyroma,check-manifest,eslint,yamllint,actionlint envlist = py39-pyqt515-cov,mypy-pyqt5,misc,vulture,flake8,pylint,pyroma,eslint,yamllint,actionlint
distshare = {toxworkdir} distshare = {toxworkdir}
skipsdist = true skipsdist = true
minversion = 3.20 minversion = 3.20
@ -57,8 +57,9 @@ deps =
pyqt67: -r{toxinidir}/misc/requirements/requirements-pyqt-6.7.txt pyqt67: -r{toxinidir}/misc/requirements/requirements-pyqt-6.7.txt
pyqt68: -r{toxinidir}/misc/requirements/requirements-pyqt-6.8.txt pyqt68: -r{toxinidir}/misc/requirements/requirements-pyqt-6.8.txt
pyqt69: -r{toxinidir}/misc/requirements/requirements-pyqt-6.9.txt pyqt69: -r{toxinidir}/misc/requirements/requirements-pyqt-6.9.txt
pyqt610: -r{toxinidir}/misc/requirements/requirements-pyqt-6.10.txt
commands = commands =
!pyqt-!pyqt515-!pyqt5152-!pyqt62-!pyqt63-!pyqt64-!pyqt65-!pyqt66-!pyqt67-!pyqt68-!pyqt69: {envpython} scripts/link_pyqt.py --tox {envdir} !pyqt-!pyqt515-!pyqt5152-!pyqt62-!pyqt63-!pyqt64-!pyqt65-!pyqt66-!pyqt67-!pyqt68-!pyqt69-!pyqt610: {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -bb -m pytest {posargs:tests} {envpython} -bb -m pytest {posargs:tests}
cov: {envpython} scripts/dev/check_coverage.py {posargs} cov: {envpython} scripts/dev/check_coverage.py {posargs}
@ -167,15 +168,7 @@ passenv =
deps = deps =
-r{toxinidir}/misc/requirements/requirements-pyroma.txt -r{toxinidir}/misc/requirements/requirements-pyroma.txt
commands = commands =
{envdir}/bin/pyroma . {envdir}/bin/pyroma -n 10 .
[testenv:check-manifest]
basepython = {env:PYTHON:python3}
passenv =
deps =
-r{toxinidir}/misc/requirements/requirements-check-manifest.txt
commands =
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,qutebrowser/html/doc/img/cheatsheet-*.png,*/__pycache__'
[testenv:docs] [testenv:docs]
basepython = {env:PYTHON:python3} basepython = {env:PYTHON:python3}
@ -221,14 +214,14 @@ deps =
allowlist_externals = bash allowlist_externals = bash
commands = bash scripts/dev/run_shellcheck.sh {posargs} commands = bash scripts/dev/run_shellcheck.sh {posargs}
[testenv:mypy-{pyqt5,pyqt6}] [testenv:mypy{,-pyqt5,-pyqt6}]
basepython = {env:PYTHON:python3} basepython = {env:PYTHON:python3}
passenv = passenv =
TERM TERM
MYPY_FORCE_TERMINAL_WIDTH MYPY_FORCE_TERMINAL_WIDTH
setenv = setenv =
# See qutebrowser/qt/machinery.py # See qutebrowser/qt/machinery.py
pyqt6: QUTE_CONSTANTS_ARGS=--always-true=USE_PYQT6 --always-false=USE_PYQT5 --always-false=USE_PYSIDE6 --always-false=IS_QT5 --always-true=IS_QT6 --always-true=IS_PYQT --always-false=IS_PYSIDE !pyqt5: QUTE_CONSTANTS_ARGS=--always-true=USE_PYQT6 --always-false=USE_PYQT5 --always-false=USE_PYSIDE6 --always-false=IS_QT5 --always-true=IS_QT6 --always-true=IS_PYQT --always-false=IS_PYSIDE
pyqt5: QUTE_CONSTANTS_ARGS=--always-false=USE_PYQT6 --always-true=USE_PYQT5 --always-false=USE_PYSIDE6 --always-true=IS_QT5 --always-false=IS_QT6 --always-true=IS_PYQT --always-false=IS_PYSIDE pyqt5: QUTE_CONSTANTS_ARGS=--always-false=USE_PYQT6 --always-true=USE_PYQT5 --always-false=USE_PYSIDE6 --always-true=IS_QT5 --always-false=IS_QT6 --always-true=IS_PYQT --always-false=IS_PYSIDE
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt