Merge branch 'master' into more-sophisticated-adblock

This commit is contained in:
Árni Dagur 2020-12-19 20:29:04 +00:00
commit fd155628e1
83 changed files with 1066 additions and 506 deletions

View File

@ -1,5 +1,8 @@
[run]
source = qutebrowser
include =
qutebrowser/*
tests/*
scripts/*
branch = true
omit =
qutebrowser/__main__.py

View File

@ -4,7 +4,7 @@ insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
max_line_length = 79
max_line_length = 88
indent_style = space
indent_size = 4

View File

@ -39,7 +39,7 @@ jobs:
.tox
~/.cache/pip
key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- uses: actions/setup-python@v2.1.1
- uses: actions/setup-python@v2.1.2
with:
python-version: '3.8'
- uses: actions/setup-node@v2.1.1
@ -129,10 +129,10 @@ jobs:
- testenv: py38-pyqt514
os: ubuntu-20.04
python: 3.8
### PyQt 5.15 (Python nightly)
- testenv: py3-pyqt515
### PyQt 5.15 (Python 3.9)
- testenv: py39-pyqt515
os: ubuntu-20.04
python: 3.10-dev
python: 3.9-dev
### PyQt 5.15 (Python 3.8, with coverage)
- testenv: py38-pyqt515-cov
os: ubuntu-20.04
@ -157,13 +157,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@v2.1.1
if: "!endsWith(matrix.python, '-dev')"
with:
python-version: "${{ matrix.python }}"
- name: Set up development Python
uses: deadsnakes/action@v1.0.0
if: "endsWith(matrix.python, '-dev')"
uses: actions/setup-python@v2.1.2
with:
python-version: "${{ matrix.python }}"
- name: Set up problem matchers
@ -184,7 +178,7 @@ jobs:
if: "failure()"
- name: Upload coverage
if: "endsWith(matrix.testenv, '-cov')"
uses: codecov/codecov-action@v1.0.12
uses: codecov/codecov-action@v1.0.13
with:
name: "${{ matrix.testenv }}"

View File

@ -20,11 +20,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2.1.1
uses: actions/setup-python@v2.1.2
with:
python-version: '3.7'
- name: Set up Python 3.8
uses: actions/setup-python@v2.1.1
uses: actions/setup-python@v2.1.2
with:
python-version: '3.8'
- name: Recompile requirements

View File

@ -59,7 +59,7 @@ docstring-min-length=3
no-docstring-rgx=(^_|^main$)
[FORMAT]
max-line-length=79
max-line-length=88
ignore-long-lines=(<?https?://|file://|^# Copyright 201\d|link:)
expected-line-ending-format=LF

View File

@ -45,12 +45,20 @@ Changed
- `:back` and `:forward` now take an optional index which is completed using
the current tab's history.
- The time a website in a tab was visited is now saved/restored in sessions.
- New argument `strip` for `:navigate` which removes queries and
fragments from the current URL.
- When attempting to download a file to a location for which there's already a
still-running download, a confirmation prompt is now displayed.
- `:completion-item-focus` now understands `next-page` and `prev-page` with
corresponding `<PgDown>` / `<PgUp>` default bindings.
- When the last private window is closed, all private browsing data is now cleared.
Added
~~~~~
- `:undo` now has a new `-w` / `--window` argument, which can be used to
restore closed windows (rather than tabs). This is bound to `U` by default.
- `:jseval` can now take `javascript:...` URLs via a new `--url` flag.
- New replacement `{aligned_index}` for `tabs.title.format` and `format_pinned`
which behaves like `{index}`, but space-pads the index based on the total
numbers of tabs. This can be used to get aligned tab texts with vertical
@ -63,6 +71,7 @@ Added
- The `:download-open` command now has a new `--dir` flag, which can be used to
open the directory containing the downloaded file. An entry to do the same
was also added to the context menu.
- Messages are now wrapped when they are too long to be displayed on a single line.
Fixed
~~~~~
@ -98,6 +107,12 @@ Fixed
instead of displaying the proper text. This is now fixed.
- When entering different modes too quickly (e.g. pressing `fV`), the statusbar
could end up in a confusing state. This is now fixed.
- When qutebrowser quits, running downloads are now cancelled properly.
- The site-specific quirk for `web.whatsapp.com` has been updated to work after recent
WhatsApp-changes.
- Highlighting in the completion now works properly when UTF-16 surrogate pairs (such as
emoji) are involved.
- When a windowed inspector is clicked, insert mode now isn't entered anymore.
v1.13.1 (2020-07-17)
--------------------

View File

@ -749,7 +749,7 @@ Insert text at cursor position.
[[jseval]]
=== jseval
Syntax: +:jseval [*--file*] [*--quiet*] [*--world* 'world'] 'js-code'+
Syntax: +:jseval [*--file*] [*--url*] [*--quiet*] [*--world* 'world'] 'js-code'+
Evaluate a JavaScript string.
@ -761,6 +761,7 @@ Evaluate a JavaScript string.
in qutebrowser's data dir, e.g.
`~/.local/share/qutebrowser/js`.
* +*-u*+, +*--url*+: Interpret js-code as a `javascript:...` URL.
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in.
@ -864,6 +865,7 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
Uses the
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option.
- `strip`: Strip query and fragment from the current URL.
@ -1662,7 +1664,9 @@ Syntax: +:completion-item-focus [*--history*] 'which'+
Shift the focus of the completion menu to another item.
==== positional arguments
* +'which'+: 'next', 'prev', 'next-category', or 'prev-category'.
* +'which'+: 'next', 'prev', 'next-category', 'prev-category',
'next-page', or 'prev-page'.
==== optional arguments
* +*-H*+, +*--history*+: Navigate through command history if no text was typed.

View File

@ -394,9 +394,10 @@ Pre-built colorschemes
^^^^^^^^^^^^^^^^^^^^^^
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
- https://gitlab.com/jjzmajic/qutewal[Pywal integration]
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
- https://github.com/dracula/qutebrowser-dracula-theme[Dracula]
- https://github.com/jjzmajic/qutewal[Pywal theme]
- https://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^

View File

@ -374,6 +374,7 @@ Backend to use to display websites.
qutebrowser supports two different web rendering engines / backends, QtWebKit and QtWebEngine.
QtWebKit was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit. It's slightly more resource hungry than QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice.
This setting requires a restart.
Type: <<types,String>>
@ -509,6 +510,8 @@ Default:
* +pass:[&lt;Ctrl-Y&gt;]+: +pass:[rl-yank]+
* +pass:[&lt;Down&gt;]+: +pass:[completion-item-focus --history next]+
* +pass:[&lt;Escape&gt;]+: +pass:[leave-mode]+
* +pass:[&lt;PgDown&gt;]+: +pass:[completion-item-focus next-page]+
* +pass:[&lt;PgUp&gt;]+: +pass:[completion-item-focus prev-page]+
* +pass:[&lt;Return&gt;]+: +pass:[command-accept]+
* +pass:[&lt;Shift-Delete&gt;]+: +pass:[completion-item-del]+
* +pass:[&lt;Shift-Tab&gt;]+: +pass:[completion-item-focus prev]+
@ -1569,6 +1572,7 @@ Default: +pass:[white]+
[[colors.webpage.darkmode.algorithm]]
=== colors.webpage.darkmode.algorithm
Which algorithm to use for modifying how colors are rendered with darkmode.
This setting requires a restart.
Type: <<types,String>>
@ -1589,6 +1593,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.contrast
Contrast for dark mode.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.
This setting requires a restart.
Type: <<types,Float>>
@ -1616,6 +1621,7 @@ Example configurations from Chromium's `chrome://flags`:
- "With selective inversion of everything": Combines the two variants
above.
This setting requires a restart.
Type: <<types,Bool>>
@ -1630,6 +1636,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.grayscale.all
Render all colors as grayscale.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.
This setting requires a restart.
Type: <<types,Bool>>
@ -1644,6 +1651,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.grayscale.images
Desaturation factor for images in dark mode.
If set to 0, images are left as-is. If set to 1, images are completely grayscale. Values between 0 and 1 desaturate the colors accordingly.
This setting requires a restart.
Type: <<types,Float>>
@ -1658,6 +1666,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
WARNING: On Qt 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt].
This setting requires a restart.
Type: <<types,String>>
@ -1677,6 +1686,7 @@ On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.policy.page]]
=== colors.webpage.darkmode.policy.page
Which pages to apply dark mode to.
This setting requires a restart.
Type: <<types,String>>
@ -1697,6 +1707,7 @@ On QtWebKit, this setting is unavailable.
Threshold for inverting background elements with dark mode.
Background elements with brightness above this threshold will be inverted, and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it.
Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`!
This setting requires a restart.
Type: <<types,Int>>
@ -1711,6 +1722,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.threshold.text
Threshold for inverting text with dark mode.
Text colors with brightness below this threshold will be inverted, and above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color.
This setting requires a restart.
Type: <<types,Int>>
@ -1854,6 +1866,7 @@ Default: +pass:[false]+
A list of patterns which should not be shown in the history.
This only affects the completion. Matching URLs are still saved in the history (and visible on the qute://history page), but hidden in the completion.
Changing this setting will cause the completion history to be regenerated on the next start, which will take a short while.
This setting requires a restart.
Type: <<types,List of UrlPattern>>
@ -2013,6 +2026,7 @@ Default: empty
=== content.canvas_reading
Allow websites to read canvas elements.
Note this is needed for some websites to work properly.
This setting requires a restart.
Type: <<types,Bool>>
@ -2173,6 +2187,7 @@ Default: +pass:[true]+
When to send the Referer header.
The Referer header tells websites from which website you were coming from when visiting them.
No restart is needed with QtWebKit.
This setting requires a restart.
Type: <<types,String>>
@ -2569,6 +2584,7 @@ On QtWebKit, this setting is unavailable.
[[content.site_specific_quirks]]
=== content.site_specific_quirks
Enable quirks (such as faked user agent headers) needed to get specific sites to work properly.
This setting requires a restart.
Type: <<types,Bool>>
@ -2633,6 +2649,7 @@ Default: +pass:[true]+
=== content.webrtc_ip_handling_policy
Which interfaces to expose via WebRTC.
On Qt 5.10, this option doesn't work because of a Qt bug.
This setting requires a restart.
Type: <<types,String>>
@ -3460,6 +3477,7 @@ Default: +pass:[8]+
=== qt.args
Additional arguments to pass to Qt, without leading `--`.
With QtWebEngine, some Chromium arguments (see https://peter.sh/experiments/chromium-command-line-switches/ for a list) will work.
This setting requires a restart.
Type: <<types,List of String>>
@ -3470,6 +3488,7 @@ Default: empty
=== qt.force_platform
Force a Qt platform to use.
This sets the `QT_QPA_PLATFORM` environment variable and is useful to force using the XCB plugin when running QtWebEngine on Wayland.
This setting requires a restart.
Type: <<types,String>>
@ -3480,6 +3499,7 @@ Default: empty
=== qt.force_platformtheme
Force a Qt platformtheme to use.
This sets the `QT_QPA_PLATFORMTHEME` environment variable which controls dialogs like the filepicker. By default, Qt determines the platform theme based on the desktop environment.
This setting requires a restart.
Type: <<types,String>>
@ -3490,6 +3510,7 @@ Default: empty
=== qt.force_software_rendering
Force software rendering for QtWebEngine.
This is needed for QtWebEngine to work with Nouveau drivers and can be useful in other scenarios related to graphic issues.
This setting requires a restart.
Type: <<types,String>>
@ -3510,6 +3531,7 @@ This setting is only available with the QtWebEngine backend.
Turn on Qt HighDPI scaling.
This is equivalent to setting QT_AUTO_SCREEN_SCALE_FACTOR=1 or QT_ENABLE_HIGHDPI_SCALING=1 (Qt >= 5.14) in the environment.
It's off by default as it can cause issues with some bitmap fonts. As an alternative to this, it's possible to set font sizes and the `zoom.default` setting.
This setting requires a restart.
Type: <<types,Bool>>
@ -3520,6 +3542,7 @@ Default: +pass:[false]+
=== qt.low_end_device_mode
When to use Chromium's low-end device mode.
This improves the RAM usage of renderer processes, at the expense of performance.
This setting requires a restart.
Type: <<types,String>>
@ -3542,6 +3565,7 @@ See the following pages for more details:
- https://www.chromium.org/developers/design-documents/process-models
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
This setting requires a restart.
Type: <<types,String>>

View File

@ -46,24 +46,25 @@ If you get stuck, you can get help in multiple ways:
* The `:help` command inside qutebrowser shows the built-in documentation.
Additionally, each command can be started with a `--help` flag to show its
help.
* IRC channel: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
* Chat via the IRC channel: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
http://freenode.net/[Freenode]
(https://webchat.freenode.net/?channels=#qutebrowser[webchat])
* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] (
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[subscribe])
* On Reddit: https://www.reddit.com/r/qutebrowser/[/r/qutebrowser]
* Via https://github.com/qutebrowser/qutebrowser/discussions[GitHub Discussions]
* Using the mailinglist: mailto:qutebrowser@lists.qutebrowser.org[]
(https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[subscribe])
Donating
--------
Working on qutebrowser is a very rewarding hobby, but like (nearly) all hobbies
it also costs some money. Namely I have to pay for the server and domain, and
do occasional hardware upgrades footnote:[It turned out a 160 GB SSD is rather
small - the VMs and custom Qt builds I use for testing/developing qutebrowser
need about 100 GB of space].
qutebrowser's primary maintainer, The-Compiler, is currently working part-time on
qutebrowser, funded by donations.
If you want to give me a beer or a pizza back, I'm trying to make it as easy as
possible for you to do so. If some other way would be easier for you, please
get in touch!
To sustain this for a long time, your help is needed! Check the
https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] for more information.
Depending on your sign-up date and how long you keep a certain level, you can get
qutebrowser t-shirts, stickers and more!
* PayPal: me@the-compiler.org
* Bitcoin: link:bitcoin:1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE[1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE]
Alternatively, there are also various options available for one-time donations, see the
https://github.com/qutebrowser/qutebrowser/blob/master/README.asciidoc#donating[donation section]
in the README for details.

View File

@ -2,15 +2,15 @@
bump2version==1.0.0
certifi==2020.6.20
cffi==1.14.1
cffi==1.14.2
chardet==3.0.4
colorama==0.4.3
cryptography==3.0
cssutils==1.0.2
github3.py==1.3.0
hunter==3.1.3
hunter==3.2.1
idna==2.10
jwcrypto==0.7
jwcrypto==0.8
manhole==1.6.0
packaging==20.4
pycparser==2.20

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==19.3.0
attrs==20.1.0
flake8==3.8.3
flake8-bugbear==20.1.4
flake8-builtins==1.5.3
@ -18,7 +18,7 @@ flake8-tuple==0.4.1
mccabe==0.6.1
pep8-naming==0.11.1
pycodestyle==2.6.0
pydocstyle==5.0.2
pydocstyle==5.1.0
pyflakes==2.2.0
six==1.15.0
snowballstemmer==2.0.0

View File

@ -13,4 +13,4 @@ Pygments==2.6.1
-e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5_stubs
six==1.15.0
typed-ast==1.4.1
typing-extensions==3.7.4.2
typing-extensions==3.7.4.3

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.17
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=pyinstaller
pyinstaller-hooks-contrib==2020.6
pyinstaller==4.0
pyinstaller-hooks-contrib==2020.7

View File

@ -1,4 +1 @@
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
# remove @commit-id for scm installs
#@ replace: @.*# @develop#
PyInstaller

View File

@ -2,13 +2,13 @@
astroid==2.3.3 # rq.filter: < 2.4
certifi==2020.6.20
cffi==1.14.1
cffi==1.14.2
chardet==3.0.4
cryptography==3.0
github3.py==1.3.0
idna==2.10
isort==4.3.21
jwcrypto==0.7
jwcrypto==0.8
lazy-object-proxy==1.4.3
mccabe==0.6.1
pycparser==2.20

View File

@ -16,7 +16,7 @@ pytz==2020.1
requests==2.24.0
six==1.15.0
snowballstemmer==2.0.0
Sphinx==3.1.2
Sphinx==3.2.1
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3

View File

@ -1,20 +1,20 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==19.3.0
attrs==20.1.0
beautifulsoup4==4.9.1
certifi==2020.6.20
chardet==3.0.4
cheroot==8.4.2
cheroot==8.4.5
click==7.1.2
# colorama==0.4.3
coverage==5.2.1
EasyProcess==0.3
Flask==1.1.2
glob2==0.7
hunter==3.1.3
hypothesis==5.23.7
hunter==3.2.1
hypothesis==5.29.0
idna==2.10
iniconfig==1.0.0
iniconfig==1.0.1
itsdangerous==1.1.0
jaraco.functools==3.0.1 ; python_version>="3.6"
# Jinja2==2.11.2
@ -33,9 +33,9 @@ pyparsing==2.4.7
pytest==6.0.1
pytest-bdd==3.4.0
pytest-benchmark==3.2.3
pytest-cov==2.10.0
pytest-cov==2.10.1
pytest-instafail==0.4.2
pytest-mock==3.2.0
pytest-mock==3.3.0
pytest-qt==3.3.0
pytest-repeat==0.8.0
pytest-rerunfailures==9.0
@ -46,9 +46,10 @@ requests-file==1.5.1
six==1.15.0
sortedcontainers==2.2.2
soupsieve==2.0.1
tldextract==2.2.2
tldextract==2.2.3
toml==0.10.1
urllib3==1.25.10
vulture==1.6
vulture==2.1 ; python_version>="3.6"
Werkzeug==1.0.1
jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0
jaraco.functools==2.0; python_version<"3.6"
vulture==1.6; python_version<"3.6"

View File

@ -30,5 +30,9 @@ PyVirtualDisplay
tldextract
#@ markers: jaraco.functools python_version>="3.6"
#@ add: jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0
#@ add: jaraco.functools==2.0; python_version<"3.6"
#@ markers: vulture python_version>="3.6"
#@ add: vulture==1.6; python_version<"3.6"
#@ ignore: Jinja2, MarkupSafe, colorama

View File

@ -9,7 +9,7 @@ py==1.9.0
pyparsing==2.4.7
six==1.15.0
toml==0.10.1
tox==3.18.1
tox==3.19.0
tox-pip-version==0.0.7
tox-venv==0.4.0
virtualenv==20.0.28
virtualenv==20.0.31

View File

@ -1,3 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
vulture==1.6
toml==0.10.1
vulture==2.1

View File

@ -27,7 +27,7 @@ for example: "github.com/cryzed" or "websites/github.com". How the username and
password are determined is freely configurable using the CLI arguments. As an
example, if you instead store the username as part of the secret (and use a
site's name as filename), instead of the default configuration, use
`--username-target secret` and `--username-regex "username: (.+)"`.
`--username-target secret` and `--username-pattern "username: (.+)"`.
The login information is inserted by emulating key events using qutebrowser's
fake-key command in this manner: [USERNAME]<Tab>[PASSWORD], which is compatible

View File

@ -1,27 +1,26 @@
#!/usr/bin/env node
//
//
// # Description
//
//
// Summarize the current page in a new tab, by processing it with the standalone readability
// library used for Firefox Reader View.
//
//
// # Prerequisites
//
// - NODE_PATH might be required to point to your global node libraries:
//
// - Setting NODE_PATH might be required to point qutebrowser to your global node libraries:
// export NODE_PATH=$NODE_PATH:$(npm root -g)
// - Mozilla's readability library (npm install -g https://github.com/mozilla/readability.git)
// NOTE: You might have to *login* as root for a system-wide installation to work (e.g. sudo -s)
// - Mozilla's readability library (npm install -g @mozilla/readability)
// - jsdom (npm install -g jsdom)
// - qutejs (npm install -g qutejs)
//
//
// # Usage
//
//
// :spawn --userscript readability-js
//
// One may wish to define an easy to type command alias in Qutebrowser's configuration file:
//
// One may wish to define an easy to type command alias in qutebrowser's configuration file:
// c.aliases = {"readability" : "spawn --userscript readability-js", ...}
const Readability = require('readability');
const { Readability } = require('@mozilla/readability');
const qute = require('qutejs');
const JSDOM = require('jsdom').JSDOM;
const fs = require('fs');

View File

@ -38,6 +38,7 @@ markers =
unicode_locale: Tests which need an unicode locale to work
qtwebkit6021_xfail: Tests which would fail on WebKit version 602.1
js_headers: Sets JS headers dynamically on QtWebEngine (unsupported on some versions)
qtwebkit_pdf_imageformat_skip: Broken on QtWebKit with PDF image format plugin installed
qt_log_level_fail = WARNING
qt_log_ignore =
^SpellCheck: .*

View File

@ -485,8 +485,7 @@ def _init_modules(*, args):
cache.init(q_app)
log.init.debug("Initializing downloads...")
download_manager = qtnetworkdownloads.DownloadManager(parent=q_app)
objreg.register('qtnetwork-download-manager', download_manager)
qtnetworkdownloads.init()
log.init.debug("Initializing Greasemonkey...")
greasemonkey.init()

View File

@ -72,9 +72,11 @@ def create(win_id: int,
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
tab_class = webenginetab.WebEngineTab # type: typing.Type[AbstractTab]
else:
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkittab
tab_class = webkittab.WebKitTab
else:
raise utils.Unreachable(objects.backend)
return tab_class(win_id=win_id, mode_manager=mode_manager, private=private,
parent=parent)
@ -84,6 +86,8 @@ def init() -> None:
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
webenginetab.init()
return
assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
class WebTabError(Exception):

View File

@ -170,7 +170,7 @@ class CommandDispatcher:
elif mode == "stack-next":
tab = tab_deque.next(cur_tab)
else:
raise NotImplementedError(
raise utils.Unreachable(
"Missing implementation for stack mode!")
except IndexError:
if not show_error:
@ -562,7 +562,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
'decrement'])
'decrement', 'strip'])
@cmdutils.argument('count', value=cmdutils.Value.count)
def navigate(self, where: str, tab: bool = False, bg: bool = False,
window: bool = False, count: int = 1) -> None:
@ -587,6 +587,7 @@ class CommandDispatcher:
Uses the
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option.
- `strip`: Strip query and fragment from the current URL.
tab: Open in a new tab.
bg: Open in a background tab.
@ -613,9 +614,7 @@ class CommandDispatcher:
handler = handlers[where]
handler(browsertab=widget, win_id=self._win_id, baseurl=url,
tab=tab, background=bg, window=window)
elif where in ['up', 'increment', 'decrement']:
if where == 'up':
url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
elif where in ['up', 'increment', 'decrement', 'strip']:
new_url = handlers[where](url, count)
self._open(new_url, tab, bg, window, related=True)
else: # pragma: no cover
@ -1627,9 +1626,31 @@ class CommandDispatcher:
tab.search.prev_result()
tab.search.prev_result(result_cb=cb)
def _jseval_cb(self, out):
"""Show the data returned from JS."""
if out is None:
# Getting the actual error (if any) seems to be difficult.
# The error does end up in
# BrowserPage.javaScriptConsoleMessage(), but
# distinguishing between :jseval errors and errors from the
# webpage is not trivial...
message.info('No output or error')
else:
# The output can be a string, number, dict, array, etc. But
# *don't* output too much data, as this will make
# qutebrowser hang
out = str(out)
if len(out) > 5000:
out = out[:5000] + ' [...trimmed...]'
message.info(out)
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_cmd_split=True)
def jseval(self, js_code: str, file: bool = False, quiet: bool = False, *,
def jseval(self, js_code: str,
file: bool = False,
url: bool = False,
quiet: bool = False,
*,
world: typing.Union[usertypes.JsWorld, int] = None) -> None:
"""Evaluate a JavaScript string.
@ -1639,33 +1660,16 @@ class CommandDispatcher:
If the path is relative, the file is searched in a js/ subdir
in qutebrowser's data dir, e.g.
`~/.local/share/qutebrowser/js`.
url: Interpret js-code as a `javascript:...` URL.
quiet: Don't show resulting JS object.
world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to
run the snippet in.
"""
cmdutils.check_exclusive((file, url), 'fu')
if world is None:
world = usertypes.JsWorld.jseval
if quiet:
jseval_cb = None
else:
def jseval_cb(out):
"""Show the data returned from JS."""
if out is None:
# Getting the actual error (if any) seems to be difficult.
# The error does end up in
# BrowserPage.javaScriptConsoleMessage(), but
# distinguishing between :jseval errors and errors from the
# webpage is not trivial...
message.info('No output or error')
else:
# The output can be a string, number, dict, array, etc. But
# *don't* output too much data, as this will make
# qutebrowser hang
out = str(out)
if len(out) > 5000:
out = out[:5000] + ' [...trimmed...]'
message.info(out)
jseval_cb = None if quiet else self._jseval_cb
if file:
path = os.path.expanduser(js_code)
@ -1677,6 +1681,11 @@ class CommandDispatcher:
js_code = f.read()
except OSError as e:
raise cmdutils.CommandError(str(e))
elif url:
try:
js_code = urlutils.parse_javascript_url(QUrl(js_code))
except urlutils.Error as e:
raise cmdutils.CommandError(str(e))
widget = self._current_widget()
try:

View File

@ -427,6 +427,7 @@ class AbstractDownloadItem(QObject):
raw_headers: The headers sent by the server.
_filename: The filename of the download.
_dead: Whether the Download has _die()'d.
_manager: The DownloadManager which started this download.
Signals:
data_changed: The downloads metadata changed.
@ -448,8 +449,9 @@ class AbstractDownloadItem(QObject):
remove_requested = pyqtSignal()
pdfjs_requested = pyqtSignal(str, QUrl)
def __init__(self, parent=None):
def __init__(self, manager, parent=None):
super().__init__(parent)
self._manager = manager
self.done = False
self.stats = DownloadItemStats(self)
self.index = 0
@ -651,7 +653,7 @@ class AbstractDownloadItem(QObject):
"""Finish initialization based on self._filename."""
raise NotImplementedError
def _ask_confirm_question(self, title, msg):
def _ask_confirm_question(self, title, msg, *, custom_yes_action=None):
"""Ask a confirmation question for the download."""
raise NotImplementedError
@ -746,7 +748,13 @@ class AbstractDownloadItem(QObject):
last_used_directory = os.path.dirname(self._filename)
log.downloads.debug("Setting filename to {}".format(self._filename))
if force_overwrite:
if self._get_conflicting_download():
txt = ("<b>{}</b> is already downloading. Cancel and "
"re-download?".format(html.escape(self._filename)))
self._ask_confirm_question(
"Cancel other download?", txt,
custom_yes_action=self._cancel_conflicting_download)
elif force_overwrite:
self._after_set_filename()
elif os.path.isfile(self._filename):
# The file already exists, so ask the user if it should be
@ -763,6 +771,28 @@ class AbstractDownloadItem(QObject):
else:
self._after_set_filename()
def _conflicts_with(self, other: 'AbstractDownloadItem') -> bool:
"""Check if this download conflicts with the other given one."""
return (
other is not self and
other._filename == self._filename and # pylint: disable=protected-access
not other.done
)
def _get_conflicting_download(self):
"""Return another potential active download with the same name."""
for download in self._manager.downloads:
if self._conflicts_with(download):
return download
return None
def _cancel_conflicting_download(self):
"""Cancel any conflicting download and call _after_set_filename."""
conflicting_download = self._get_conflicting_download()
if conflicting_download:
conflicting_download.cancel(remove_data=False)
self._after_set_filename()
def _open_if_successful(self, cmdline):
"""Open the downloaded file, but only if it was successful.
@ -947,6 +977,12 @@ class AbstractDownloadManager(QObject):
download.cancelled.connect(question.abort)
download.error.connect(question.abort)
@pyqtSlot()
def shutdown(self):
"""Cancel all downloads when shutting down."""
for download in self.downloads:
download.cancel(remove_data=False)
class DownloadModel(QAbstractListModel):

View File

@ -27,6 +27,43 @@ from qutebrowser.misc import objects
from qutebrowser.keyinput import modeman
class FocusWorkaroundEventFilter(QObject):
"""An event filter working Qt 5.11 keyboard focus issues.
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
"""
def __init__(self, win_id, widget, parent=None):
super().__init__(parent)
self._win_id = win_id
self._widget = widget
def eventFilter(self, _obj, event):
"""Act on ChildAdded events."""
if event.type() != QEvent.ChildAdded:
return False
pass_modes = [usertypes.KeyMode.command,
usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]
if modeman.instance(self._win_id).mode in pass_modes:
return False
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
current_index = tabbed_browser.widget.currentIndex()
try:
widget_index = tabbed_browser.widget.indexOf(self._widget.parent())
except RuntimeError:
widget_index = -1
if current_index == widget_index:
QTimer.singleShot(0, self._widget.setFocus)
return False
class ChildEventFilter(QObject):
"""An event filter re-adding TabEventFilter on ChildEvent.
@ -39,43 +76,12 @@ class ChildEventFilter(QObject):
Attributes:
_filter: The event filter to install.
_widget: The widget expected to send out childEvents.
_win_id: The window this ChildEventFilter lives in.
_focus_workaround: Whether to enable a workaround for QTBUG-68076.
"""
def __init__(self, *, eventfilter, win_id, focus_workaround=False,
widget=None, parent=None):
def __init__(self, *, eventfilter, widget=None, parent=None):
super().__init__(parent)
self._filter = eventfilter
self._widget = widget
self._win_id = win_id
self._focus_workaround = focus_workaround
if focus_workaround:
assert widget is not None
def _do_focus_workaround(self):
"""WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076."""
if not self._focus_workaround:
return
assert self._widget is not None
pass_modes = [usertypes.KeyMode.command,
usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]
if modeman.instance(self._win_id).mode in pass_modes:
return
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
current_index = tabbed_browser.widget.currentIndex()
try:
widget_index = tabbed_browser.widget.indexOf(self._widget.parent())
except RuntimeError:
widget_index = -1
if current_index == widget_index:
QTimer.singleShot(0, self._widget.setFocus)
def eventFilter(self, obj, event):
"""Act on ChildAdded events."""
@ -89,7 +95,6 @@ class ChildEventFilter(QObject):
assert obj is self._widget
child.installEventFilter(self._filter)
self._do_focus_workaround()
elif event.type() == QEvent.ChildRemoved:
child = event.child()
log.misc.debug("{}: removed child {}".format(obj, child))

View File

@ -136,6 +136,7 @@ class GreasemonkeyScript:
those by forcing them to use document-end instead.
"""
if objects.backend != usertypes.Backend.QtWebEngine:
assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
return False
elif not qtutils.version_check('5.12', compiled=False):
return False

View File

@ -401,3 +401,5 @@ def init(parent=None):
if objects.backend == usertypes.Backend.QtWebKit: # pragma: no cover
from qutebrowser.browser.webkit import webkithistory
webkithistory.init(web_history)
return
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend

View File

@ -30,7 +30,7 @@ from PyQt5.QtGui import QCloseEvent
from qutebrowser.browser import eventfilter
from qutebrowser.config import configfiles
from qutebrowser.utils import log, usertypes
from qutebrowser.utils import log, usertypes, utils
from qutebrowser.keyinput import modeman
from qutebrowser.misc import miscwidgets, objects
@ -55,9 +55,10 @@ def create(*, splitter: 'miscwidgets.InspectorSplitter',
else:
return webengineinspector.LegacyWebEngineInspector(
splitter, win_id, parent)
else:
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkitinspector
return webkitinspector.WebKitInspector(splitter, win_id, parent)
raise utils.Unreachable(objects.backend)
class Position(enum.Enum):
@ -91,15 +92,12 @@ class _EventFilter(QObject):
the QWebInspector.
"""
def __init__(self, win_id: int, parent: QObject) -> None:
super().__init__(parent)
self._win_id = win_id
clicked = pyqtSignal()
def eventFilter(self, _obj: QObject, event: QEvent) -> bool:
"""Enter insert mode if the inspector is clicked."""
"""Translate mouse presses to a clicked signal."""
if event.type() == QEvent.MouseButtonPress:
modeman.enter(self._win_id, usertypes.KeyMode.insert,
reason='Inspector clicked', only_if_normal=True)
self.clicked.emit()
return False
@ -125,10 +123,12 @@ class AbstractWebInspector(QWidget):
self._layout = miscwidgets.WrapperLayout(self)
self._splitter = splitter
self._position = None # type: typing.Optional[Position]
self._event_filter = _EventFilter(win_id, parent=self)
self._win_id = win_id
self._event_filter = _EventFilter(parent=self)
self._event_filter.clicked.connect(self._on_clicked)
self._child_event_filter = eventfilter.ChildEventFilter(
eventfilter=self._event_filter,
win_id=win_id,
parent=self)
def _set_widget(self, widget: QWidget) -> None:
@ -156,6 +156,13 @@ class AbstractWebInspector(QWidget):
"""
return False
@pyqtSlot()
def _on_clicked(self) -> None:
"""Enter insert mode if a docked inspector was clicked."""
if self._position != Position.window:
modeman.enter(self._win_id, usertypes.KeyMode.insert,
reason='Inspector clicked', only_if_normal=True)
def set_position(self, position: typing.Optional[Position]) -> None:
"""Set the position of the inspector.

View File

@ -132,6 +132,8 @@ def path_up(url, count):
url: The current url.
count: The number of levels to go up in the url.
"""
urlutils.ensure_valid(url)
url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
path = url.path()
if not path or path == '/':
raise Error("Can't go up!")
@ -142,6 +144,14 @@ def path_up(url, count):
return url
def strip(url, count):
"""Strip fragment/query from a URL."""
if count != 1:
raise Error("Count is not supported when stripping URL components")
urlutils.ensure_valid(url)
return url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
def _find_prevnext(prev, elems):
"""Find a prev/next element in the given list of elements."""
# First check for <link rel="prev(ious)|next">

View File

@ -23,7 +23,7 @@ from PyQt5.QtCore import QUrl, pyqtSlot
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.config import config, configtypes
from qutebrowser.utils import message, usertypes, urlutils
from qutebrowser.utils import message, usertypes, urlutils, utils
from qutebrowser.misc import objects
from qutebrowser.browser.network import pac
@ -105,8 +105,10 @@ class ProxyFactory(QNetworkProxyFactory):
proxy = urlutils.proxy_from_url(QUrl('direct://'))
assert not isinstance(proxy, pac.PACFetcher)
proxies = [proxy]
else:
elif objects.backend == usertypes.Backend.QtWebKit:
proxies = proxy.resolve(query)
else:
raise utils.Unreachable(objects.backend)
else:
proxies = [proxy]
for proxy in proxies:

View File

@ -27,10 +27,12 @@ import typing
import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from qutebrowser.config import config, websettings
from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug
from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg
from qutebrowser.misc import quitter
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
@ -70,7 +72,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
target file.
_read_timer: A Timer which reads the QNetworkReply into self._buffer
periodically.
_manager: The DownloadManager which started this download
_reply: The QNetworkReply associated with this download.
_autoclose: Whether to close the associated file when the download is
done.
@ -90,12 +91,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
Args:
reply: The QNetworkReply to download.
"""
super().__init__(parent=manager)
super().__init__(manager=manager, parent=manager)
self.fileobj = None # type: typing.Optional[typing.IO[bytes]]
self.raw_headers = {} # type: typing.Dict[bytes, bytes]
self._autoclose = True
self._manager = manager
self._retry_info = None
self._reply = None
self._buffer = io.BytesIO()
@ -206,11 +206,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
def _after_set_filename(self):
self._create_fileobj()
def _ask_confirm_question(self, title, msg):
def _ask_confirm_question(self, title, msg, *, custom_yes_action=None):
yes_action = custom_yes_action or self._after_set_filename
no_action = functools.partial(self.cancel, remove_data=False)
url = 'file://{}'.format(self._filename)
message.confirm_async(title=title, text=msg,
yes_action=self._after_set_filename,
message.confirm_async(title=title, text=msg, yes_action=yes_action,
no_action=no_action, cancel_action=no_action,
abort_on=[self.cancelled, self.error], url=url)
@ -578,3 +578,10 @@ class DownloadManager(downloads.AbstractDownloadManager):
if download._uses_nam(nam): # pylint: disable=protected-access
nam.adopt_download(download)
return nam.adopted_downloads
def init():
"""Initialize the global QtNetwork download manager."""
download_manager = DownloadManager(parent=QApplication.instance())
objreg.register('qtnetwork-download-manager', download_manager)
quitter.instance.shutting_down.connect(download_manager.shutdown)

View File

@ -28,7 +28,7 @@ from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QObject
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
from qutebrowser.browser import downloads, pdfjs
from qutebrowser.utils import debug, usertypes, message, log, qtutils
from qutebrowser.utils import debug, usertypes, message, log, qtutils, objreg
class DownloadItem(downloads.AbstractDownloadItem):
@ -40,8 +40,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
"""
def __init__(self, qt_item: QWebEngineDownloadItem,
manager: downloads.AbstractDownloadManager,
parent: QObject = None) -> None:
super().__init__(parent)
super().__init__(manager=manager, parent=manager)
self._qt_item = qt_item
qt_item.downloadProgress.connect( # type: ignore[attr-defined]
self.stats.on_download_progress)
@ -140,14 +141,15 @@ class DownloadItem(downloads.AbstractDownloadItem):
"state {} (not in requested state)!".format(
filename, self, state_name))
def _ask_confirm_question(self, title, msg):
def _ask_confirm_question(self, title, msg, *, custom_yes_action=None):
yes_action = custom_yes_action or self._after_set_filename
no_action = functools.partial(self.cancel, remove_data=False)
question = usertypes.Question()
question.title = title
question.text = msg
question.url = 'file://{}'.format(self._filename)
question.mode = usertypes.PromptMode.yesno
question.answered_yes.connect(self._after_set_filename)
question.answered_yes.connect(yes_action)
question.answered_no.connect(no_action)
question.cancelled.connect(no_action)
self.cancelled.connect(question.abort)
@ -185,6 +187,26 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._qt_item.accept()
def _get_conflicting_download(self):
"""Return another potential active download with the same name.
webenginedownloads.DownloadItem needs to look for downloads both in its
manager and in qtnetwork-download-manager as both are used
simultaneously.
This method can be safely removed once #2328 is fixed.
"""
conflicting_download = super()._get_conflicting_download()
if conflicting_download:
return conflicting_download
qtnetwork_download_manager = objreg.get(
'qtnetwork-download-manager')
for download in qtnetwork_download_manager.downloads:
if self._conflicts_with(download):
return download
return None
def _get_suggested_filename(path):
"""Convert a path we got from chromium to a suggested filename.
@ -244,7 +266,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
suggested_filename = _get_suggested_filename(qt_item.path())
use_pdfjs = pdfjs.should_use_pdfjs(qt_item.mimeType(), qt_item.url())
download = DownloadItem(qt_item)
download = DownloadItem(qt_item, manager=self)
self._init_item(download, auto_remove=use_pdfjs,
suggested_filename=suggested_filename)

View File

@ -55,45 +55,45 @@ class _SettingsWrapper:
For read operations, the default profile value is always used.
"""
def __init__(self):
self._settings = [default_profile.settings()]
def _settings(self):
yield default_profile.settings()
if private_profile:
self._settings.append(private_profile.settings())
yield private_profile.settings()
def setAttribute(self, attribute, on):
for settings in self._settings:
for settings in self._settings():
settings.setAttribute(attribute, on)
def setFontFamily(self, which, family):
for settings in self._settings:
for settings in self._settings():
settings.setFontFamily(which, family)
def setFontSize(self, fonttype, size):
for settings in self._settings:
for settings in self._settings():
settings.setFontSize(fonttype, size)
def setDefaultTextEncoding(self, encoding):
for settings in self._settings:
for settings in self._settings():
settings.setDefaultTextEncoding(encoding)
def setUnknownUrlSchemePolicy(self, policy):
for settings in self._settings:
for settings in self._settings():
settings.setUnknownUrlSchemePolicy(policy)
def testAttribute(self, attribute):
return self._settings[0].testAttribute(attribute)
return default_profile.settings().testAttribute(attribute)
def fontSize(self, fonttype):
return self._settings[0].fontSize(fonttype)
return default_profile.settings().fontSize(fonttype)
def fontFamily(self, which):
return self._settings[0].fontFamily(which)
return default_profile.settings().fontFamily(which)
def defaultTextEncoding(self):
return self._settings[0].defaultTextEncoding()
return default_profile.settings().defaultTextEncoding()
def unknownUrlSchemePolicy(self):
return self._settings[0].unknownUrlSchemePolicy()
return default_profile.settings().unknownUrlSchemePolicy()
class WebEngineSettings(websettings.AbstractSettings):
@ -360,9 +360,9 @@ def init_user_agent():
_init_user_agent_str(QWebEngineProfile.defaultProfile().httpUserAgent())
def _init_profiles():
"""Init the two used QWebEngineProfiles."""
global default_profile, private_profile
def _init_default_profile():
"""Init the default QWebEngineProfile."""
global default_profile
default_profile = QWebEngineProfile.defaultProfile()
init_user_agent()
@ -376,6 +376,11 @@ def _init_profiles():
default_profile.setter.init_profile()
default_profile.setter.set_persistent_cookie_policy()
def init_private_profile():
"""Init the private QWebEngineProfile."""
global private_profile
if not qtutils.is_single_process():
private_profile = QWebEngineProfile()
private_profile.setter = ProfileSetter( # type: ignore[attr-defined]
@ -450,7 +455,8 @@ def init(args):
webenginequtescheme.init()
spell.init()
_init_profiles()
_init_default_profile()
init_private_profile()
config.instance.changed.connect(_update_settings)
global global_settings

View File

@ -38,7 +38,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme,
cookies, webenginedownloads,
webenginesettings, certificateerror)
from qutebrowser.misc import miscwidgets, objects
from qutebrowser.misc import miscwidgets, objects, quitter
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, objreg, jinja, debug)
from qutebrowser.keyinput import modeman
@ -74,6 +74,7 @@ def init():
if webenginesettings.private_profile:
download_manager.install(webenginesettings.private_profile)
objreg.register('webengine-download-manager', download_manager)
quitter.instance.shutting_down.connect(download_manager.shutdown)
log.init.debug("Initializing cookie filter...")
cookies.install_filter(webenginesettings.default_profile)
@ -1392,15 +1393,20 @@ class WebEngineTab(browsertab.AbstractTab):
fp = self._widget.focusProxy()
if fp is not None:
fp.installEventFilter(self._tab_event_filter)
self._child_event_filter = eventfilter.ChildEventFilter(
eventfilter=self._tab_event_filter,
widget=self._widget,
win_id=self.win_id,
focus_workaround=qtutils.version_check(
'5.11', compiled=False, exact=True),
parent=self)
self._widget.installEventFilter(self._child_event_filter)
if qtutils.version_check('5.11', compiled=False, exact=True):
focus_event_filter = eventfilter.FocusWorkaroundEventFilter(
win_id=self.win_id,
widget=self._widget,
parent=self)
self._widget.installEventFilter(focus_event_filter)
@pyqtSlot()
def _restore_zoom(self):
if sip.isdeleted(self._widget):

View File

@ -55,6 +55,24 @@ class WebKitAction(browsertab.AbstractAction):
def show_source(self, pygments=False):
self._show_source_pygments()
def run_string(self, name: str) -> None:
"""Add special cases for new API.
Those were added to QtWebKit 5.212 (which we enforce), but we don't get
the new API from PyQt. Thus, we'll need to use the raw numbers.
"""
new_actions = {
# https://github.com/qtwebkit/qtwebkit/commit/a96d9ef5d24b02d996ad14ff050d0e485c9ddc97
'RequestClose': QWebPage.ToggleVideoFullscreen + 1,
# https://github.com/qtwebkit/qtwebkit/commit/96b9ba6269a5be44343635a7aaca4a153ea0366b
'Unselect': QWebPage.ToggleVideoFullscreen + 2,
}
if name in new_actions:
self._widget.triggerPageAction(new_actions[name])
return
super().run_string(name)
class WebKitPrinting(browsertab.AbstractPrinting):

View File

@ -26,7 +26,7 @@ import re
import html
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
from PyQt5.QtCore import QRectF, QSize, Qt
from PyQt5.QtCore import QRectF, QRegularExpression, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout, QSyntaxHighlighter,
QTextCharFormat)
@ -41,14 +41,23 @@ class _Highlighter(QSyntaxHighlighter):
super().__init__(doc)
self._format = QTextCharFormat()
self._format.setForeground(color)
self._pattern = pattern
words = pattern.split()
words.sort(key=len, reverse=True)
pat = "|".join(re.escape(word) for word in words)
self._expression = QRegularExpression(
pat, QRegularExpression.CaseInsensitiveOption
)
def highlightBlock(self, text):
"""Override highlightBlock for custom highlighting."""
for match in re.finditer(self._pattern, text, re.IGNORECASE):
start, end = match.span()
length = end - start
self.setFormat(start, length, self._format)
match_iterator = self._expression.globalMatch(text)
while match_iterator.hasNext():
match = match_iterator.next()
self.setFormat(
match.capturedStart(),
match.capturedLength(),
self._format
)
class CompletionItemDelegate(QStyledItemDelegate):
@ -226,12 +235,11 @@ class CompletionItemDelegate(QStyledItemDelegate):
pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index)
if index.column() in columns_to_filter and pattern:
pat = re.escape(pattern).replace(r'\ ', r'|')
if self._opt.state & QStyle.State_Selected:
color = config.val.colors.completion.item.selected.match.fg
else:
color = config.val.colors.completion.match.fg
_Highlighter(self._doc, pat, color)
_Highlighter(self._doc, pattern, color)
self._doc.setPlainText(self._opt.text)
else:
self._doc.setHtml(

View File

@ -205,6 +205,49 @@ class CompletionView(QTreeView):
raise utils.Unreachable
def _next_page(self, upwards):
"""Return the index a page away from the selected index.
Args:
upwards: Get previous item, not next.
Return:
A QModelIndex.
"""
old_idx = self.selectionModel().currentIndex()
idx = old_idx
model = self.model()
if not idx.isValid():
# No item selected yet
return model.last_item() if upwards else model.first_item()
# Find height of each CompletionView element
element_height = self.visualRect(idx).height()
page_length = self.height() // element_height
# Skip one pageful, except leave one old line visible
offset = -(page_length - 1) if upwards else page_length - 1
idx = model.sibling(old_idx.row() + offset, old_idx.column(), old_idx)
# Skip category headers
while idx.isValid() and not idx.parent().isValid():
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
if idx.isValid():
return idx
border_item = model.first_item() if upwards else model.last_item()
# Wrap around if we were already at the beginning/end
if old_idx == border_item:
return self._next_idx(upwards)
# Select the first/last item before wrapping around
if upwards:
self.scrollTo(border_item.parent())
return border_item
def _next_category_idx(self, upwards):
"""Get the index of the previous/next category.
@ -238,14 +281,17 @@ class CompletionView(QTreeView):
@cmdutils.register(instance='completion',
modes=[usertypes.KeyMode.command], scope='window')
@cmdutils.argument('which', choices=['next', 'prev', 'next-category',
'prev-category'])
@cmdutils.argument('which', choices=['next', 'prev',
'next-category', 'prev-category',
'next-page', 'prev-page'])
@cmdutils.argument('history', flag='H')
def completion_item_focus(self, which, history=False):
"""Shift the focus of the completion menu to another item.
Args:
which: 'next', 'prev', 'next-category', or 'prev-category'.
which: 'next', 'prev',
'next-category', 'prev-category',
'next-page', or 'prev-page'.
history: Navigate through command history if no text was typed.
"""
if history:
@ -266,12 +312,14 @@ class CompletionView(QTreeView):
selmodel = self.selectionModel()
indices = {
'next': self._next_idx(upwards=False),
'prev': self._next_idx(upwards=True),
'next-category': self._next_category_idx(upwards=False),
'prev-category': self._next_category_idx(upwards=True),
'next': lambda: self._next_idx(upwards=False),
'prev': lambda: self._next_idx(upwards=True),
'next-category': lambda: self._next_category_idx(upwards=False),
'prev-category': lambda: self._next_category_idx(upwards=True),
'next-page': lambda: self._next_page(upwards=False),
'prev-page': lambda: self._next_page(upwards=True),
}
idx = indices[which]
idx = indices[which]()
if not idx.isValid():
return

View File

@ -3320,6 +3320,8 @@ bindings.default:
<Tab>: completion-item-focus next
<Ctrl-Tab>: completion-item-focus next-category
<Ctrl-Shift-Tab>: completion-item-focus prev-category
<PgDown>: completion-item-focus next-page
<PgUp>: completion-item-focus prev-page
<Ctrl-D>: completion-item-del
<Shift-Delete>: completion-item-del
<Ctrl-C>: completion-item-yank

View File

@ -50,6 +50,7 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]:
argv += ['--' + arg for arg in config.val.qt.args]
if objects.backend != usertypes.Backend.QtWebEngine:
assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
return argv
feature_flags = [flag for flag in argv
@ -307,6 +308,8 @@ def init_envvars() -> None:
os.environ['QT_QUICK_BACKEND'] = 'software'
elif software_rendering == 'chromium':
os.environ['QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND'] = '1'
else:
assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
if config.val.qt.force_platform is not None:
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform

View File

@ -30,7 +30,7 @@ from PyQt5.QtGui import QFont
import qutebrowser
from qutebrowser.config import config
from qutebrowser.utils import log, usertypes, urlmatch, qtutils
from qutebrowser.utils import log, usertypes, urlmatch, qtutils, utils
from qutebrowser.misc import objects, debugcachestats
UNSET = object()
@ -269,9 +269,11 @@ def init(args: argparse.Namespace) -> None:
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings
webenginesettings.init(args)
else:
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkitsettings
webkitsettings.init(args)
else:
raise utils.Unreachable(objects.backend)
# Make sure special URLs always get JS support
for pattern in ['chrome://*/*', 'qute://*/*']:
@ -280,12 +282,27 @@ def init(args: argparse.Namespace) -> None:
hide_userconfig=True)
def clear_private_data() -> None:
"""Clear cookies, cache and related data for private browsing sessions."""
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings
webenginesettings.init_private_profile()
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import cookies
assert cookies.ram_cookie_jar is not None
cookies.ram_cookie_jar.setAllCookies([])
else:
raise utils.Unreachable(objects.backend)
@pyqtSlot()
def shutdown() -> None:
"""Shut down QWeb(Engine)Settings."""
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings
webenginesettings.shutdown()
else:
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkitsettings
webkitsettings.shutdown()
else:
raise utils.Unreachable(objects.backend)

View File

@ -9,7 +9,9 @@
if (document.querySelector("a[href='https://support.google.com/chrome/answer/95414']")) {
navigator.serviceWorker.getRegistration().then((registration) => {
registration.unregister();
if (registration) {
registration.unregister();
}
document.location.reload();
});
}

View File

@ -32,7 +32,7 @@ from PyQt5.QtGui import QPalette
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
from qutebrowser.config import config, configfiles, stylesheet
from qutebrowser.config import config, configfiles, stylesheet, websettings
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
jinja, debug)
from qutebrowser.mainwindow import messageview, prompt
@ -231,10 +231,10 @@ class MainWindow(QWidget):
self._downloadview = downloadview.DownloadView(
model=self._download_model)
self._private = config.val.content.private_browsing or private
self.is_private = config.val.content.private_browsing or private
self.tabbed_browser = tabbedbrowser.TabbedBrowser(
win_id=self.win_id, private=self._private, parent=self
win_id=self.win_id, private=self.is_private, parent=self
) # type: tabbedbrowser.TabbedBrowser
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
window=self.win_id)
@ -243,7 +243,8 @@ class MainWindow(QWidget):
# We need to set an explicit parent for StatusBar because it does some
# show/hide magic immediately which would mean it'd show up as a
# window.
self.status = bar.StatusBar(win_id=self.win_id, private=self._private,
self.status = bar.StatusBar(win_id=self.win_id,
private=self.is_private,
parent=self)
self._add_widgets()
@ -310,12 +311,17 @@ class MainWindow(QWidget):
if not widget.isVisible():
return
size_hint = widget.sizeHint()
if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding:
width = self.width() - 2 * padding
if widget.hasHeightForWidth():
height = widget.heightForWidth(width)
else:
height = widget.sizeHint().height()
left = padding
else:
size_hint = widget.sizeHint()
width = min(size_hint.width(), self.width() - 2 * padding)
height = size_hint.height()
left = (self.width() - width) // 2 if centered else 0
height_padding = 20
@ -327,7 +333,7 @@ class MainWindow(QWidget):
else:
status_height = 0
bottom = self.height()
top = self.height() - status_height - size_hint.height()
top = self.height() - status_height - height
top = qtutils.check_overflow(top, 'int', fatal=False)
topleft = QPoint(left, max(height_padding, top))
bottomright = QPoint(left + width, bottom)
@ -339,7 +345,7 @@ class MainWindow(QWidget):
status_height = 0
top = 0
topleft = QPoint(left, top)
bottom = status_height + size_hint.height()
bottom = status_height + height
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
bottomright = QPoint(left + width,
min(self.height() - height_padding, bottom))
@ -674,15 +680,28 @@ class MainWindow(QWidget):
e.accept()
try:
last_visible = objreg.get('last-visible-main-window')
if self is last_visible:
objreg.delete('last-visible-main-window')
except KeyError:
pass
for key in ['last-visible-main-window', 'last-focused-main-window']:
try:
win = objreg.get(key)
if self is win:
objreg.delete(key)
except KeyError:
pass
sessions.session_manager.save_last_window_session()
self._save_geometry()
# Wipe private data if we close the last private window, but there are
# still other windows
if (
self.is_private and
len(objreg.window_registry) > 1 and
len([window for window in objreg.window_registry.values()
if window.is_private]) == 1
):
log.destroy.debug("Wiping private data before closing last "
"private window")
websettings.clear_private_data()
log.destroy.debug("Closing window {}".format(self.win_id))
self.tabbed_browser.shutdown()

View File

@ -21,7 +21,7 @@
import typing
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
from qutebrowser.config import config, stylesheet
@ -36,6 +36,7 @@ class Message(QLabel):
super().__init__(text, parent)
self.replace = replace
self.setAttribute(Qt.WA_StyledBackground, True)
self.setWordWrap(True)
qss = """
padding-top: 2px;
padding-bottom: 2px;
@ -64,8 +65,6 @@ class Message(QLabel):
"""
else: # pragma: no cover
raise ValueError("Invalid level {!r}".format(level))
# We don't bother with set_register_stylesheet here as it's short-lived
# anyways.
stylesheet.set_register(self, qss, update=False)
@ -89,12 +88,6 @@ class MessageView(QWidget):
self._last_text = None
def sizeHint(self):
"""Get the proposed height for the view."""
height = sum(label.sizeHint().height() for label in self._messages)
# The width isn't really relevant as we're expanding anyways.
return QSize(-1, height)
@config.change_filter('messages.timeout')
def _set_clear_timer_interval(self):
"""Configure self._clear_timer according to the config."""

View File

@ -38,7 +38,6 @@ class _WindowUndoEntry:
"""Information needed for :undo -w."""
private = attr.ib()
geometry = attr.ib()
tab_stack = attr.ib()
@ -60,9 +59,11 @@ class WindowUndoManager(QObject):
self._update_undo_stack_size()
def _on_window_closing(self, window):
if window.tabbed_browser.is_private:
return
self._undos.append(_WindowUndoEntry(
geometry=window.saveGeometry(),
private=window.tabbed_browser.is_private,
tab_stack=window.tabbed_browser.undo_stack,
))
@ -79,7 +80,7 @@ class WindowUndoManager(QObject):
"""
entry = self._undos.pop()
window = mainwindow.MainWindow(
private=entry.private,
private=False,
geometry=entry.geometry,
)
window.show()

View File

@ -65,7 +65,9 @@ class ExternalEditor(QObject):
def _cleanup(self):
"""Clean up temporary files after the editor closed."""
assert self._remove_file is not None
if self._watcher is not None and self._watcher.files():
if (self._watcher is not None and
not sip.isdeleted(self._watcher) and
self._watcher.files()):
failed = self._watcher.removePaths(self._watcher.files())
if failed:
log.procs.error("Failed to unwatch paths: {}".format(failed))

View File

@ -124,6 +124,7 @@ def is_single_process() -> bool:
"""Check whether QtWebEngine is running in single-process mode."""
if objects.backend == usertypes.Backend.QtWebKit:
return False
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
args = QApplication.instance().arguments()
return '--single-process' in args

View File

@ -55,7 +55,12 @@ WEBENGINE_SCHEMES = [
]
class InvalidUrlError(Exception):
class Error(Exception):
"""Base class for errors in this module."""
class InvalidUrlError(Error):
"""Error raised if a function got an invalid URL."""
@ -624,3 +629,28 @@ def proxy_from_url(url: QUrl) -> typing.Union[QNetworkProxy, pac.PACFetcher]:
if url.password():
proxy.setPassword(url.password())
return proxy
def parse_javascript_url(url: QUrl) -> str:
"""Get JavaScript source from the given URL.
See https://wiki.whatwg.org/wiki/URL_schemes#javascript:_URLs
and https://github.com/whatwg/url/issues/385
"""
ensure_valid(url)
if url.scheme() != 'javascript':
raise Error("Expected a javascript:... URL")
if url.authority():
raise Error("URL contains unexpected components: {}"
.format(url.authority()))
code = url.path(QUrl.FullyDecoded)
if url.hasQuery():
code += '?' + url.query(QUrl.FullyDecoded)
if url.hasFragment():
code += '#' + url.fragment(QUrl.FullyDecoded)
if not code:
raise Error("Resulted in empty JavaScript code")
return code

View File

@ -337,16 +337,14 @@ def _pdfjs_version() -> str:
else:
pdfjs_file = pdfjs_file.decode('utf-8')
version_re = re.compile(
r"^ *(PDFJS\.version|var pdfjsVersion) = '([^']+)';$",
r"^ *(PDFJS\.version|(var|const) pdfjsVersion) = '(?P<version>[^']+)';$",
re.MULTILINE)
match = version_re.search(pdfjs_file)
if not match:
pdfjs_version = 'unknown'
else:
pdfjs_version = match.group(2)
pdfjs_version = 'unknown' if not match else match.group('version')
if file_path is None:
file_path = 'bundled'
return '{} ({})'.format(pdfjs_version, file_path)
@ -360,6 +358,7 @@ def _chromium_version() -> str:
Qt 5.7: Chromium 49
49.0.2623.111 (2016-03-31)
5.7.0: Security fixes from Chromium 50 and 51
5.7.1: Security fixes up to 54.0.2840.87 (2016-11-01)
Qt 5.8: Chromium 53
@ -368,34 +367,64 @@ def _chromium_version() -> str:
Qt 5.9: Chromium 56
(LTS) 56.0.2924.122 (2017-01-25)
5.9.0: Security fixes up to 56.0.2924.122 (?)
5.9.1: Security fixes up to 59.0.3071.104 (2017-06-15)
5.9.2: Security fixes up to 61.0.3163.79 (2017-09-05)
5.9.3: Security fixes up to 62.0.3202.89 (2017-11-06)
5.9.4: Security fixes up to 63.0.3239.132 (~2017-12-14)
5.9.5: Security fixes up to 65.0.3325.146 (~2018-03-13)
5.9.6: Security fixes up to 66.0.3359.170 (2018-05-10)
5.9.7: Security fixes up to 69.0.3497.113 (~2018-09-11)
5.9.8: Security fixes up to 72.0.3626.121 (2019-03-01)
5.9.9: Security fixes up to 78.0.3904.108 (2019-11-18)
Qt 5.10: Chromium 61
61.0.3163.140 (2017-09-05)
5.10.0: Security fixes up to 62.0.3202.94 (2017-11-13)
5.10.1: Security fixes up to 64.0.3282.140 (2018-02-01)
Qt 5.11: Chromium 65
65.0.3325.151 (.1: .230) (2018-03-06)
65.0.3325.151 (2018-03-06)
5.11.0: Security fixes up to 66.0.3359.139 (2018-04-26)
5.11.1: Updated to 65.0.3325.15.230
Security fixes up to 67.0.3396.87 (2018-06-12)
5.11.2: Security fixes up to 68.0.3440.75 (~2018-07-31)
5.11.3: Security fixes up to 70.0.3538.102 (2018-11-09)
Qt 5.12: Chromium 69
(LTS) 69.0.3497.113 (2018-09-27)
5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03)
(LTS) 69.0.3497.128 (~2018-09-11)
5.12.0: Security fixes up to 70.0.3538.102 (~2018-10-24)
5.12.1: Security fixes up to 71.0.3578.94 (2018-12-12)
5.12.2: Security fixes up to 72.0.3626.121 (2019-03-01)
5.12.3: Security fixes up to 73.0.3683.75 (2019-03-12)
5.12.4: Security fixes up to 74.0.3729.157 (2019-05-14)
5.12.5: Security fixes up to 76.0.3809.87 (2019-07-30)
5.12.6: Security fixes up to 77.0.3865.120 (~2019-09-10)
5.12.7: Security fixes up to 79.0.3945.130 (2020-01-16)
5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18)
5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03)
Qt 5.13: Chromium 73
73.0.3683.105 (~2019-02-28)
5.13.0: Security fixes up to 74.0.3729.157 (2019-05-14)
5.13.1: Security fixes up to 76.0.3809.87 (2019-07-30)
5.13.2: Security fixes up to 77.0.3865.120 (2019-10-10)
Qt 5.14: Chromium 77
77.0.3865.129 (~2019-10-10)
5.14.0: Security fixes up to 77.0.3865.129 (~2019-09-10)
5.14.1: Security fixes up to 79.0.3945.117 (2020-01-07)
5.14.2: Security fixes up to 80.0.3987.132 (2020-03-03)
Qt 5.15: Chromium 80
80.0.3987.163 (2020-04-02)
5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05)
Also see https://www.chromium.org/developers/calendar
and https://chromereleases.googleblog.com/
Also see:
- https://chromiumdash.appspot.com/schedule
- https://www.chromium.org/developers/calendar
- https://chromereleases.googleblog.com/
"""
if webenginesettings is None:
return 'unavailable' # type: ignore[unreachable]
@ -411,10 +440,11 @@ def _backend() -> str:
"""Get the backend line with relevant information."""
if objects.backend == usertypes.Backend.QtWebKit:
return 'new QtWebKit (WebKit {})'.format(qWebKitVersion())
else:
elif objects.backend == usertypes.Backend.QtWebEngine:
webengine = usertypes.Backend.QtWebEngine
assert objects.backend == webengine, objects.backend
return 'QtWebEngine (Chromium {})'.format(_chromium_version())
raise utils.Unreachable(objects.backend)
def _uptime() -> datetime.timedelta:

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
adblock==0.3.0
attrs==19.3.0
attrs==20.1.0
colorama==0.4.3
cssutils==1.0.2
Jinja2==2.11.2

View File

@ -86,7 +86,9 @@ def main():
for wheel in input_files:
utils.print_subtitle(wheel.stem.split('-')[0])
subprocess.run([str(pyqt_bundle),
'--qt-dir', args.qt_location, str(wheel)],
'--qt-dir', args.qt_location,
'--ignore-missing',
str(wheel)],
check=True)
wheel.unlink()

View File

@ -59,170 +59,170 @@ MsgType = enum.Enum('MsgType', 'insufficient_coverage, perfect_file')
# A list of (test_file, tested_file) tuples. test_file can be None.
PERFECT_FILES = [
(None,
'commands/cmdexc.py'),
'qutebrowser/commands/cmdexc.py'),
('tests/unit/commands/test_argparser.py',
'commands/argparser.py'),
'qutebrowser/commands/argparser.py'),
('tests/unit/api/test_cmdutils.py',
'api/cmdutils.py'),
'qutebrowser/api/cmdutils.py'),
(None,
'api/apitypes.py'),
'qutebrowser/api/apitypes.py'),
(None,
'api/config.py'),
'qutebrowser/api/config.py'),
(None,
'api/message.py'),
'qutebrowser/api/message.py'),
(None,
'api/qtutils.py'),
'qutebrowser/api/qtutils.py'),
('tests/unit/browser/webkit/test_cache.py',
'browser/webkit/cache.py'),
'qutebrowser/browser/webkit/cache.py'),
('tests/unit/browser/webkit/test_cookies.py',
'browser/webkit/cookies.py'),
'qutebrowser/browser/webkit/cookies.py'),
('tests/unit/browser/test_history.py',
'browser/history.py'),
'qutebrowser/browser/history.py'),
('tests/unit/browser/test_pdfjs.py',
'browser/pdfjs.py'),
'qutebrowser/browser/pdfjs.py'),
('tests/unit/browser/webkit/http/test_http.py',
'browser/webkit/http.py'),
'qutebrowser/browser/webkit/http.py'),
('tests/unit/browser/webkit/http/test_content_disposition.py',
'browser/webkit/rfc6266.py'),
'qutebrowser/browser/webkit/rfc6266.py'),
# ('tests/unit/browser/webkit/test_webkitelem.py',
# 'browser/webkit/webkitelem.py'),
# 'qutebrowser/browser/webkit/webkitelem.py'),
# ('tests/unit/browser/webkit/test_webkitelem.py',
# 'browser/webelem.py'),
# 'qutebrowser/browser/webelem.py'),
('tests/unit/browser/webkit/network/test_filescheme.py',
'browser/webkit/network/filescheme.py'),
'qutebrowser/browser/webkit/network/filescheme.py'),
('tests/unit/browser/webkit/network/test_networkreply.py',
'browser/webkit/network/networkreply.py'),
'qutebrowser/browser/webkit/network/networkreply.py'),
('tests/unit/browser/test_signalfilter.py',
'browser/signalfilter.py'),
'qutebrowser/browser/signalfilter.py'),
(None,
'browser/webengine/certificateerror.py'),
'qutebrowser/browser/webengine/certificateerror.py'),
# ('tests/unit/browser/test_tab.py',
# 'browser/tab.py'),
# 'qutebrowser/browser/tab.py'),
('tests/unit/keyinput/test_basekeyparser.py',
'keyinput/basekeyparser.py'),
'qutebrowser/keyinput/basekeyparser.py'),
('tests/unit/keyinput/test_keyutils.py',
'keyinput/keyutils.py'),
'qutebrowser/keyinput/keyutils.py'),
('tests/unit/components/test_readlinecommands.py',
'components/readlinecommands.py'),
'qutebrowser/components/readlinecommands.py'),
('tests/unit/misc/test_autoupdate.py',
'misc/autoupdate.py'),
'qutebrowser/misc/autoupdate.py'),
('tests/unit/misc/test_split.py',
'misc/split.py'),
'qutebrowser/misc/split.py'),
('tests/unit/misc/test_msgbox.py',
'misc/msgbox.py'),
'qutebrowser/misc/msgbox.py'),
('tests/unit/misc/test_checkpyver.py',
'misc/checkpyver.py'),
'qutebrowser/misc/checkpyver.py'),
('tests/unit/misc/test_guiprocess.py',
'misc/guiprocess.py'),
'qutebrowser/misc/guiprocess.py'),
('tests/unit/misc/test_editor.py',
'misc/editor.py'),
'qutebrowser/misc/editor.py'),
('tests/unit/misc/test_cmdhistory.py',
'misc/cmdhistory.py'),
'qutebrowser/misc/cmdhistory.py'),
('tests/unit/misc/test_ipc.py',
'misc/ipc.py'),
'qutebrowser/misc/ipc.py'),
('tests/unit/misc/test_keyhints.py',
'misc/keyhintwidget.py'),
'qutebrowser/misc/keyhintwidget.py'),
('tests/unit/misc/test_pastebin.py',
'misc/pastebin.py'),
'qutebrowser/misc/pastebin.py'),
('tests/unit/misc/test_objects.py',
'misc/objects.py'),
'qutebrowser/misc/objects.py'),
('tests/unit/misc/test_throttle.py',
'misc/throttle.py'),
'qutebrowser/misc/throttle.py'),
(None,
'mainwindow/statusbar/keystring.py'),
'qutebrowser/mainwindow/statusbar/keystring.py'),
('tests/unit/mainwindow/statusbar/test_percentage.py',
'mainwindow/statusbar/percentage.py'),
'qutebrowser/mainwindow/statusbar/percentage.py'),
('tests/unit/mainwindow/statusbar/test_progress.py',
'mainwindow/statusbar/progress.py'),
'qutebrowser/mainwindow/statusbar/progress.py'),
('tests/unit/mainwindow/statusbar/test_tabindex.py',
'mainwindow/statusbar/tabindex.py'),
'qutebrowser/mainwindow/statusbar/tabindex.py'),
('tests/unit/mainwindow/statusbar/test_textbase.py',
'mainwindow/statusbar/textbase.py'),
'qutebrowser/mainwindow/statusbar/textbase.py'),
('tests/unit/mainwindow/statusbar/test_url.py',
'mainwindow/statusbar/url.py'),
'qutebrowser/mainwindow/statusbar/url.py'),
('tests/unit/mainwindow/statusbar/test_backforward.py',
'mainwindow/statusbar/backforward.py'),
'qutebrowser/mainwindow/statusbar/backforward.py'),
('tests/unit/mainwindow/test_messageview.py',
'mainwindow/messageview.py'),
'qutebrowser/mainwindow/messageview.py'),
('tests/unit/config/test_config.py',
'config/config.py'),
'qutebrowser/config/config.py'),
('tests/unit/config/test_stylesheet.py',
'config/stylesheet.py'),
'qutebrowser/config/stylesheet.py'),
('tests/unit/config/test_configdata.py',
'config/configdata.py'),
'qutebrowser/config/configdata.py'),
('tests/unit/config/test_configexc.py',
'config/configexc.py'),
'qutebrowser/config/configexc.py'),
('tests/unit/config/test_configfiles.py',
'config/configfiles.py'),
'qutebrowser/config/configfiles.py'),
('tests/unit/config/test_configtypes.py',
'config/configtypes.py'),
'qutebrowser/config/configtypes.py'),
('tests/unit/config/test_configinit.py',
'config/configinit.py'),
'qutebrowser/config/configinit.py'),
('tests/unit/config/test_qtargs.py',
'config/qtargs.py'),
'qutebrowser/config/qtargs.py'),
('tests/unit/config/test_configcommands.py',
'config/configcommands.py'),
'qutebrowser/config/configcommands.py'),
('tests/unit/config/test_configutils.py',
'config/configutils.py'),
'qutebrowser/config/configutils.py'),
('tests/unit/config/test_configcache.py',
'config/configcache.py'),
'qutebrowser/config/configcache.py'),
('tests/unit/utils/test_qtutils.py',
'utils/qtutils.py'),
'qutebrowser/utils/qtutils.py'),
('tests/unit/utils/test_standarddir.py',
'utils/standarddir.py'),
'qutebrowser/utils/standarddir.py'),
('tests/unit/utils/test_urlutils.py',
'utils/urlutils.py'),
'qutebrowser/utils/urlutils.py'),
('tests/unit/utils/usertypes',
'utils/usertypes.py'),
'qutebrowser/utils/usertypes.py'),
('tests/unit/utils/test_utils.py',
'utils/utils.py'),
'qutebrowser/utils/utils.py'),
('tests/unit/utils/test_version.py',
'utils/version.py'),
'qutebrowser/utils/version.py'),
('tests/unit/utils/test_debug.py',
'utils/debug.py'),
'qutebrowser/utils/debug.py'),
('tests/unit/utils/test_jinja.py',
'utils/jinja.py'),
'qutebrowser/utils/jinja.py'),
('tests/unit/utils/test_error.py',
'utils/error.py'),
'qutebrowser/utils/error.py'),
('tests/unit/utils/test_javascript.py',
'utils/javascript.py'),
'qutebrowser/utils/javascript.py'),
('tests/unit/utils/test_urlmatch.py',
'utils/urlmatch.py'),
'qutebrowser/utils/urlmatch.py'),
(None,
'completion/models/util.py'),
'qutebrowser/completion/models/util.py'),
('tests/unit/completion/test_models.py',
'completion/models/urlmodel.py'),
'qutebrowser/completion/models/urlmodel.py'),
('tests/unit/completion/test_models.py',
'completion/models/configmodel.py'),
'qutebrowser/completion/models/configmodel.py'),
('tests/unit/completion/test_histcategory.py',
'completion/models/histcategory.py'),
'qutebrowser/completion/models/histcategory.py'),
('tests/unit/completion/test_listcategory.py',
'completion/models/listcategory.py'),
'qutebrowser/completion/models/listcategory.py'),
('tests/unit/browser/webengine/test_spell.py',
'browser/webengine/spell.py'),
'qutebrowser/browser/webengine/spell.py'),
('tests/unit/browser/webengine/test_webengine_cookies.py',
'browser/webengine/cookies.py'),
'qutebrowser/browser/webengine/cookies.py'),
]
# 100% coverage because of end2end tests, but no perfect unit tests yet.
WHITELISTED_FILES = [
'browser/webkit/webkitinspector.py',
'misc/debugcachestats.py',
'keyinput/macros.py',
'browser/webkit/webkitelem.py',
'api/interceptor.py',
'qutebrowser/browser/webkit/webkitinspector.py',
'qutebrowser/misc/debugcachestats.py',
'qutebrowser/keyinput/macros.py',
'qutebrowser/browser/webkit/webkitelem.py',
'qutebrowser/api/interceptor.py',
]
@ -243,8 +243,6 @@ def _get_filename(filename):
common_path = os.path.commonprefix([basedir, filename])
if common_path:
filename = filename[len(common_path):].lstrip('/')
if filename.startswith('qutebrowser/'):
filename = filename.split('/', maxsplit=1)[1]
return filename
@ -295,8 +293,10 @@ def check(fileobj, perfect_files):
filename, line_cov, branch_cov)
messages.append(Message(MsgType.insufficient_coverage, filename,
text))
elif (filename not in perfect_src_files and not is_bad and
filename not in WHITELISTED_FILES):
elif (filename not in perfect_src_files and
not is_bad and
filename not in WHITELISTED_FILES and
not filename.startswith('tests/')):
text = ("{} has 100% coverage but is not in "
"perfect_files!".format(filename))
messages.append(Message(MsgType.perfect_file, filename, text))
@ -320,7 +320,7 @@ def main_check():
for msg in messages:
msg.show()
print()
filters = ','.join('qutebrowser/' + msg.filename for msg in messages)
filters = ','.join(msg.filename for msg in messages)
subprocess.run([sys.executable, '-m', 'coverage', 'report',
'--show-missing', '--include', filters], check=True)
print()

View File

@ -162,7 +162,7 @@ def check_userscripts_descriptions():
described.add(match.group(1))
present = {path.name for path in folder.iterdir()}
present.remove('README.md')
present -= {'README.md', '.mypy_cache', '__pycache__'}
missing = present - described
additional = described - present

View File

@ -42,7 +42,7 @@ CHANGELOG_URLS = {
'cherrypy': 'https://github.com/cherrypy/cherrypy/blob/master/CHANGES.rst',
'pylint': 'http://pylint.pycqa.org/en/latest/whatsnew/changelog.html',
'setuptools': 'https://github.com/pypa/setuptools/blob/master/CHANGES.rst',
'pytest-cov': 'https://github.com/pytest-dev/pytest-cov',
'pytest-cov': 'https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst',
'requests': 'https://github.com/psf/requests/blob/master/HISTORY.md',
'requests-file': 'https://github.com/dashea/requests-file/blob/master/CHANGES.rst',
'werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst',
@ -110,6 +110,7 @@ CHANGELOG_URLS = {
'chardet': 'https://github.com/chardet/chardet/releases',
'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst',
'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md',
'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
}
# PyQt versions which need SIP v4

View File

@ -429,7 +429,7 @@ def _generate_setting_option(f, opt):
f.write("=== {}".format(opt.name) + "\n")
f.write(opt.description + "\n")
if opt.restart:
f.write("This setting requires a restart.\n")
f.write("\nThis setting requires a restart.\n")
if opt.supports_pattern:
f.write("\nThis setting supports URL patterns.\n")
if opt.no_autoconfig:

View File

@ -98,6 +98,7 @@ try:
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers',

View File

@ -28,9 +28,10 @@ import sys
import shutil
import pstats
import operator
import pathlib
import pytest
from PyQt5.QtCore import PYQT_VERSION
from PyQt5.QtCore import PYQT_VERSION, QCoreApplication
pytest.register_assert_rewrite('end2end.fixtures')
@ -142,6 +143,9 @@ def pytest_collection_modifyitems(config, items):
header_bug_fixed = (not qtutils.version_check('5.12', compiled=False) or
qtutils.version_check('5.15', compiled=False))
lib_path = pathlib.Path(QCoreApplication.libraryPaths()[0])
qpdf_image_plugin = lib_path / 'imageformats' / 'libqpdf.so'
markers = [
('qtwebengine_todo', 'QtWebEngine TODO', pytest.mark.xfail,
config.webengine),
@ -160,6 +164,10 @@ def pytest_collection_modifyitems(config, items):
('js_headers', 'Sets headers dynamically via JS',
pytest.mark.skipif,
config.webengine and not header_bug_fixed),
('qtwebkit_pdf_imageformat_skip',
'Skipped with QtWebKit if PDF image plugin is available',
pytest.mark.skipif,
not config.webengine and qpdf_image_plugin.exists()),
]
for item in items:

View File

@ -89,6 +89,10 @@ Feature: Various utility commands.
When I run :jseval Array(5002).join("x")
Then the message "x* [...trimmed...]" should be shown
Scenario: :jseval --url
When I run :jseval --url javascript:console.log("hello world?")
Then the javascript message "hello world?" should be logged
@qtwebengine_skip
Scenario: :jseval with --world on QtWebKit
When I run :jseval --world=1 console.log("Hello from JS!");

View File

@ -4,25 +4,10 @@ Feature: Using :navigate
Scenario: :navigate with invalid argument
When I run :navigate foo
Then the error "where: Invalid value foo - expected one of: prev, next, up, increment, decrement" should be shown
Then the error "where: Invalid value foo - expected one of: prev, next, up, increment, decrement, strip" should be shown
# up
Scenario: Navigating up
When I open data/navigate/sub
And I run :navigate up
Then data/navigate should be loaded
Scenario: Navigating up with a query
When I open data/navigate/sub?foo=bar
And I run :navigate up
Then data/navigate should be loaded
Scenario: Navigating up by count
When I open data/navigate/sub/index.html
And I run :navigate up with count 2
Then data/navigate should be loaded
Scenario: Navigating up in qute://help/
When the documentation is up to date
And I open qute://help/commands.html
@ -90,48 +75,6 @@ Feature: Using :navigate
# increment/decrement
Scenario: Incrementing number in URL
When I open data/numbers/1.txt
And I run :navigate increment
Then data/numbers/2.txt should be loaded
Scenario: Decrementing number in URL
When I open data/numbers/4.txt
And I run :navigate decrement
Then data/numbers/3.txt should be loaded
Scenario: Decrementing with no number in URL
When I open data/navigate
And I run :navigate decrement
Then the error "No number found in URL!" should be shown
Scenario: Incrementing with no number in URL
When I open data/navigate
And I run :navigate increment
Then the error "No number found in URL!" should be shown
Scenario: Incrementing number in URL by count
When I open data/numbers/3.txt
And I run :navigate increment with count 3
Then data/numbers/6.txt should be loaded
Scenario: Decrementing number in URL by count
When I open data/numbers/8.txt
And I run :navigate decrement with count 5
Then data/numbers/3.txt should be loaded
Scenario: Setting url.incdec_segments
When I set url.incdec_segments to [anchor]
And I open data/numbers/1.txt
And I run :navigate increment
Then the error "No number found in URL!" should be shown
Scenario: Incrementing query
When I set url.incdec_segments to ["query"]
And I open data/numbers/1.txt?value=2
And I run :navigate increment
Then data/numbers/1.txt?value=3 should be loaded
@qtwebengine_todo: Doesn't find any elements
Scenario: Navigating multiline links
When I open data/navigate/multilinelinks.html

View File

@ -42,6 +42,25 @@ Feature: Using private browsing
## https://github.com/qutebrowser/qutebrowser/issues/1219
Scenario: Make sure private data is cleared when closing last private window
When I open about:blank in a private window
And I open cookies/set?cookie-to-delete=1 without waiting in a new tab
And I wait until cookies is loaded
And I run :close
And I open about:blank in a private window
And I open cookies
Then the cookie cookie-to-delete should not be set
Scenario: Make sure private data is not cleared when closing a private window but another remains
When I open about:blank in a private window
And I open about:blank in a private window
And I open cookies/set?cookie-to-preserve=1 without waiting in a new tab
And I wait until cookies is loaded
And I run :close
And I open about:blank in a private window
And I open cookies
Then the cookie cookie-to-preserve should be set to 1
Scenario: Sharing cookies with private browsing
When I open cookies/set?qute-test=42 without waiting in a private window
And I wait until cookies is loaded

View File

@ -177,6 +177,7 @@ Feature: Special qute:// pages
And I open data/misc/test.pdf without waiting
Then the javascript message "PDF * [*] (PDF.js: *)" should be logged
@qtwebkit_pdf_imageformat_skip
Scenario: pdfjs is not used when disabled
When I set content.pdfjs to false
And I set downloads.location.prompt to false

View File

@ -106,6 +106,7 @@ Feature: Scrolling
When I run :scroll bottom
Then the page should be scrolled vertically
@flaky
Scenario: Scrolling to bottom and to top
When I run :scroll bottom
And I wait until the scroll position changed
@ -219,6 +220,7 @@ Feature: Scrolling
When I run :scroll-to-perc --horizontal
Then the page should be scrolled horizontally
@flaky
Scenario: :scroll-to-perc with count
When I run :scroll-to-perc with count 50
Then the page should be scrolled vertically

View File

@ -114,6 +114,12 @@ def is_ignored_lowlevel_message(message):
'*/QtWebEngineProcess: /lib/x86_64-linux-gnu/libdbus-1.so.3: no '
'version information available (required by '
'*/libQt5WebEngineCore.so.5)',
# hunter and Python 3.9
# https://github.com/ionelmc/python-hunter/issues/87
'<frozen importlib._bootstrap>:*: RuntimeWarning: builtins.type size changed, '
'may indicate binary incompatibility. Expected 872 from C header, got 880 from '
'PyObject',
]
return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages)

View File

@ -58,6 +58,7 @@ def test_insert_mode(file_name, elem_id, source, input_text, zoom,
(True, False, True), # enabled and foreground tab
(True, True, False), # background tab
])
@pytest.mark.flaky
def test_auto_load(quteproc, auto_load, background, insert_mode):
quteproc.set_setting('input.insert_mode.auto_load', str(auto_load))
url_path = 'data/insert_mode_settings/html/autofocus.html'

View File

@ -215,6 +215,7 @@ def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager,
tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
private=False)
tab.backend = usertypes.Backend.QtWebKit
widget_container.set_widget(tab)
yield tab
@ -238,6 +239,7 @@ def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data,
tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager,
private=False)
tab.backend = usertypes.Backend.QtWebEngine
widget_container.set_widget(tab)
yield tab

View File

@ -22,10 +22,14 @@ import pytest
from qutebrowser.browser import downloads, qtnetworkdownloads
def test_download_model(qapp, qtmodeltester, config_stub, cookiejar_and_cache,
fake_args):
@pytest.fixture
def manager(config_stub, cookiejar_and_cache):
"""A QtNetwork download manager."""
return qtnetworkdownloads.DownloadManager()
def test_download_model(qapp, qtmodeltester, manager):
"""Simple check for download model internals."""
manager = qtnetworkdownloads.DownloadManager()
model = downloads.DownloadModel(manager)
qtmodeltester.check(model)
@ -107,7 +111,7 @@ def test_sanitized_filenames(raw, expected,
config_stub, download_tmpdir, monkeypatch):
manager = downloads.AbstractDownloadManager()
target = downloads.FileDownloadTarget(str(download_tmpdir))
item = downloads.AbstractDownloadItem()
item = downloads.AbstractDownloadItem(manager=manager)
# Don't try to start a timer outside of a QThread
manager._update_timer.isActive = lambda: True
@ -116,6 +120,58 @@ def test_sanitized_filenames(raw, expected,
item._ensure_can_set_filename = lambda *args: True
item._after_set_filename = lambda *args: True
# Don't try to get current window
monkeypatch.setattr(item, '_get_conflicting_download', list)
manager._init_item(item, True, raw)
item.set_target(target)
assert item._filename.endswith(expected)
class TestConflictingDownloads:
@pytest.fixture
def item1(self, manager):
return downloads.AbstractDownloadItem(manager=manager)
@pytest.fixture
def item2(self, manager):
return downloads.AbstractDownloadItem(manager=manager)
def test_no_downloads(self, item1):
item1._filename = 'download.txt'
assert item1._get_conflicting_download() is None
@pytest.mark.parametrize('filename1, filename2, done, conflict', [
# Different name
('download.txt', 'download2.txt', False, False),
# Finished
('download.txt', 'download.txt', True, False),
# Conflict
('download.txt', 'download.txt', False, True),
])
def test_conflicts(self, manager, item1, item2,
filename1, filename2, done, conflict):
item1._filename = filename1
item2._filename = filename2
item2.done = done
manager.downloads.append(item1)
manager.downloads.append(item2)
expected = item2 if conflict else None
assert item1._get_conflicting_download() is expected
def test_cancel_conflicting_downloads(self, manager, item1, item2, monkeypatch):
item1._filename = 'download.txt'
item2._filename = 'download.txt'
item2.done = False
manager.downloads.append(item1)
manager.downloads.append(item2)
def patched_cancel(remove_data=True):
assert not remove_data
item2.done = True
monkeypatch.setattr(item2, 'cancel', patched_cancel)
monkeypatch.setattr(item1, '_after_set_filename', lambda: None)
item1._cancel_conflicting_download()
assert item2.done

View File

@ -172,10 +172,55 @@ class TestIncDec:
def test_invalid_url(self):
with pytest.raises(urlutils.InvalidUrlError):
navigate.incdec(QUrl(""), 1, "increment")
navigate.incdec(QUrl(), 1, "increment")
def test_wrong_mode(self):
"""Test if incdec rejects a wrong parameter for inc_or_dec."""
valid_url = QUrl("http://example.com/0")
with pytest.raises(ValueError):
navigate.incdec(valid_url, 1, "foobar")
class TestUp:
@pytest.mark.parametrize('url_suffix, count, expected_suffix', [
('/one/two/three', 1, '/one/two'),
('/one/two/three?foo=bar', 1, '/one/two'),
('/one/two/three', 2, '/one'),
])
def test_up(self, url_suffix, count, expected_suffix):
url_base = 'https://example.com'
url = QUrl(url_base + url_suffix)
assert url.isValid()
new = navigate.path_up(url, count)
assert new == QUrl(url_base + expected_suffix)
def test_invalid_url(self):
with pytest.raises(urlutils.InvalidUrlError):
navigate.path_up(QUrl(), count=1)
class TestStrip:
@pytest.mark.parametrize('url_suffix', [
'?foo=bar',
'#label',
'?foo=bar#label',
])
def test_strip(self, url_suffix):
url_base = 'https://example.com/test'
url = QUrl(url_base + url_suffix)
assert url.isValid()
stripped = navigate.strip(url, count=1)
assert stripped.isValid()
assert stripped == QUrl(url_base)
def test_count(self):
with pytest.raises(navigate.Error, match='Count is not supported'):
navigate.strip(QUrl('https://example.com/'), count=2)
def test_invalid_url(self):
with pytest.raises(urlutils.InvalidUrlError):
navigate.strip(QUrl(), count=1)

View File

@ -87,7 +87,7 @@ def _pac_noexcept_test(call):
_pac_common_test(test_str_f.format(call))
# pylint: disable=line-too-long, invalid-name
# pylint: disable=invalid-name
@pytest.mark.parametrize("domain, expected", [

View File

@ -743,8 +743,8 @@ class TestGetChildFrames:
def test_one_level(self, stubs):
r"""Test get_child_frames with one level of children.
o parent
/ \
o parent
/ \ ------
child1 o o child2
"""
child1 = stubs.FakeChildrenFrame()
@ -763,9 +763,9 @@ class TestGetChildFrames:
r"""Test get_child_frames with multiple levels of children.
o root
/ \
/ \ ------
o o first
/\ /\
/\ /\ ------
o o o o second
"""
second = [stubs.FakeChildrenFrame() for _ in range(4)]

View File

@ -34,7 +34,7 @@ from qutebrowser.completion import completiondelegate
('foo', 'barfoobaz', [(3, 3)]),
('foo', 'barfoobazfoo', [(3, 3), (9, 3)]),
('foo', 'foofoo', [(0, 3), (3, 3)]),
('a|b', 'cadb', [(1, 1), (3, 1)]),
('a b', 'cadb', [(1, 1), (3, 1)]),
('foo', '<foo>', [(1, 3)]),
('<a>', "<a>bc", [(0, 3)]),
@ -42,6 +42,10 @@ from qutebrowser.completion import completiondelegate
('foo', "'foo'", [(1, 3)]),
('x', "'x'", [(1, 1)]),
('lt', "<lt", [(1, 2)]),
# See https://github.com/qutebrowser/qutebrowser/pull/5111
('bar', '\U0001d65b\U0001d664\U0001d664bar', [(6, 3)]),
('an anomaly', 'an anomaly', [(0, 2), (3, 7)]),
])
def test_highlight(pat, txt, segments):
doc = QTextDocument(txt)
@ -53,6 +57,18 @@ def test_highlight(pat, txt, segments):
])
def test_benchmark_highlight(benchmark):
txt = 'boofoobar'
pat = 'foo bar'
doc = QTextDocument(txt)
def bench():
highlighter = completiondelegate._Highlighter(doc, pat, Qt.red)
highlighter.highlightBlock(txt)
benchmark(bench)
def test_highlighted(qtbot):
"""Make sure highlighting works.

View File

@ -22,6 +22,7 @@
from unittest import mock
import pytest
from PyQt5.QtCore import QRect
from qutebrowser.completion import completionwidget
from qutebrowser.completion.models import completionmodel, listcategory
@ -42,9 +43,13 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
return view
def test_set_model(completionview):
@pytest.fixture()
def model():
return completionmodel.CompletionModel()
def test_set_model(completionview, model):
"""Ensure set_model actually sets the model and expands all categories."""
model = completionmodel.CompletionModel()
for _i in range(3):
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
@ -53,8 +58,7 @@ def test_set_model(completionview):
assert completionview.isExpanded(model.index(i, 0))
def test_set_pattern(completionview):
model = completionmodel.CompletionModel()
def test_set_pattern(completionview, model):
model.set_pattern = mock.Mock(spec=[])
completionview.set_model(model)
completionview.set_pattern('foo')
@ -116,7 +120,7 @@ def test_maybe_update_geometry(completionview, config_stub, qtbot):
('next-category', [[]], [None, None]),
('prev-category', [[]], [None, None]),
])
def test_completion_item_focus(which, tree, expected, completionview, qtbot):
def test_completion_item_focus(which, tree, expected, completionview, model, qtbot):
"""Test that on_next_prev_item moves the selection properly.
Args:
@ -127,7 +131,6 @@ def test_completion_item_focus(which, tree, expected, completionview, qtbot):
successive movement. None implies no signal should be
emitted.
"""
model = completionmodel.CompletionModel()
for catdata in tree:
cat = listcategory.ListCategory('', ((x,) for x in catdata))
model.add_category(cat)
@ -142,23 +145,23 @@ def test_completion_item_focus(which, tree, expected, completionview, qtbot):
assert sig.args == [entry]
@pytest.mark.parametrize('which', ['next', 'prev', 'next-category',
'prev-category'])
def test_completion_item_focus_no_model(which, completionview, qtbot):
@pytest.mark.parametrize('which', ['next', 'prev',
'next-category', 'prev-category',
'next-page', 'prev-page'])
def test_completion_item_focus_no_model(which, completionview, model, qtbot):
"""Test that selectionChanged is not fired when the model is None.
Validates #1812: help completion repeatedly completes
"""
with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which)
model = completionmodel.CompletionModel()
completionview.set_model(model)
completionview.set_model(None)
with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which)
def test_completion_item_focus_fetch(completionview, qtbot):
def test_completion_item_focus_fetch(completionview, model, qtbot):
"""Test that on_next_prev_item moves the selection properly.
Args:
@ -169,7 +172,6 @@ def test_completion_item_focus_fetch(completionview, qtbot):
successive movement. None implies no signal should be
emitted.
"""
model = completionmodel.CompletionModel()
cat = mock.Mock(spec=[
'layoutChanged', 'layoutAboutToBeChanged', 'canFetchMore',
'fetchMore', 'rowCount', 'index', 'data'])
@ -190,10 +192,95 @@ def test_completion_item_focus_fetch(completionview, qtbot):
assert cat.fetchMore.called
class TestCompletionItemFocusPage:
"""Test :completion-item-focus with prev-page/next-page."""
@pytest.fixture(autouse=True)
def patch_heights(self, monkeypatch, completionview):
"""Patch the item/widget heights so that 10 items are always visible."""
monkeypatch.setattr(completionview, 'visualRect',
lambda _idx: QRect(0, 0, 100, 20))
monkeypatch.setattr(completionview, 'height', lambda: 200)
@pytest.mark.parametrize('which, expected', [
('prev-page', 'Last Item'),
('next-page', 'First Item'),
])
def test_no_selection(self, qtbot, completionview, model, which, expected):
"""With no selection, the first/last item should be selected."""
items = [("First Item",), ("Middle Item",), ("Last Item",)]
cat = listcategory.ListCategory('Test', items)
model.add_category(cat)
completionview.set_model(model)
with qtbot.waitSignal(completionview.selection_changed) as blocker:
completionview.completion_item_focus(which)
assert blocker.args == [expected]
@pytest.mark.parametrize('steps', [
# Select first item and go down
[('next', 'Item 1'), ('next-page', 'Item 10')],
# Go down twice
[('next', 'Item 1'), ('next-page', 'Item 10'), ('next-page', 'Item 19')],
# Last item via Page Down
[('next', 'Item 1'),
('next-page', 'Item 10'),
('next-page', 'Item 19'),
('next-page', 'Item 24')],
# Wrapping around via Page Down
[('next', 'Item 1'),
('next-page', 'Item 10'),
('next-page', 'Item 19'),
('next-page', 'Item 24'),
('next-page', 'Item 1')],
# Select last item and go up
[('prev', 'Item 24'), ('prev-page', 'Item 15')],
# Go up twice
[('prev', 'Item 24'), ('prev-page', 'Item 15'), ('prev-page', 'Item 6')],
# Last item via Page Up
[('prev', 'Item 24'),
('prev-page', 'Item 15'),
('prev-page', 'Item 6'),
('prev-page', 'Item 1')],
# Wrapping around via Page Up
[('prev', 'Item 24'),
('prev-page', 'Item 15'),
('prev-page', 'Item 6'),
('prev-page', 'Item 1'),
('prev-page', 'Item 24')],
])
def test_steps(self, completionview, qtbot, model, steps):
items = [("Item {}".format(i),) for i in range(1, 25)]
cat = listcategory.ListCategory('Test', items)
model.add_category(cat)
completionview.set_model(model)
for move, item in steps:
print('{:9} -> expecting {}'.format(move, item))
with qtbot.waitSignal(completionview.selection_changed) as blocker:
completionview.completion_item_focus(move)
assert blocker.args == [item]
def test_category_headers(self, completionview, qtbot, model):
for name, items in [
("First", [("Item {}".format(i),) for i in range(1, 9)]),
("Second", []),
("Third", [("Target item",)])]:
cat = listcategory.ListCategory(name, items)
model.add_category(cat)
completionview.set_model(model)
for move, item in [('next', 'Item 1'), ('next-page', 'Target item')]:
with qtbot.waitSignal(completionview.selection_changed) as blocker:
completionview.completion_item_focus(move)
assert blocker.args == [item]
@pytest.mark.parametrize('show', ['always', 'auto', 'never'])
@pytest.mark.parametrize('rows', [[], ['Aa'], ['Aa', 'Bb']])
@pytest.mark.parametrize('quick_complete', [True, False])
def test_completion_show(show, rows, quick_complete, completionview,
def test_completion_show(show, rows, quick_complete, completionview, model,
config_stub):
"""Test that the completion widget is shown at appropriate times.
@ -205,7 +292,6 @@ def test_completion_show(show, rows, quick_complete, completionview,
config_stub.val.completion.show = show
config_stub.val.completion.quick = quick_complete
model = completionmodel.CompletionModel()
for name in rows:
cat = listcategory.ListCategory('', [(name,)])
model.add_category(cat)
@ -222,10 +308,9 @@ def test_completion_show(show, rows, quick_complete, completionview,
assert not completionview.isVisible()
def test_completion_item_del(completionview):
def test_completion_item_del(completionview, model):
"""Test that completion_item_del invokes delete_cur_item in the model."""
func = mock.Mock(spec=[])
model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo', 'bar')], delete_func=func)
model.add_category(cat)
completionview.set_model(model)
@ -234,10 +319,9 @@ def test_completion_item_del(completionview):
func.assert_called_once_with(['foo', 'bar'])
def test_completion_item_del_no_selection(completionview):
def test_completion_item_del_no_selection(completionview, model):
"""Test that completion_item_del with an invalid index."""
func = mock.Mock(spec=[])
model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo',)], delete_func=func)
model.add_category(cat)
completionview.set_model(model)
@ -247,12 +331,11 @@ def test_completion_item_del_no_selection(completionview):
@pytest.mark.parametrize('sel', [True, False])
def test_completion_item_yank(completionview, mocker, sel):
def test_completion_item_yank(completionview, model, mocker, sel):
"""Test that completion_item_yank invokes delete_cur_item in the model."""
m = mocker.patch(
'qutebrowser.completion.completionwidget.utils',
autospec=True)
model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo', 'bar')])
model.add_category(cat)
@ -264,13 +347,12 @@ def test_completion_item_yank(completionview, mocker, sel):
@pytest.mark.parametrize('sel', [True, False])
def test_completion_item_yank_selected(completionview, status_command_stub,
mocker, sel):
def test_completion_item_yank_selected(completionview, model,
status_command_stub, mocker, sel):
"""Test that completion_item_yank yanks selected text."""
m = mocker.patch(
'qutebrowser.completion.completionwidget.utils',
autospec=True)
model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo', 'bar')])
model.add_category(cat)

View File

@ -63,28 +63,6 @@ class Font(QFont):
return utils.get_repr(self, **kwargs)
@classmethod
def fromdesc(cls, desc):
"""Get a Font based on a font description."""
f = cls()
f.setStyle(desc.style)
f.setWeight(desc.weight)
if desc.pt is not None and desc.pt != -1:
f.setPointSize(desc.pt)
if desc.px is not None and desc.pt != -1:
f.setPixelSize(desc.px)
f.setFamily(desc.family)
try:
f.setFamilies([desc.family])
except AttributeError:
# Added in Qt 5.13
pass
return f
class RegexEq:
@ -1434,10 +1412,6 @@ class TestFont:
def klass(self):
return configtypes.Font
@pytest.fixture
def font_class(self):
return configtypes.Font
@pytest.mark.parametrize('val, desc', sorted(TESTS.items()))
def test_to_py_valid(self, klass, val, desc):
assert klass().to_py(val) == val
@ -1743,10 +1717,6 @@ class TestFile:
def klass(self, request):
return request.param
@pytest.fixture
def file_class(self):
return configtypes.File
def test_to_py_does_not_exist_file(self, os_mock):
"""Test to_py with a file which does not exist (File)."""
os_mock.path.isfile.return_value = False

View File

@ -59,6 +59,26 @@ def test_size_hint(view):
assert height2 == height1 * 2
def test_word_wrap(view, qtbot):
"""A long message should be wrapped."""
with qtbot.waitSignal(view._clear_timer.timeout):
view.show_message(usertypes.MessageLevel.info, 'short')
height1 = view.sizeHint().height()
assert height1 > 0
text = ("Athene, the bright-eyed goddess, answered him at once: Father of "
"us all, Son of Cronos, Highest King, clearly that man deserved to be "
"destroyed: so let all be destroyed who act as he did. But my heart aches "
"for Odysseus, wise but ill fated, who suffers far from his friends on an "
"island deep in the sea.")
view.show_message(usertypes.MessageLevel.info, text)
height2 = view.sizeHint().height()
assert height2 > height1
assert view._messages[0].wordWrap()
def test_show_message_twice(view):
"""Show the same message twice -> only one should be shown."""
view.show_message(usertypes.MessageLevel.info, 'test')

View File

@ -18,10 +18,9 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import logging
from unittest import mock
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtWidgets import QWidget
import pytest
from qutebrowser.misc import miscwidgets
@ -41,19 +40,6 @@ class TestCommandLineEdit:
assert cmd_edit.text() == ''
yield cmd_edit
@pytest.fixture
def mock_clipboard(self, mocker):
"""Fixture to mock QApplication.clipboard.
Return:
The mocked QClipboard object.
"""
mocker.patch.object(QApplication, 'clipboard')
clipboard = mock.MagicMock()
clipboard.supportsSelection.return_value = True
QApplication.clipboard.return_value = clipboard
return clipboard
def test_position(self, qtbot, cmd_edit):
"""Test cursor position based on the prompt."""
qtbot.keyClicks(cmd_edit, ':hello')

View File

@ -84,7 +84,6 @@ class TestQuteLastPassComponents:
"""Test if fake_key_raw properly escapes characters."""
qute_lastpass.fake_key_raw('john.doe@example.com ')
# pylint: disable=line-too-long
qutecommand_mock.assert_called_once_with(
'fake-key \\j\\o\\h\\n\\.\\d\\o\\e\\@\\e\\x\\a\\m\\p\\l\\e\\.\\c\\o\\m" "'
)
@ -258,7 +257,6 @@ class TestQuteLastPassMain:
assert exit_code == qute_lastpass.ExitCodes.SUCCESS
# pylint: disable=line-too-long
subprocess_mock.assert_has_calls([
call(['lpass', 'show', '-x', '-j', '-G', '\\bwww\\.example\\.com'],
stdout=ANY, stderr=ANY),
@ -325,7 +323,6 @@ class TestQuteLastPassMain:
assert exit_code == qute_lastpass.ExitCodes.SUCCESS
# pylint: disable=line-too-long
subprocess_mock.assert_has_calls([
call(['lpass', 'show', '-x', '-j', '-G', '\\bwww\\.example\\.com'],
stdout=ANY, stderr=ANY),

View File

@ -227,11 +227,11 @@ def test_skipped_non_linux(covtest):
def _generate_files():
"""Get filenames from WHITELISTED_/PERFECT_FILES."""
for src_file in check_coverage.WHITELISTED_FILES:
yield pathlib.Path('qutebrowser') / src_file
yield pathlib.Path(src_file)
for test_file, src_file in check_coverage.PERFECT_FILES:
if test_file is not None:
yield pathlib.Path(test_file)
yield pathlib.Path('qutebrowser') / src_file
yield pathlib.Path(src_file)
@pytest.mark.parametrize('filename', list(_generate_files()))

View File

@ -22,57 +22,76 @@
import pytest
import hypothesis
import hypothesis.strategies
import attr
from qutebrowser.utils import javascript
from qutebrowser.utils import javascript, usertypes
@attr.s
class Case:
original = attr.ib()
replacement = attr.ib()
webkit_only = attr.ib(False)
def __str__(self):
return self.original
class TestStringEscape:
TESTS = {
'foo\\bar': r'foo\\bar',
'foo\nbar': r'foo\nbar',
'foo\rbar': r'foo\rbar',
"foo'bar": r"foo\'bar",
'foo"bar': r'foo\"bar',
'one\\two\rthree\nfour\'five"six': r'one\\two\rthree\nfour\'five\"six',
'\x00': r'\x00',
'hellö': 'hellö',
'': '',
'\x80Ā': '\x80Ā',
'𐀀\x00𐀀\x00': r'𐀀\x00𐀀\x00',
'𐀀\ufeff': r'𐀀\ufeff',
'\ufeff': r'\ufeff',
TESTS = [
Case('foo\\bar', r'foo\\bar'),
Case('foo\nbar', r'foo\nbar'),
Case('foo\rbar', r'foo\rbar'),
Case("foo'bar", r"foo\'bar"),
Case('foo"bar', r'foo\"bar'),
Case('one\\two\rthree\nfour\'five"six', r'one\\two\rthree\nfour\'five\"six'),
Case('\x00', r'\x00', webkit_only=True),
Case('hellö', 'hellö'),
Case('', ''),
Case('\x80Ā', '\x80Ā'),
Case('𐀀\x00𐀀\x00', r'𐀀\x00𐀀\x00', webkit_only=True),
Case('𐀀\ufeff', r'𐀀\ufeff'),
Case('\ufeff', r'\ufeff', webkit_only=True),
# http://stackoverflow.com/questions/2965293/
'\u2028': r'\u2028',
'\u2029': r'\u2029',
}
Case('\u2028', r'\u2028'),
Case('\u2029', r'\u2029'),
]
# Once there was this warning here:
# load glyph failed err=6 face=0x2680ba0, glyph=1912
# http://qutebrowser.org:8010/builders/debian-jessie/builds/765/steps/unittests/
# Should that be ignored?
@pytest.mark.parametrize('before, after', sorted(TESTS.items()), ids=repr)
def test_fake_escape(self, before, after):
@pytest.mark.parametrize('case', TESTS, ids=str)
def test_fake_escape(self, case):
"""Test javascript escaping with some expected outcomes."""
assert javascript.string_escape(before) == after
assert javascript.string_escape(case.original) == case.replacement
def _test_escape(self, text, webframe):
"""Test conversion by using evaluateJavaScript."""
def _test_escape(self, text, web_tab, qtbot):
"""Test conversion by running JS in a tab."""
escaped = javascript.string_escape(text)
result = webframe.evaluateJavaScript('"{}";'.format(escaped))
assert result == text
@pytest.mark.parametrize('text', sorted(TESTS), ids=repr)
def test_real_escape(self, webframe, text):
with qtbot.waitCallback() as cb:
web_tab.run_js_async('"{}";'.format(escaped), cb)
cb.assert_called_with(text)
@pytest.mark.parametrize('case', TESTS, ids=str)
def test_real_escape(self, web_tab, qtbot, case):
"""Test javascript escaping with a real QWebPage."""
self._test_escape(text, webframe)
if web_tab.backend == usertypes.Backend.QtWebEngine and case.webkit_only:
pytest.xfail("Not supported with QtWebEngine")
self._test_escape(case.original, web_tab, qtbot)
@pytest.mark.qt_log_ignore('^OpenType support missing for script')
@hypothesis.given(hypothesis.strategies.text())
def test_real_escape_hypothesis(self, webframe, text):
def test_real_escape_hypothesis(self, web_tab, qtbot, text):
"""Test javascript escaping with a real QWebPage and hypothesis."""
self._test_escape(text, webframe)
if web_tab.backend == usertypes.Backend.QtWebEngine:
hypothesis.assume('\x00' not in text)
self._test_escape(text, web_tab, qtbot)
@pytest.mark.parametrize('arg, expected', [

View File

@ -21,11 +21,14 @@
import os.path
import logging
import urllib.parse
import attr
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkProxy
import pytest
import hypothesis
import hypothesis.strategies
from qutebrowser.api import cmdutils
from qutebrowser.browser.network import pac
@ -760,3 +763,42 @@ class TestProxyFromUrl:
def test_invalid(self, url, exception):
with pytest.raises(exception):
urlutils.proxy_from_url(QUrl(url))
class TestParseJavascriptUrl:
@pytest.mark.parametrize('url, message', [
(QUrl(), ""),
(QUrl('https://example.com'), "Expected a javascript:... URL"),
(QUrl('javascript://example.com'),
"URL contains unexpected components: example.com"),
(QUrl('javascript://foo:bar@example.com:1234'),
"URL contains unexpected components: foo:bar@example.com:1234"),
])
def test_invalid(self, url, message):
with pytest.raises(urlutils.Error, match=message):
urlutils.parse_javascript_url(url)
@pytest.mark.parametrize('url, source', [
(QUrl('javascript:"hello" %0a "world"'), '"hello" \n "world"'),
# https://github.com/web-platform-tests/wpt/blob/master/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-query-fragment-components.html
(QUrl('javascript:"nope" ? "yep" : "what";'), '"nope" ? "yep" : "what";'),
(QUrl('javascript:"wrong"; // # %0a "ok";'), '"wrong"; // # \n "ok";'),
(QUrl('javascript:"%252525 ? %252525 # %252525"'),
'"%2525 ? %2525 # %2525"'),
])
def test_valid(self, url, source):
assert urlutils.parse_javascript_url(url) == source
@hypothesis.given(source=hypothesis.strategies.text())
def test_hypothesis(self, source):
scheme = 'javascript:'
url = QUrl(scheme + urllib.parse.quote(source))
hypothesis.assume(url.isValid())
try:
parsed = urlutils.parse_javascript_url(url)
except urlutils.Error:
pass
else:
assert parsed == source

View File

@ -809,8 +809,9 @@ class TestPDFJSVersion:
assert version._pdfjs_version() == 'unknown (bundled)'
@pytest.mark.parametrize('varname', [
'PDFJS.version', # older versions
'var pdfjsVersion', # newer versions
'PDFJS.version', # v1.10.100 and older
'var pdfjsVersion', # v2.0.943
'const pdfjsVersion', # v2.5.207
])
def test_known(self, monkeypatch, varname):
pdfjs_code = textwrap.dedent("""

View File

@ -22,6 +22,7 @@ basepython =
py36: {env:PYTHON:python3.6}
py37: {env:PYTHON:python3.7}
py38: {env:PYTHON:python3.8}
py39: {env:PYTHON:python3.9}
pip_version = pip
deps =
-r{toxinidir}/requirements.txt