Merge branch 'qutebrowser:main' into feature_contexthinter
This commit is contained in:
commit
d74d434c76
|
|
@ -1,5 +1,5 @@
|
|||
[tool.bumpversion]
|
||||
current_version = "3.5.1"
|
||||
current_version = "3.6.1"
|
||||
commit = true
|
||||
message = "Release v{new_version}"
|
||||
tag = true
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ include =
|
|||
tests/*
|
||||
scripts/*
|
||||
branch = true
|
||||
patch = subprocess
|
||||
omit =
|
||||
qutebrowser/__main__.py
|
||||
*/__init__.py
|
||||
|
|
|
|||
3
.flake8
3
.flake8
|
|
@ -42,6 +42,7 @@ exclude = .*,__pycache__,resources.py
|
|||
# W503: like break before binary operator
|
||||
# W504: line break after binary operator
|
||||
# FI18: __future__ import "annotations" missing
|
||||
# FI58: __future__ import "annotations" present
|
||||
# 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
|
||||
# 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,
|
||||
A003,
|
||||
W503, W504,
|
||||
FI18,
|
||||
FI18,FI58,
|
||||
PT004,
|
||||
PT011,
|
||||
PT012
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ jobs:
|
|||
shell: bash
|
||||
if: failure()
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
|
||||
path: |
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ jobs:
|
|||
.tox
|
||||
~/.cache/pip
|
||||
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:
|
||||
python-version: '3.10'
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
if: "matrix.testenv == 'eslint'"
|
||||
|
|
@ -125,7 +125,7 @@ jobs:
|
|||
shell: bash
|
||||
if: failure()
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
|
||||
path: |
|
||||
|
|
@ -190,31 +190,35 @@ jobs:
|
|||
- testenv: py313-pyqt68
|
||||
os: ubuntu-24.04
|
||||
python: "3.13"
|
||||
### PyQt 6.8 (Python 3.14)
|
||||
- testenv: py314-pyqt68
|
||||
os: ubuntu-24.04
|
||||
python: "3.14-dev"
|
||||
### PyQt 6.9 (Python 3.13)
|
||||
- testenv: py313-pyqt69
|
||||
### PyQt 6.8 (Python 3.13)
|
||||
- testenv: py313-pyqt68
|
||||
os: ubuntu-24.04
|
||||
python: "3.13"
|
||||
### macOS Ventura
|
||||
- testenv: py313-pyqt69
|
||||
os: macos-13
|
||||
python: "3.13"
|
||||
args: "tests/unit" # Only run unit tests on macOS
|
||||
### PyQt 6.9 (Python 3.14)
|
||||
- testenv: py314-pyqt69
|
||||
os: ubuntu-24.04
|
||||
python: "3.14"
|
||||
### PyQt 6.10 (Python 3.14)
|
||||
- testenv: py314-pyqt610
|
||||
os: ubuntu-24.04
|
||||
python: "3.14"
|
||||
### macOS Sonoma (M1 runner)
|
||||
- testenv: py313-pyqt69
|
||||
- testenv: py314-pyqt610
|
||||
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
|
||||
### Windows
|
||||
- testenv: py313-pyqt69
|
||||
- testenv: py314-pyqt610
|
||||
os: windows-2022
|
||||
python: "3.13"
|
||||
- testenv: py313-pyqt69
|
||||
python: "3.14"
|
||||
- testenv: py314-pyqt610
|
||||
os: windows-2025
|
||||
python: "3.13"
|
||||
python: "3.14"
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
|
@ -228,7 +232,7 @@ jobs:
|
|||
~/.cache/pip
|
||||
key: "${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "${{ matrix.python }}"
|
||||
- name: Set up problem matchers
|
||||
|
|
@ -269,7 +273,7 @@ jobs:
|
|||
shell: bash
|
||||
if: failure()
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.testenv }}-${{ matrix.os }}"
|
||||
path: |
|
||||
|
|
@ -289,12 +293,12 @@ jobs:
|
|||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: javascript, python
|
||||
queries: +security-extended
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
irc:
|
||||
timeout-minutes: 2
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- archlinux-webengine-qt6
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- run: pip install jinja2
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-13
|
||||
- os: macos-15-intel
|
||||
toxenv: build-release
|
||||
name: macos-intel
|
||||
- os: macos-14
|
||||
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
- os: windows-latest
|
||||
toxenv: build-release
|
||||
name: windows
|
||||
- os: macos-13
|
||||
- os: macos-15-intel
|
||||
args: --debug
|
||||
toxenv: build-release
|
||||
name: macos-debug-intel
|
||||
|
|
@ -41,9 +41,18 @@ jobs:
|
|||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
python -m pip install -U pip
|
||||
|
|
@ -63,7 +72,7 @@ jobs:
|
|||
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: "qutebrowser-nightly-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.name }}"
|
||||
path: |
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Recompile requirements
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ on:
|
|||
- 'patch'
|
||||
- 'minor'
|
||||
- 'major'
|
||||
- 'reupload' # reupload last release
|
||||
# FIXME do we want a possibility to do prereleases here?
|
||||
python_version:
|
||||
description: 'Python version'
|
||||
required: true
|
||||
default: '3.13'
|
||||
default: '3.14'
|
||||
type: choice
|
||||
options:
|
||||
- '3.9'
|
||||
|
|
@ -24,18 +25,20 @@ on:
|
|||
- '3.11'
|
||||
- '3.12'
|
||||
- '3.13'
|
||||
- '3.14'
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
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:
|
||||
contents: write # To push release commit/tag
|
||||
steps:
|
||||
- name: Find release branch
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
id: find-branch
|
||||
with:
|
||||
script: |
|
||||
|
|
@ -61,7 +64,7 @@ jobs:
|
|||
result-encoding: string
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
# Doesn't really matter what we prepare the release with, but let's
|
||||
# use the same version for consistency.
|
||||
|
|
@ -83,9 +86,9 @@ jobs:
|
|||
gpg --import <<< "${{ secrets.QUTEBROWSER_BOT_GPGKEY }}"
|
||||
- name: Bump version
|
||||
id: bump
|
||||
run: "tox -e update-version -- ${{ github.event.inputs.release_type }}"
|
||||
run: "tox -e update-version -- ${{ inputs.release_type }}"
|
||||
- name: Check milestone
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const milestones = await github.paginate(github.rest.issues.listMilestones, {
|
||||
|
|
@ -100,35 +103,59 @@ jobs:
|
|||
core.setFailed(`Found open milestone ${milestone.title} with ${milestone.open_issues} open and ${milestone.closed_issues} closed issues!`);
|
||||
}
|
||||
- name: Push release commit/tag
|
||||
if: ${{ inputs.release_type != 'reupload' }}
|
||||
run: |
|
||||
git push origin ${{ steps.find-branch.outputs.result }}
|
||||
git push origin v${{ steps.bump.outputs.version }}
|
||||
- name: Cherry-pick release commit
|
||||
if: ${{ github.event.inputs.release_type == 'patch' }}
|
||||
if: ${{ inputs.release_type == 'patch' }}
|
||||
run: |
|
||||
git checkout main
|
||||
git cherry-pick -x v${{ steps.bump.outputs.version }}
|
||||
git push origin main
|
||||
git checkout v${{ steps.bump.outputs.version_x }}
|
||||
- name: Create release branch
|
||||
if: ${{ github.event.inputs.release_type != 'patch' }}
|
||||
if: ${{ inputs.release_type == 'minor' || inputs.release_type == 'major' }}
|
||||
run: |
|
||||
git checkout -b v${{ steps.bump.outputs.version_x }}
|
||||
git push --set-upstream origin v${{ steps.bump.outputs.version_x }}
|
||||
- name: Create GitHub draft release
|
||||
if: ${{ inputs.release_type != 'reupload' }}
|
||||
id: create-release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.bump.outputs.version }}
|
||||
draft: true
|
||||
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:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-13
|
||||
- os: macos-14
|
||||
- os: windows-2019
|
||||
- os: macos-14-large # Intel
|
||||
- os: macos-14 # Apple Silicon
|
||||
- os: windows-2022
|
||||
- os: ubuntu-24.04
|
||||
runs-on: "${{ matrix.os }}"
|
||||
timeout-minutes: 45
|
||||
|
|
@ -138,11 +165,11 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
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
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ github.event.inputs.python_version }}
|
||||
python-version: ${{ inputs.python_version }}
|
||||
- name: Import GPG Key
|
||||
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
|
||||
run: |
|
||||
|
|
@ -167,7 +194,7 @@ jobs:
|
|||
# FIXME consider switching to trusted publishers:
|
||||
# https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/
|
||||
- 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:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.QUTEBROWSER_BOT_PYPI_TOKEN }}
|
||||
|
|
@ -180,7 +207,7 @@ jobs:
|
|||
contents: write # To change release
|
||||
steps:
|
||||
- name: Publish final release
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
await github.rest.repos.updateRelease({
|
||||
|
|
|
|||
|
|
@ -15,34 +15,88 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
|||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
[[v3.7.0]]
|
||||
v3.7.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- When switching between normal/fullscreen tabs, the fullscreen state now gets
|
||||
left/restored properly. (#5283)
|
||||
|
||||
[[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 (unreleased)
|
||||
v3.6.0 (2025-10-24)
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- The `:version` info now shows the X11 window manager / Wayland compositor name
|
||||
(mostly useful for bug/crash reports)
|
||||
- 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.
|
||||
|
||||
[[v3.5.2]]
|
||||
v3.5.2 (unreleased)
|
||||
-------------------
|
||||
- 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
|
||||
~~~~~
|
||||
|
||||
- The package version for Jinja 3.3+ is now correctly displayed in `:version`.
|
||||
- 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 (2025-06-05)
|
||||
|
|
|
|||
|
|
@ -802,7 +802,8 @@ qutebrowser release
|
|||
**Automatic release via GitHub Actions (starting with v3.0.0):**
|
||||
|
||||
* 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:**
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ Why Python?::
|
|||
point, I wasn't comfortable with C++ so that wasn't an alternative.
|
||||
|
||||
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
|
||||
QtWebKit/QtWebEngine in C++, with the
|
||||
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.
|
||||
+
|
||||
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.
|
||||
+
|
||||
There is a very useful script for mpv, which emulates "unique application"
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ customizable for a given <<patterns,URL patterns>>.
|
|||
|
||||
[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
|
||||
|
|
@ -187,7 +187,7 @@ similar to `c.` which is scoped to the given domain:
|
|||
|
||||
[source,python]
|
||||
----
|
||||
with config.pattern('*://example.com/') as p:
|
||||
with config.pattern('*://example.com/*') as p:
|
||||
p.content.images = False
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -2770,6 +2770,7 @@ Valid values:
|
|||
|
||||
* +ua-google+
|
||||
* +ua-googledocs+
|
||||
* +ua-gnome-gitlab+
|
||||
* +js-whatsapp-web+
|
||||
* +js-discord+
|
||||
* +js-string-replaceall+
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@
|
|||
</content_rating>
|
||||
<releases>
|
||||
<!-- Add new releases here -->
|
||||
<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.4.0" date="2024-12-14"/>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
annotated-types==0.7.0
|
||||
anyio==4.10.0
|
||||
anyio==4.11.0
|
||||
autocommand==2.2.2
|
||||
backports.tarfile==1.2.0
|
||||
bracex==2.6
|
||||
build==1.3.0
|
||||
bump-my-version==1.2.1
|
||||
certifi==2025.8.3
|
||||
cffi==1.17.1
|
||||
charset-normalizer==3.4.3
|
||||
bump-my-version==1.2.4
|
||||
certifi==2025.10.5
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.1.8
|
||||
cryptography==45.0.6
|
||||
docutils==0.22
|
||||
cryptography==46.0.3
|
||||
docutils==0.22.2
|
||||
exceptiongroup==1.3.0
|
||||
github3.py==4.0.1
|
||||
h11==0.16.0
|
||||
|
|
@ -20,7 +20,7 @@ httpcore==1.0.9
|
|||
httpx==0.28.1
|
||||
hunter==3.9.0
|
||||
id==1.5.0
|
||||
idna==3.10
|
||||
idna==3.11
|
||||
importlib_metadata==8.7.0
|
||||
importlib_resources==6.5.2
|
||||
inflect==7.3.1
|
||||
|
|
@ -34,41 +34,41 @@ keyring==25.6.0
|
|||
manhole==1.8.1
|
||||
markdown-it-py==3.0.0
|
||||
mdurl==0.1.2
|
||||
more-itertools==10.7.0
|
||||
nh3==0.3.0
|
||||
more-itertools==10.8.0
|
||||
nh3==0.3.2
|
||||
packaging==25.0
|
||||
platformdirs==4.3.8
|
||||
prompt_toolkit==3.0.51
|
||||
pycparser==2.22
|
||||
pydantic==2.11.7
|
||||
pydantic-settings==2.10.1
|
||||
pydantic_core==2.33.2
|
||||
platformdirs==4.4.0
|
||||
prompt_toolkit==3.0.52
|
||||
pycparser==2.23
|
||||
pydantic==2.12.3
|
||||
pydantic-settings==2.11.0
|
||||
pydantic_core==2.41.4
|
||||
Pygments==2.19.2
|
||||
PyJWT==2.10.1
|
||||
Pympler==1.1
|
||||
pyproject_hooks==1.2.0
|
||||
PyQt-builder==1.18.2
|
||||
PyQt-builder==1.19.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.1.1
|
||||
questionary==2.1.0
|
||||
python-dotenv==1.2.1
|
||||
questionary==2.1.1
|
||||
readme_renderer==44.0
|
||||
requests==2.32.5
|
||||
requests-toolbelt==1.0.0
|
||||
rfc3986==2.0.0
|
||||
rich==14.1.0
|
||||
rich-click==1.8.9
|
||||
rich==14.2.0
|
||||
rich-click==1.9.4
|
||||
SecretStorage==3.3.3
|
||||
sip==6.12.0
|
||||
sip==6.14.0
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
tomli==2.2.1
|
||||
tomli==2.3.0
|
||||
tomlkit==0.13.3
|
||||
twine==6.1.0
|
||||
twine==6.2.0
|
||||
typeguard==4.3.0
|
||||
typing-inspection==0.4.1
|
||||
typing_extensions==4.14.1
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
uritemplate==4.2.0
|
||||
# urllib3==2.5.0
|
||||
wcmatch==10.1
|
||||
wcwidth==0.2.13
|
||||
wcwidth==0.2.14
|
||||
zipp==3.23.0
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==25.3.0
|
||||
attrs==25.4.0
|
||||
flake8==7.3.0
|
||||
flake8-bugbear==24.12.12
|
||||
flake8-builtins==3.0.0
|
||||
flake8-comprehensions==3.16.0
|
||||
flake8-comprehensions==3.17.0
|
||||
flake8-debugger==4.1.2
|
||||
flake8-deprecated==2.2.1
|
||||
flake8-docstrings==1.7.0
|
||||
|
|
@ -12,7 +12,7 @@ flake8-future-import==0.4.7
|
|||
flake8-plugin-utils==1.3.3
|
||||
flake8-pytest-style==2.1.0
|
||||
flake8-string-format==0.3.0
|
||||
flake8-tidy-imports==4.11.0
|
||||
flake8-tidy-imports==4.12.0
|
||||
flake8-tuple==0.4.1
|
||||
mccabe==0.7.0
|
||||
pep8-naming==0.15.1
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
chardet==5.2.0
|
||||
diff_cover==9.6.0
|
||||
diff_cover==9.7.1
|
||||
Jinja2==3.1.6
|
||||
lxml==6.0.1
|
||||
MarkupSafe==3.0.2
|
||||
mypy==1.17.1
|
||||
lxml==6.0.2
|
||||
MarkupSafe==3.0.3
|
||||
mypy==1.18.2
|
||||
mypy_extensions==1.1.0
|
||||
pathspec==0.12.1
|
||||
pluggy==1.6.0
|
||||
Pygments==2.19.2
|
||||
PyQt5-stubs==5.15.6.0
|
||||
tomli==2.2.1
|
||||
tomli==2.3.0
|
||||
types-colorama==0.4.15.20250801
|
||||
types-docutils==0.22.0.20250822
|
||||
types-docutils==0.22.2.20251006
|
||||
types-Pygments==2.19.0.20250809
|
||||
types-PyYAML==6.0.12.20250822
|
||||
typing_extensions==4.14.1
|
||||
types-PyYAML==6.0.12.20250915
|
||||
typing_extensions==4.15.0
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
altgraph==0.17.4
|
||||
importlib_metadata==8.7.0
|
||||
packaging==25.0
|
||||
pyinstaller==6.15.0
|
||||
pyinstaller-hooks-contrib==2025.8
|
||||
pyinstaller==6.16.0
|
||||
pyinstaller-hooks-contrib==2025.9
|
||||
zipp==3.23.0
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==3.3.11
|
||||
certifi==2025.8.3
|
||||
cffi==1.17.1
|
||||
charset-normalizer==3.4.3
|
||||
cryptography==45.0.6
|
||||
certifi==2025.10.5
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.4
|
||||
cryptography==46.0.3
|
||||
dill==0.4.0
|
||||
github3.py==4.0.1
|
||||
idna==3.10
|
||||
isort==6.0.1
|
||||
idna==3.11
|
||||
importlib_metadata==8.7.0
|
||||
isort==6.1.0
|
||||
mccabe==0.7.0
|
||||
pefile==2024.8.26
|
||||
platformdirs==4.3.8
|
||||
pycparser==2.22
|
||||
platformdirs==4.4.0
|
||||
pycparser==2.23
|
||||
PyJWT==2.10.1
|
||||
pylint==3.3.8
|
||||
pylint==3.3.9
|
||||
python-dateutil==2.9.0.post0
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.32.5
|
||||
six==1.17.0
|
||||
tomli==2.2.1
|
||||
tomli==2.3.0
|
||||
tomlkit==0.13.3
|
||||
typing_extensions==4.14.1
|
||||
typing_extensions==4.15.0
|
||||
uritemplate==4.2.0
|
||||
# urllib3==2.5.0
|
||||
zipp==3.23.0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
PyQt5==5.15.11 # rq.filter: < 5.16
|
||||
PyQt5-Qt5==5.15.17
|
||||
PyQt5_sip==12.17.0
|
||||
PyQt5_sip==12.17.1
|
||||
PyQtWebEngine==5.15.7 # rq.filter: < 5.16
|
||||
PyQtWebEngine-Qt5==5.15.17
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
PyQt5==5.15.11
|
||||
PyQt5-Qt5==5.15.17
|
||||
PyQt5_sip==12.17.0
|
||||
PyQt5_sip==12.17.1
|
||||
PyQtWebEngine==5.15.7
|
||||
PyQtWebEngine-Qt5==5.15.17
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt6==6.10.0
|
||||
PyQt6-Qt6==6.10.0
|
||||
PyQt6-WebEngine==6.10.0
|
||||
PyQt6-WebEngine-Qt6==6.10.0
|
||||
PyQt6_sip==13.10.2
|
||||
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/
|
||||
|
|
@ -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/
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt6==6.9.1
|
||||
PyQt6-Qt6==6.9.1
|
||||
PyQt6-Qt6==6.9.2
|
||||
PyQt6-WebEngine==6.9.0
|
||||
PyQt6-WebEngine-Qt6==6.9.1
|
||||
PyQt6-WebEngine-Qt6==6.9.2
|
||||
PyQt6_sip==13.10.2
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt6==6.9.1
|
||||
PyQt6-Qt6==6.9.1
|
||||
PyQt6-WebEngine==6.9.0
|
||||
PyQt6-WebEngine-Qt6==6.9.1
|
||||
PyQt6==6.10.0
|
||||
PyQt6-Qt6==6.10.0
|
||||
PyQt6-WebEngine==6.10.0
|
||||
PyQt6-WebEngine-Qt6==6.10.0
|
||||
PyQt6_sip==13.10.2
|
||||
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/
|
||||
|
|
|
|||
|
|
@ -2,3 +2,7 @@ PyQt6
|
|||
PyQt6-Qt6
|
||||
PyQt6-WebEngine
|
||||
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/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt6==6.9.1
|
||||
PyQt6-Qt6==6.9.1
|
||||
PyQt6-WebEngine==6.9.0
|
||||
PyQt6-WebEngine-Qt6==6.9.1
|
||||
PyQt6==6.10.0
|
||||
PyQt6-Qt6==6.10.0
|
||||
PyQt6-WebEngine==6.10.0
|
||||
PyQt6-WebEngine-Qt6==6.10.0
|
||||
PyQt6_sip==13.10.2
|
||||
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/
|
||||
|
|
|
|||
|
|
@ -2,3 +2,7 @@ PyQt6
|
|||
PyQt6-Qt6
|
||||
PyQt6-WebEngine
|
||||
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/
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
build==1.3.0
|
||||
certifi==2025.8.3
|
||||
charset-normalizer==3.4.3
|
||||
check-manifest==0.50
|
||||
docutils==0.22
|
||||
idna==3.10
|
||||
certifi==2025.10.5
|
||||
charset-normalizer==3.4.4
|
||||
check-manifest==0.51
|
||||
docutils==0.22.2
|
||||
idna==3.11
|
||||
importlib_metadata==8.7.0
|
||||
packaging==25.0
|
||||
Pygments==2.19.2
|
||||
pyproject_hooks==1.2.0
|
||||
pyroma==5.0
|
||||
requests==2.32.5
|
||||
tomli==2.2.1
|
||||
trove-classifiers==2025.8.6.13
|
||||
tomli==2.3.0
|
||||
trove-classifiers==2025.9.11.17
|
||||
urllib3==2.5.0
|
||||
zipp==3.23.0
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
alabaster==0.7.16
|
||||
babel==2.17.0
|
||||
certifi==2025.8.3
|
||||
charset-normalizer==3.4.3
|
||||
certifi==2025.10.5
|
||||
charset-normalizer==3.4.4
|
||||
docutils==0.21.2
|
||||
idna==3.10
|
||||
idna==3.11
|
||||
imagesize==1.4.1
|
||||
importlib_metadata==8.7.0
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.2
|
||||
MarkupSafe==3.0.3
|
||||
packaging==25.0
|
||||
Pygments==2.19.2
|
||||
requests==2.32.5
|
||||
|
|
@ -21,6 +21,6 @@ sphinxcontrib-htmlhelp==2.1.0
|
|||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==2.0.0
|
||||
sphinxcontrib-serializinghtml==2.0.0
|
||||
tomli==2.2.1
|
||||
tomli==2.3.0
|
||||
urllib3==2.5.0
|
||||
zipp==3.23.0
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==25.3.0
|
||||
attrs==25.4.0
|
||||
autocommand==2.2.2
|
||||
backports.tarfile==1.2.0
|
||||
beautifulsoup4==4.13.5
|
||||
beautifulsoup4==4.14.2
|
||||
blinker==1.9.0
|
||||
certifi==2025.8.3
|
||||
charset-normalizer==3.4.3
|
||||
cheroot==10.0.1
|
||||
certifi==2025.10.5
|
||||
charset-normalizer==3.4.4
|
||||
cheroot==11.1.0
|
||||
click==8.1.8
|
||||
coverage==7.10.5
|
||||
coverage==7.10.7
|
||||
exceptiongroup==1.3.0
|
||||
execnet==2.1.1
|
||||
filelock==3.19.1
|
||||
Flask==3.1.2
|
||||
gherkin-official==29.0.0
|
||||
hunter==3.9.0
|
||||
hypothesis==6.138.3
|
||||
idna==3.10
|
||||
hypothesis==6.141.1
|
||||
idna==3.11
|
||||
importlib_metadata==8.7.0
|
||||
importlib_resources==6.5.2
|
||||
inflect==7.3.1
|
||||
|
|
@ -30,37 +30,37 @@ jaraco.text==3.12.1
|
|||
# Jinja2==3.1.6
|
||||
Mako==1.3.10
|
||||
manhole==1.8.1
|
||||
# MarkupSafe==3.0.2
|
||||
more-itertools==10.7.0
|
||||
# MarkupSafe==3.0.3
|
||||
more-itertools==10.8.0
|
||||
packaging==25.0
|
||||
parse==1.20.2
|
||||
parse_type==0.6.6
|
||||
pillow==11.3.0
|
||||
platformdirs==4.3.8
|
||||
platformdirs==4.4.0
|
||||
pluggy==1.6.0
|
||||
py-cpuinfo==9.0.0
|
||||
Pygments==2.19.2
|
||||
pytest==8.4.1
|
||||
pytest==8.4.2
|
||||
pytest-bdd==8.1.0
|
||||
pytest-benchmark==5.1.0
|
||||
pytest-cov==6.2.1
|
||||
pytest-benchmark==5.2.0
|
||||
pytest-cov==7.0.0
|
||||
pytest-instafail==0.5.0
|
||||
pytest-mock==3.14.1
|
||||
pytest-mock==3.15.1
|
||||
pytest-qt==4.5.0
|
||||
pytest-repeat==0.9.4
|
||||
pytest-rerunfailures==15.1
|
||||
pytest-rerunfailures==16.0.1
|
||||
pytest-xdist==3.8.0
|
||||
pytest-xvfb==3.1.1
|
||||
PyVirtualDisplay==3.0
|
||||
requests==2.32.5
|
||||
requests-file==2.1.0
|
||||
requests-file==3.0.1
|
||||
six==1.17.0
|
||||
sortedcontainers==2.4.0
|
||||
soupsieve==2.7
|
||||
soupsieve==2.8
|
||||
tldextract==5.3.0
|
||||
tomli==2.2.1
|
||||
tomli==2.3.0
|
||||
typeguard==4.3.0
|
||||
typing_extensions==4.14.1
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.5.0
|
||||
vulture==2.14
|
||||
Werkzeug==3.1.3
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
cachetools==6.1.0
|
||||
cachetools==6.2.1
|
||||
chardet==5.2.0
|
||||
colorama==0.4.6
|
||||
distlib==0.4.0
|
||||
filelock==3.19.1
|
||||
packaging==25.0
|
||||
pip==25.2
|
||||
platformdirs==4.3.8
|
||||
pip==25.3
|
||||
platformdirs==4.4.0
|
||||
pluggy==1.6.0
|
||||
pyproject-api==1.9.1
|
||||
setuptools==80.9.0
|
||||
tomli==2.2.1
|
||||
tox==4.28.4 ; python_full_version!="3.14.0b1"
|
||||
typing_extensions==4.14.1
|
||||
virtualenv==20.34.0
|
||||
tomli==2.3.0
|
||||
tox==4.30.3 ; python_full_version!="3.14.0b1"
|
||||
typing_extensions==4.15.0
|
||||
virtualenv==20.35.4
|
||||
wheel==0.45.1
|
||||
tox @ git+https://github.com/tox-dev/tox ; python_full_version=="3.14.0b1"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
tomli==2.2.1
|
||||
tomli==2.3.0
|
||||
vulture==2.14
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
pathspec==0.12.1
|
||||
PyYAML==6.0.2
|
||||
PyYAML==6.0.3
|
||||
yamllint==1.37.1
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
[pytest]
|
||||
pythonpath = .
|
||||
log_level = NOTSET
|
||||
addopts = --strict-markers --strict-config --instafail --benchmark-columns=Min,Max,Median
|
||||
testpaths = tests
|
||||
|
|
@ -91,6 +92,9 @@ qt_log_ignore =
|
|||
^Unable to detect GPU vendor\.$
|
||||
# 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\)$
|
||||
# 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
|
||||
filterwarnings =
|
||||
error
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ __copyright__ = "Copyright 2013-{} Florian Bruhin (The Compiler)".format(_year)
|
|||
__license__ = "GPL-3.0-or-later"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version__ = "3.5.1"
|
||||
__version__ = "3.6.1"
|
||||
__version_info__ = tuple(int(part) for part in __version__.split('.'))
|
||||
__description__ = "A keyboard-driven, vim-like browser based on Python and Qt."
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ if TYPE_CHECKING:
|
|||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.config import config, websettings
|
||||
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.browser import eventfilter, inspector
|
||||
from qutebrowser.qt import sip
|
||||
|
|
@ -1177,37 +1177,6 @@ class AbstractTab(QWidget):
|
|||
navigation.url.errorString()))
|
||||
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)
|
||||
def _on_load_finished(self, ok: bool) -> None:
|
||||
assert self._widget is not None
|
||||
|
|
|
|||
|
|
@ -123,25 +123,24 @@ def data_for_url(url: QUrl) -> tuple[str, bytes]:
|
|||
|
||||
path = url.path()
|
||||
host = url.host()
|
||||
query = url.query()
|
||||
# A url like "qute:foo" is split as "scheme:path", not "scheme:host".
|
||||
log.misc.debug("url: {}, path: {}, host {}".format(
|
||||
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('/')
|
||||
if query:
|
||||
new_url.setQuery(query)
|
||||
if new_url.host(): # path was a valid host
|
||||
raise Redirect(new_url)
|
||||
if not new_url.host(): # Valid path but not valid host
|
||||
raise UrlInvalidError(f"Invalid host (from path): {path!r}")
|
||||
raise Redirect(new_url)
|
||||
|
||||
if not path:
|
||||
# Redirect qute://help -> qute://help/
|
||||
new_url = QUrl(url)
|
||||
new_url.setPath('/')
|
||||
raise Redirect(new_url)
|
||||
|
||||
try:
|
||||
handler = _HANDLERS[host]
|
||||
|
|
|
|||
|
|
@ -417,6 +417,26 @@ def _init_profile(profile: QWebEngineProfile) -> None:
|
|||
lambda url: profile.clearVisitedLinks([url]))
|
||||
|
||||
_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
|
||||
|
||||
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():
|
||||
|
|
@ -506,7 +526,21 @@ def _init_site_specific_quirks():
|
|||
# "{qt_key}/{qt_version} "
|
||||
# "{upstream_browser_key}/{upstream_browser_version_short} "
|
||||
# "Safari/{webkit_version}")
|
||||
firefox_ua = "Mozilla/5.0 ({os_info}; rv:136.0) Gecko/20100101 Firefox/139.0"
|
||||
firefox_ua = "Mozilla/5.0 ({os_info}; rv:144.0) Gecko/20100101 Firefox/144.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):
|
||||
"""Return a new UA if our current chrome version isn't at least at_least_version."""
|
||||
|
|
@ -528,6 +562,7 @@ def _init_site_specific_quirks():
|
|||
# to keep your account secure" error.
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/5182
|
||||
("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:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import html as html_utils
|
|||
from typing import cast, Union, Optional
|
||||
|
||||
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.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory
|
||||
|
||||
|
|
@ -940,6 +940,10 @@ class _WebEnginePermissions(QObject):
|
|||
notif = miscwidgets.FullscreenNotification(self._widget)
|
||||
notif.set_timeout(timeout)
|
||||
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')
|
||||
def _on_feature_permission_requested(self, url, feature):
|
||||
|
|
@ -1619,6 +1623,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
def _on_navigation_request(self, navigation):
|
||||
super()._on_navigation_request(navigation)
|
||||
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103778
|
||||
local_schemes = {"qute", "file"}
|
||||
qtwe_ver = version.qtwebengine_versions().webengine
|
||||
if (
|
||||
|
|
@ -1631,7 +1636,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
(utils.VersionNumber(6, 2) <= qtwe_ver < utils.VersionNumber(6, 2, 5) or
|
||||
utils.VersionNumber(6, 3) <= qtwe_ver < utils.VersionNumber(6, 3, 1))
|
||||
):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103778
|
||||
log.webview.debug(
|
||||
"Working around blocked request from local page "
|
||||
f"{self.url().toDisplayString()}"
|
||||
|
|
@ -1639,6 +1643,51 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
navigation.accepted = False
|
||||
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:
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -333,8 +333,13 @@ class Config(QObject):
|
|||
pattern, hide_userconfig=hide_userconfig)
|
||||
|
||||
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:
|
||||
"""Make sure the given option may be set in autoconfig.yml."""
|
||||
|
|
|
|||
|
|
@ -356,9 +356,8 @@ class ConfigCommands:
|
|||
raise cmdutils.CommandError(":config-list-remove can only be used "
|
||||
"for lists")
|
||||
|
||||
converted = opt.typ.valtype.from_str(value)
|
||||
|
||||
with self._handle_config_error():
|
||||
converted = opt.typ.valtype.from_str(value)
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
|
||||
if converted not in option_value:
|
||||
|
|
|
|||
|
|
@ -658,6 +658,7 @@ content.site_specific_quirks.skip:
|
|||
valid_values:
|
||||
- ua-google
|
||||
- ua-googledocs
|
||||
- ua-gnome-gitlab
|
||||
- js-whatsapp-web
|
||||
- js-discord
|
||||
- js-string-replaceall
|
||||
|
|
@ -772,14 +773,14 @@ content.headers.user_agent:
|
|||
# Vim-protip: Place your cursor below this comment and run
|
||||
# :r!python scripts/dev/ua_fetch.py
|
||||
- - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
|
||||
(KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
||||
- Chrome 137 macOS
|
||||
(KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
|
||||
- Chrome 141 macOS
|
||||
- - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
|
||||
like Gecko) Chrome/137.0.0.0 Safari/537.36"
|
||||
- Chrome 137 Win10
|
||||
like Gecko) Chrome/141.0.0.0 Safari/537.36"
|
||||
- Chrome 141 Win10
|
||||
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
|
||||
Gecko) Chrome/137.0.0.0 Safari/537.36"
|
||||
- Chrome 137 Linux
|
||||
Gecko) Chrome/141.0.0.0 Safari/537.36"
|
||||
- Chrome 141 Linux
|
||||
supports_pattern: true
|
||||
desc: |
|
||||
User agent to send.
|
||||
|
|
@ -1602,6 +1603,7 @@ fileselect.single_file.command:
|
|||
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
|
||||
- ['["xterm", "-e", "fff", "-p", "{}"]', "fff in xterm"]
|
||||
- ['["xterm", "-e", "lf", "-selection-path", "{}"]', "lf in xterm"]
|
||||
- ['["xterm", "-e", "yazi", "--chooser-file", "{}"]', "yazi in xterm"]
|
||||
default: ['xterm', '-e', 'ranger', '--choosefile={}']
|
||||
desc: >-
|
||||
Command (and arguments) to use for selecting a single file in forms.
|
||||
|
|
@ -1622,6 +1624,7 @@ fileselect.multiple_files.command:
|
|||
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
|
||||
- ['["xterm", "-e", "fff", "-p", "{}"]', "fff in xterm"]
|
||||
- ['["xterm", "-e", "lf", "-selection-path", "{}"]', "lf in xterm"]
|
||||
- ['["xterm", "-e", "yazi", "--chooser-file", "{}"]', "yazi in xterm"]
|
||||
default: ['xterm', '-e', 'ranger', '--choosefiles={}']
|
||||
desc: >-
|
||||
Command (and arguments) to use for selecting multiple files in forms.
|
||||
|
|
@ -1641,6 +1644,7 @@ fileselect.folder.command:
|
|||
- ['["xterm", "-e", "ranger", "--choosedir={}"]', "Ranger in xterm"]
|
||||
- ['["xterm", "-e", "vifm", "--choose-dir", "{}"]', "vifm in xterm"]
|
||||
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
|
||||
- ['["xterm", "-e", "yazi", "--cwd-file", "{}"]', "yazi in xterm"]
|
||||
default: ['xterm', '-e', 'ranger', '--choosedir={}']
|
||||
desc: >-
|
||||
Command (and arguments) to use for selecting a single folder in forms.
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ def _qtwebengine_features( # noqa: C901
|
|||
|
||||
if versions.webengine >= utils.VersionNumber(6, 9):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-135787
|
||||
# and https://bugreports.qt.io/browse/QTBUG-141096
|
||||
# TODO adjust if fixed in Qt 6.9.2+
|
||||
disabled_features.append('PermissionElement')
|
||||
|
||||
|
|
@ -356,7 +357,11 @@ _WEBENGINE_SETTINGS: dict[str, dict[Any, Optional[_SettingValueType]]] = {
|
|||
'qt.workarounds.disable_accelerated_2d_canvas': {
|
||||
'always': '--disable-accelerated-2d-canvas',
|
||||
'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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ li {
|
|||
the required packages for pdf.js are also installed.
|
||||
<br/>
|
||||
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.
|
||||
</li>
|
||||
|
||||
|
|
|
|||
|
|
@ -562,7 +562,7 @@ class MainWindow(QWidget):
|
|||
self._completion.on_clear_completion_selection)
|
||||
self.status.cmd.hide_completion.connect(
|
||||
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):
|
||||
"""Set the visibility of the window decoration via Qt."""
|
||||
|
|
|
|||
|
|
@ -342,9 +342,9 @@ class PromptContainer(QWidget):
|
|||
"""Leave KEY_MODE whenever a prompt is aborted."""
|
||||
try:
|
||||
modeman.leave(self._win_id, key_mode, 'aborted', maybe=True)
|
||||
except objreg.RegistryUnavailableError:
|
||||
except (objreg.RegistryUnavailableError, RuntimeError):
|
||||
# window was deleted: ignore
|
||||
pass
|
||||
log.prompt.debug(f"Ignoring leaving {key_mode} as window was deleted")
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_prompt_done(self, key_mode):
|
||||
|
|
@ -654,6 +654,12 @@ class FilenamePrompt(_BasePrompt):
|
|||
|
||||
"""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):
|
||||
super().__init__(question, parent)
|
||||
self._init_texts(question)
|
||||
|
|
@ -753,7 +759,7 @@ class FilenamePrompt(_BasePrompt):
|
|||
self._file_model = QFileSystemModel(self)
|
||||
|
||||
# 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.clicked.connect(self._insert_path)
|
||||
|
|
|
|||
|
|
@ -140,10 +140,12 @@ class StatusBar(QWidget):
|
|||
moved: Emitted when the statusbar has moved, so the completion widget
|
||||
can move to the right position.
|
||||
arg: The new position.
|
||||
release_focus: Emitted just before the statusbar is hidden.
|
||||
"""
|
||||
|
||||
resized = pyqtSignal('QRect')
|
||||
moved = pyqtSignal('QPoint')
|
||||
release_focus = pyqtSignal()
|
||||
|
||||
STYLESHEET = _generate_stylesheet()
|
||||
|
||||
|
|
@ -365,6 +367,7 @@ class StatusBar(QWidget):
|
|||
def _hide_cmd_widget(self):
|
||||
"""Show temporary text instead of command widget."""
|
||||
log.statusbar.debug("Hiding cmd widget")
|
||||
self.release_focus.emit()
|
||||
self._stack.setCurrentWidget(self.txt)
|
||||
self.maybe_hide()
|
||||
|
||||
|
|
|
|||
|
|
@ -926,6 +926,7 @@ class TabbedBrowser(QWidget):
|
|||
.format(current_mode.name, mode_on_change))
|
||||
self._now_focused = tab
|
||||
self.current_tab_changed.emit(tab)
|
||||
self.cur_fullscreen_requested.emit(tab.data.fullscreen)
|
||||
self.cur_search_match_changed.emit(tab.search.match)
|
||||
QTimer.singleShot(0, self._update_window_title)
|
||||
self._tab_insert_idx_left = self.widget.currentIndex()
|
||||
|
|
|
|||
|
|
@ -422,6 +422,37 @@ class _BackendProblemChecker:
|
|||
|
||||
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:
|
||||
assert objects.backend == backend, objects.backend
|
||||
|
||||
|
|
@ -433,6 +464,7 @@ class _BackendProblemChecker:
|
|||
self._handle_ssl_support()
|
||||
self._handle_serviceworker_nuking()
|
||||
self._check_software_rendering()
|
||||
self._force_wayland_hardware_acceleration()
|
||||
self._confirm_chromium_version_changes()
|
||||
else:
|
||||
self._assert_backend(usertypes.Backend.QtWebKit)
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ def parse_fatal_stacktrace(text):
|
|||
lines = [
|
||||
r'(?P<type>Fatal Python error|Windows fatal exception): (?P<msg>.*)',
|
||||
r' *',
|
||||
r'(Current )?[Tt]hread [^ ]* \(most recent call first\): *',
|
||||
r' File ".*", line \d+ in (?P<func>.*)',
|
||||
r'(Current )?[Tt]hread .* \(most recent call first\): *',
|
||||
r' (File ".*", line \d+ in (?P<func>.*)|<no Python frame>)',
|
||||
]
|
||||
m = re.search('\n'.join(lines), text)
|
||||
if m is None:
|
||||
|
|
@ -58,7 +58,7 @@ def parse_fatal_stacktrace(text):
|
|||
else:
|
||||
msg = m.group('msg')
|
||||
typ = m.group('type')
|
||||
func = m.group('func')
|
||||
func = m.group('func') or ''
|
||||
if typ == 'Windows fatal exception':
|
||||
msg = 'Windows ' + msg
|
||||
return msg, func
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ instead of crashing.
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import pathlib
|
||||
import dataclasses
|
||||
|
|
@ -35,9 +36,13 @@ from collections.abc import Iterator
|
|||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import binparsing, objects
|
||||
from qutebrowser.qt import core
|
||||
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 = [
|
||||
# Linux
|
||||
47222, # QtWebEngine 6.9 Beta 3
|
||||
|
|
@ -57,7 +62,11 @@ PAK_VERSION = 5
|
|||
RESOURCES_ENV_VAR = "QTWEBENGINE_RESOURCES_PATH"
|
||||
DISABLE_ENV_VAR = "QUTE_DISABLE_PAKJOY"
|
||||
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/*"
|
||||
REPLACEMENT_URL = b"https://qute.invalid/*"
|
||||
|
|
@ -222,7 +231,7 @@ def copy_webengine_resources() -> Optional[pathlib.Path]:
|
|||
)
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/8257
|
||||
or config.val.qt.workarounds.disable_hangouts_extension
|
||||
):
|
||||
) or hasattr(QWebEngineProfile, "extensionManager"): # Qt 6.10+
|
||||
# No patching needed
|
||||
return None
|
||||
|
||||
|
|
@ -303,3 +312,16 @@ def patch_webengine() -> Iterator[None]:
|
|||
del os.environ[RESOURCES_ENV_VAR]
|
||||
else:
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -559,6 +559,7 @@ class WebEngineVersions:
|
|||
118: '118.0.5993.220', # 2024-01-25, Qt 6.7
|
||||
122: '122.0.6261.171', # 2024-04-15, Qt 6.8
|
||||
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/
|
||||
|
|
@ -652,9 +653,10 @@ class WebEngineVersions:
|
|||
## Qt 6.9
|
||||
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, 2): (_BASES[130], '139.0.7258.67'), # 2025-07-29
|
||||
|
||||
## Qt 6.10 (WIP)
|
||||
utils.VersionNumber(6, 10): (_BASES[130], '137.0.7151.68'), # 2025-05-30
|
||||
## Qt 6.10
|
||||
utils.VersionNumber(6, 10): (_BASES[134], '140.0.7339.207'), # 2025-09-22
|
||||
}
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
|
|
@ -923,6 +925,46 @@ def _backend() -> str:
|
|||
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 "avoid-chromium-init" not in objects.debug_flags
|
||||
and machinery.IS_QT6 # mypy; TODO early return once Qt 5 is dropped
|
||||
):
|
||||
from qutebrowser.qt.webenginecore import QWebEngineProfile
|
||||
profile = QWebEngineProfile.defaultProfile()
|
||||
assert profile is not None # mypy
|
||||
|
||||
try:
|
||||
ext_manager = profile.extensionManager()
|
||||
except AttributeError:
|
||||
# Added in QtWebEngine 6.10
|
||||
return []
|
||||
assert ext_manager is not None # mypy
|
||||
|
||||
lines.append("WebExtensions:")
|
||||
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:
|
||||
time_delta = datetime.datetime.now() - objects.qapp.launch_time
|
||||
# Round off microseconds
|
||||
|
|
@ -972,6 +1014,8 @@ def version_info() -> str:
|
|||
if QSslSocket.supportsSsl() else 'no'),
|
||||
]
|
||||
|
||||
lines += _webengine_extensions()
|
||||
|
||||
if objects.qapp:
|
||||
style = objects.qapp.style()
|
||||
assert style is not None
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
adblock==0.6.0
|
||||
colorama==0.4.6
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.2
|
||||
MarkupSafe==3.0.3
|
||||
Pygments==2.19.2
|
||||
PyYAML==6.0.2
|
||||
PyYAML==6.0.3
|
||||
# Unpinned due to recompile_requirements.py limitations
|
||||
pyobjc-core ; sys_platform=="darwin"
|
||||
pyobjc-framework-Cocoa ; sys_platform=="darwin"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
"""Build a new release."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -20,7 +21,8 @@ import platform
|
|||
import collections
|
||||
import dataclasses
|
||||
import re
|
||||
from typing import Optional
|
||||
import http
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from collections.abc import Iterable
|
||||
|
||||
try:
|
||||
|
|
@ -28,6 +30,12 @@ try:
|
|||
except ImportError:
|
||||
pass
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import github3
|
||||
import github3.repos.release
|
||||
|
||||
import requests
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
|
|
@ -176,6 +184,10 @@ def smoke_test(executable: pathlib.Path, debug: bool) -> None:
|
|||
|
||||
# Qt 6.9 on macOS
|
||||
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:
|
||||
stderr_whitelist.extend([
|
||||
|
|
@ -184,6 +196,12 @@ def smoke_test(executable: pathlib.Path, debug: bool) -> None:
|
|||
(r'\[.*:ERROR:dxva_video_decode_accelerator_win.cc\(\d+\)\] '
|
||||
r'DXVAVDA fatal error: could not LoadLibrary: .*: The specified '
|
||||
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)
|
||||
|
|
@ -551,11 +569,36 @@ def read_github_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(
|
||||
artifacts: list[Artifact],
|
||||
tag: str,
|
||||
gh_token: str,
|
||||
experimental: bool,
|
||||
skip_if_exists: bool,
|
||||
) -> None:
|
||||
"""Upload the given artifacts to GitHub.
|
||||
|
||||
|
|
@ -564,35 +607,25 @@ def github_upload(
|
|||
tag: The name of the release tag
|
||||
gh_token: The GitHub token to use
|
||||
experimental: Upload to the experiments repo
|
||||
skip_if_exists: Skip uploading artifacts that already exist
|
||||
"""
|
||||
# pylint: disable=broad-exception-raised
|
||||
import github3
|
||||
import github3.exceptions
|
||||
utils.print_title("Uploading to github...")
|
||||
|
||||
gh = github3.login(token=gh_token)
|
||||
|
||||
if 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}")
|
||||
assert gh is not None
|
||||
release = _github_find_release(gh=gh, tag=tag, experimental=experimental)
|
||||
|
||||
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:
|
||||
print(f"Uploading {artifact.path}")
|
||||
|
||||
assets = [asset for asset in release.assets()
|
||||
if asset.name == artifact.path.name]
|
||||
if assets:
|
||||
if (assets := _github_assets(release, artifact)):
|
||||
print(f"Assets already exist: {assets}")
|
||||
|
||||
if utils.ON_CI:
|
||||
|
|
@ -620,9 +653,7 @@ def github_upload(
|
|||
|
||||
print("Retrying!")
|
||||
|
||||
assets = [asset for asset in release.assets()
|
||||
if asset.name == artifact.path.name]
|
||||
if assets:
|
||||
if (assets := _github_assets(release, artifact)):
|
||||
stray_asset = assets[0]
|
||||
print(f"Deleting stray asset {stray_asset.name}")
|
||||
stray_asset.delete()
|
||||
|
|
@ -630,12 +661,29 @@ def github_upload(
|
|||
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."""
|
||||
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/
|
||||
artifacts = [a for a in artifacts if a.mimetype != 'application/pgp-signature']
|
||||
|
||||
utils.print_title("Uploading to PyPI...")
|
||||
if experimental:
|
||||
run_twine('upload', artifacts, "-r", "testpypi")
|
||||
else:
|
||||
|
|
@ -661,6 +709,8 @@ def main() -> None:
|
|||
nargs='?')
|
||||
parser.add_argument('--upload', action='store_true', required=False,
|
||||
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,
|
||||
help="Skip confirmation before uploading.")
|
||||
parser.add_argument('--skip-packaging', action='store_true', required=False,
|
||||
|
|
@ -720,9 +770,16 @@ def main() -> None:
|
|||
|
||||
assert gh_token is not None
|
||||
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:
|
||||
pypi_upload(artifacts, experimental=args.experimental)
|
||||
pypi_upload(
|
||||
artifacts, experimental=args.experimental, skip_if_exists=args.reupload
|
||||
)
|
||||
else:
|
||||
print()
|
||||
utils.print_title("Artifacts")
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@
|
|||
"distlib": "https://github.com/pypa/distlib/blob/master/CHANGES.rst",
|
||||
"py-cpuinfo": "https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog",
|
||||
"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",
|
||||
"charset-normalizer": "https://github.com/Ousret/charset_normalizer/blob/master/CHANGELOG.md",
|
||||
"idna": "https://github.com/kjd/idna/blob/master/HISTORY.rst",
|
||||
|
|
|
|||
|
|
@ -54,13 +54,14 @@ def show_commit():
|
|||
git_args = ['git', 'show']
|
||||
if utils.ON_CI:
|
||||
git_args.append("--color")
|
||||
git_args.append("--no-patch") # shows entire git tree on CI (shallow clone)
|
||||
subprocess.run(git_args, check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Update release version.")
|
||||
parser.add_argument('bump', action="store",
|
||||
choices=["major", "minor", "patch"],
|
||||
choices=["major", "minor", "patch", "reupload"],
|
||||
help="Update release version")
|
||||
parser.add_argument('--commands', action="store_true",
|
||||
help="Only show commands to run post-release.")
|
||||
|
|
@ -70,7 +71,8 @@ if __name__ == "__main__":
|
|||
|
||||
if not args.commands:
|
||||
verify_branch(args.bump)
|
||||
bump_version(args.bump)
|
||||
if args.bump != "reupload":
|
||||
bump_version(args.bump)
|
||||
show_commit()
|
||||
|
||||
import qutebrowser
|
||||
|
|
@ -87,15 +89,16 @@ if __name__ == "__main__":
|
|||
print(f"Outputs for {version} written to GitHub Actions output file")
|
||||
else:
|
||||
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 == 'patch':
|
||||
print("* git checkout main && git cherry-pick -x v{v} && "
|
||||
"git push origin".format(v=version))
|
||||
else:
|
||||
print("* git branch v{x} v{v} && git push --set-upstream origin v{x}"
|
||||
.format(v=version, x=version_x))
|
||||
print("* Create new release via GitHub (required to upload release "
|
||||
"artifacts)")
|
||||
if args.bump != 'reupload':
|
||||
print("* git push origin; git push origin v{v}".format(v=version))
|
||||
if args.bump == 'patch':
|
||||
print("* git checkout main && git cherry-pick -x v{v} && "
|
||||
"git push origin".format(v=version))
|
||||
else:
|
||||
print("* git branch v{x} v{v} && git push --set-upstream origin v{x}"
|
||||
.format(v=version, x=version_x))
|
||||
print("* Create new release via GitHub (required to upload release "
|
||||
"artifacts)")
|
||||
print("* Linux: git fetch && git checkout v{v} && "
|
||||
"tox -e build-release -- --upload"
|
||||
.format(v=version))
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ def get_lib_path(executable, name, required=True):
|
|||
return data
|
||||
elif prefix == 'ImportError':
|
||||
if required:
|
||||
wrapper = os.environ["QUTE_QT_WRAPPER"]
|
||||
wrapper = os.environ.get("QUTE_QT_WRAPPER", "unset")
|
||||
raise Error(
|
||||
f"Could not import {name} with {executable}: {data} "
|
||||
f"(QUTE_QT_WRAPPER: {wrapper})"
|
||||
|
|
|
|||
14
setup.py
14
setup.py
|
|
@ -9,8 +9,8 @@
|
|||
import re
|
||||
import ast
|
||||
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.
|
||||
|
|
@ -25,7 +25,7 @@ import setuptools
|
|||
|
||||
|
||||
try:
|
||||
BASEDIR = os.path.dirname(os.path.realpath(__file__))
|
||||
BASEDIR = pathlib.Path(__file__).resolve().parent
|
||||
except NameError:
|
||||
BASEDIR = None
|
||||
|
||||
|
|
@ -50,8 +50,8 @@ def _get_constant(name):
|
|||
The value of the argument.
|
||||
"""
|
||||
field_re = re.compile(r'__{}__\s+=\s+(.*)'.format(re.escape(name)))
|
||||
path = os.path.join(BASEDIR, 'qutebrowser', '__init__.py')
|
||||
line = field_re.search(read_file(path)).group(1)
|
||||
init_path = BASEDIR / 'qutebrowser' / '__init__.py'
|
||||
line = field_re.search(read_file(init_path)).group(1)
|
||||
value = ast.literal_eval(line)
|
||||
return value
|
||||
|
||||
|
|
@ -99,6 +99,6 @@ try:
|
|||
)
|
||||
finally:
|
||||
if BASEDIR is not None:
|
||||
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
git_commit_id_path = BASEDIR / 'qutebrowser' / 'git-commit-id'
|
||||
if git_commit_id_path.exists():
|
||||
git_commit_id_path.unlink()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Scrolling</title>
|
||||
<script>requestAnimationFrame(() => console.log('position_absolute loaded'))</script>
|
||||
</head>
|
||||
<body style="position: absolute">
|
||||
<a href="/data/hello.txt" id="link">Just a link</a>
|
||||
|
|
|
|||
|
|
@ -103,3 +103,11 @@ Feature: Using completion
|
|||
And I run :completion-item-focus next
|
||||
And I run :cmd-set-text -s :set
|
||||
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
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ Feature: Downloading things from a website.
|
|||
And I open data/downloads/issue1243.html
|
||||
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
|
||||
Then the error "Download error: No handler found for qute://" should be shown
|
||||
And "NotFoundError while handling qute://* URL" should be logged
|
||||
Then the error "Download error: Invalid host (from path): ''" should be shown
|
||||
And "UrlInvalidError while handling qute://* URL" should be logged
|
||||
|
||||
Scenario: Downloading a data: link (issue 1214)
|
||||
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
|
||||
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
|
||||
When I open data/downloads/download with spaces.bin without waiting
|
||||
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
|
||||
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
|
||||
Scenario: user-agent when using :download
|
||||
When I open user-agent
|
||||
|
|
|
|||
|
|
@ -386,6 +386,13 @@ Feature: Various utility commands.
|
|||
And I run :jseval console.log(window.navigator.userAgent)
|
||||
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)
|
||||
When I set content.headers.user_agent to toaster
|
||||
And I open about:blank
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ Feature: Scrolling
|
|||
|
||||
Scenario: Relative scroll position with a position:absolute page
|
||||
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 wait until the scroll position changed
|
||||
And I run :scroll-page --bottom-navigate next 0 1
|
||||
|
|
@ -339,3 +340,11 @@ Feature: Scrolling
|
|||
And I run :tab-next
|
||||
And I run :jseval --world main checkAnchor()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -49,11 +49,14 @@ def fresh_instance(quteproc):
|
|||
# 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
|
||||
# browser to make it forget previous prompts.
|
||||
#
|
||||
# Qt 6.10 Beta 4 accidentally persists some permissions;
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-140194
|
||||
if (
|
||||
qtutils.version_check("6.8", compiled=False)
|
||||
and PYQT_WEBENGINE_VERSION
|
||||
and PYQT_WEBENGINE_VERSION < 0x60800
|
||||
):
|
||||
) or qtutils.version_check("6.10", compiled=False, exact=True):
|
||||
quteproc.terminate()
|
||||
quteproc.start()
|
||||
|
||||
|
|
|
|||
|
|
@ -253,6 +253,24 @@ def is_ignored_chromium_message(line):
|
|||
# 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)
|
||||
for pattern in ignored_messages)
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ class Process(QObject):
|
|||
match = self._wait_for_match(spy, kwargs)
|
||||
if match is not None:
|
||||
if message is not None:
|
||||
self._log("----> found it")
|
||||
self._log(f"----> found it: {match.formatted_str()}")
|
||||
return match
|
||||
|
||||
raise quteutils.Unreachable
|
||||
|
|
|
|||
|
|
@ -17,6 +17,36 @@ from qutebrowser.utils import resources, urlmatch
|
|||
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:
|
||||
|
||||
"""Test the qute://javascript endpoint."""
|
||||
|
|
|
|||
|
|
@ -315,9 +315,10 @@ class TestAdd:
|
|||
with pytest.raises(cmdutils.CommandError, match="Invalid value ''"):
|
||||
commands.config_list_add('content.blocking.whitelist', '')
|
||||
|
||||
# FIXME test value conversion for :list-add like in test_dict_add_value_type
|
||||
# (once we have a List config option using a non-str type, or a way to
|
||||
# dynamically add new option definitions).
|
||||
def test_list_add_value_type(self, commands, config_stub):
|
||||
commands.config_list_add("completion.web_history.exclude", "*")
|
||||
value = config_stub.val.completion.web_history.exclude
|
||||
assert value == [urlmatch.UrlPattern("*")]
|
||||
|
||||
@pytest.mark.parametrize('value', ['test1', 'test2'])
|
||||
@pytest.mark.parametrize('temp', [True, False])
|
||||
|
|
@ -410,9 +411,16 @@ class TestRemove:
|
|||
match="#133742 is not in colors.completion.fg!"):
|
||||
commands.config_list_remove('colors.completion.fg', '#133742')
|
||||
|
||||
# FIXME test value conversion for :list-remove like in test_dict_add_value_type
|
||||
# (once we have a List config option using a non-str type, or a way to
|
||||
# dynamically add new option definitions).
|
||||
def test_list_remove_value_type(self, commands, config_stub):
|
||||
config_stub.val.completion.web_history.exclude = ["*"]
|
||||
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('temp', [True, False])
|
||||
|
|
|
|||
|
|
@ -1629,7 +1629,24 @@ class TestDict:
|
|||
none_ok=True)
|
||||
converted = d.to_py(val)
|
||||
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),
|
||||
strategies.booleans()))
|
||||
|
|
|
|||
|
|
@ -158,12 +158,17 @@ class TestWebEngineArgs:
|
|||
assert '--enable-in-process-stack-traces' not in args
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'qt6, value, has_arg',
|
||||
'qt_version, qt6, value, has_arg',
|
||||
[
|
||||
(False, 'auto', False),
|
||||
(True, 'auto', True),
|
||||
(True, 'always', True),
|
||||
(True, 'never', False),
|
||||
('5.15.2', False, 'auto', False),
|
||||
('6.5.3', True, 'auto', True),
|
||||
('6.6.0', True, 'auto', True),
|
||||
('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(
|
||||
|
|
@ -172,10 +177,12 @@ class TestWebEngineArgs:
|
|||
version_patcher,
|
||||
config_stub,
|
||||
monkeypatch,
|
||||
qt_version,
|
||||
qt6,
|
||||
value,
|
||||
has_arg,
|
||||
):
|
||||
version_patcher(qt_version)
|
||||
config_stub.val.qt.workarounds.disable_accelerated_2d_canvas = value
|
||||
monkeypatch.setattr(machinery, 'IS_QT6', qt6)
|
||||
|
||||
|
|
|
|||
|
|
@ -551,6 +551,7 @@ KEYS = [
|
|||
|
||||
Key('MicVolumeUp', 'Microphone Volume Up', qtest=False),
|
||||
Key('MicVolumeDown', 'Microphone Volume Down', qtest=False),
|
||||
Key('Keyboard', 'Keyboard', qtest=False),
|
||||
|
||||
Key('New', qtest=False),
|
||||
Key('Open', qtest=False),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,31 @@ Thread 0x00007fa135ac7700 (most recent call first):
|
|||
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 fatal exception: access violation
|
||||
_
|
||||
|
|
@ -45,13 +70,32 @@ Hello world!
|
|||
"""
|
||||
|
||||
|
||||
@pytest.mark.parametrize('text, typ, func', [
|
||||
(VALID_CRASH_TEXT, 'Segmentation fault', 'testfunc'),
|
||||
(VALID_CRASH_TEXT_THREAD, 'Segmentation fault', 'testfunc'),
|
||||
(VALID_CRASH_TEXT_EMPTY, 'Aborted', ''),
|
||||
(WINDOWS_CRASH_TEXT, 'Windows access violation', 'tabopen'),
|
||||
(INVALID_CRASH_TEXT, '', ''),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"text, typ, func",
|
||||
[
|
||||
pytest.param(VALID_CRASH_TEXT, "Segmentation fault", "testfunc", id="valid"),
|
||||
pytest.param(
|
||||
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):
|
||||
text = text.strip().replace('_', ' ')
|
||||
assert crashdialog.parse_fatal_stacktrace(text) == (typ, func)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import shutil
|
|||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.qt import machinery
|
||||
from qutebrowser.misc import pakjoy, binparsing
|
||||
from qutebrowser.utils import utils, version, standarddir, usertypes
|
||||
|
||||
|
|
@ -193,11 +194,19 @@ def read_patched_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")
|
||||
@skip_if_incompatible
|
||||
class TestWithRealResourcesFile:
|
||||
"""Tests that use the real pak file form the Qt installation."""
|
||||
|
||||
@pytest.mark.qt6_only
|
||||
def test_happy_path(self):
|
||||
# Go through the full patching processes with the real resources file from
|
||||
# the current installation. Make sure our replacement string is in it
|
||||
|
|
@ -257,7 +266,6 @@ class TestWithRealResourcesFile:
|
|||
"Not applying quirks. Expected location: "
|
||||
)
|
||||
|
||||
@pytest.mark.qt6_only
|
||||
def test_hardcoded_ids(self):
|
||||
"""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:
|
||||
return tmp_path / "cache" / pakjoy.CACHE_DIR_NAME
|
||||
|
||||
@skip_if_incompatible
|
||||
def test_patching(self, resources_path: pathlib.Path, quirk_dir_path: pathlib.Path):
|
||||
"""Go through the full patching processes with a fake resources file."""
|
||||
with pakjoy.patch_webengine():
|
||||
|
|
@ -457,13 +466,14 @@ class TestWithConstructedResourcesFile:
|
|||
)
|
||||
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):
|
||||
patch_version(monkeypatch, utils.VersionNumber(6, 7)) # unaffected
|
||||
config_stub.val.qt.workarounds.disable_hangouts_extension = True
|
||||
with pakjoy.patch_webengine():
|
||||
assert pakjoy.RESOURCES_ENV_VAR in os.environ
|
||||
|
||||
@skip_if_incompatible
|
||||
def test_preset_env_var(
|
||||
self,
|
||||
resources_path: pathlib.Path,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ import pytest_mock
|
|||
import hypothesis
|
||||
import hypothesis.strategies
|
||||
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
|
||||
from qutebrowser.config import config, websettings
|
||||
|
|
@ -1153,13 +1154,7 @@ class TestChromiumVersion:
|
|||
|
||||
def test_prefers_saved_user_agent(self, monkeypatch, patch_no_api):
|
||||
webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87'))
|
||||
|
||||
class FakeProfile:
|
||||
def defaultProfile(self):
|
||||
raise AssertionError("Should not be called")
|
||||
|
||||
monkeypatch.setattr(webenginesettings, 'QWebEngineProfile', FakeProfile())
|
||||
|
||||
monkeypatch.setattr(QWebEngineProfile, "defaultProfile", lambda: 1/0)
|
||||
version.qtwebengine_versions()
|
||||
|
||||
def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub):
|
||||
|
|
@ -1280,6 +1275,62 @@ class TestChromiumVersion:
|
|||
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
|
||||
class VersionParams:
|
||||
|
||||
|
|
@ -1373,6 +1424,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
|
|||
'python_path': 'EXECUTABLE PATH',
|
||||
'uptime': "1:23:45",
|
||||
'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no",
|
||||
'webextensions': "", # overridden below if QtWebEngine is used
|
||||
}
|
||||
|
||||
patches['qtwebengine_versions'] = (
|
||||
|
|
@ -1395,6 +1447,21 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
|
|||
substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)'
|
||||
else:
|
||||
monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False)
|
||||
if machinery.IS_QT6:
|
||||
monkeypatch.setattr(
|
||||
QWebEngineProfile,
|
||||
"defaultProfile",
|
||||
lambda: 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
|
||||
substitutions['backend'] = 'QtWebEngine 1.2.3\n (source: faked)'
|
||||
|
||||
|
|
@ -1434,7 +1501,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
|
|||
pdf.js: PDFJS VERSION
|
||||
sqlite: SQLITE VERSION
|
||||
QtNetwork SSL: {ssl}
|
||||
{style}{platform_plugin}{opengl}
|
||||
{webextensions}{style}{platform_plugin}{opengl}
|
||||
Platform: PLATFORM, ARCHITECTURE{linuxdist}
|
||||
Frozen: {frozen}
|
||||
Imported from {import_path}
|
||||
|
|
@ -1519,6 +1586,95 @@ class TestOpenGLInfo:
|
|||
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:
|
||||
monkeypatch.setattr(version.objects, "backend", usertypes.Backend.QtWebKit)
|
||||
monkeypatch.setattr(QWebEngineProfile, "defaultProfile", lambda: 1/0)
|
||||
assert not version._webengine_extensions()
|
||||
|
||||
def test_avoid_chromium_init(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(version.objects, "backend", usertypes.Backend.QtWebEngine)
|
||||
monkeypatch.setattr(objects, "debug_flags", {"avoid-chromium-init"})
|
||||
monkeypatch.setattr(QWebEngineProfile, "defaultProfile", lambda: 1/0)
|
||||
assert not version._webengine_extensions()
|
||||
|
||||
def test_no_extension_manager(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(QWebEngineProfile, "defaultProfile", object)
|
||||
assert not version._webengine_extensions()
|
||||
|
||||
@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(
|
||||
QWebEngineProfile,
|
||||
"defaultProfile",
|
||||
lambda: FakeExtensionProfile(
|
||||
FakeExtensionManager(extensions)
|
||||
),
|
||||
)
|
||||
assert version._webengine_extensions() == expected
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pbclient(stubs):
|
||||
http_stub = stubs.HTTPPostStub()
|
||||
|
|
|
|||
7
tox.ini
7
tox.ini
|
|
@ -57,8 +57,9 @@ deps =
|
|||
pyqt67: -r{toxinidir}/misc/requirements/requirements-pyqt-6.7.txt
|
||||
pyqt68: -r{toxinidir}/misc/requirements/requirements-pyqt-6.8.txt
|
||||
pyqt69: -r{toxinidir}/misc/requirements/requirements-pyqt-6.9.txt
|
||||
pyqt610: -r{toxinidir}/misc/requirements/requirements-pyqt-6.10.txt
|
||||
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}
|
||||
cov: {envpython} scripts/dev/check_coverage.py {posargs}
|
||||
|
||||
|
|
@ -213,14 +214,14 @@ deps =
|
|||
allowlist_externals = bash
|
||||
commands = bash scripts/dev/run_shellcheck.sh {posargs}
|
||||
|
||||
[testenv:mypy-{pyqt5,pyqt6}]
|
||||
[testenv:mypy{,-pyqt5,-pyqt6}]
|
||||
basepython = {env:PYTHON:python3}
|
||||
passenv =
|
||||
TERM
|
||||
MYPY_FORCE_TERMINAL_WIDTH
|
||||
setenv =
|
||||
# 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
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
|
|
|
|||
Loading…
Reference in New Issue