Merge branch 'master' into more-sophisticated-adblock
This commit is contained in:
commit
fd155628e1
|
|
@ -1,5 +1,8 @@
|
|||
[run]
|
||||
source = qutebrowser
|
||||
include =
|
||||
qutebrowser/*
|
||||
tests/*
|
||||
scripts/*
|
||||
branch = true
|
||||
omit =
|
||||
qutebrowser/__main__.py
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
--------------------
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
|
|||
|
|
@ -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:[<Ctrl-Y>]+: +pass:[rl-yank]+
|
||||
* +pass:[<Down>]+: +pass:[completion-item-focus --history next]+
|
||||
* +pass:[<Escape>]+: +pass:[leave-mode]+
|
||||
* +pass:[<PgDown>]+: +pass:[completion-item-focus next-page]+
|
||||
* +pass:[<PgUp>]+: +pass:[completion-item-focus prev-page]+
|
||||
* +pass:[<Return>]+: +pass:[command-accept]+
|
||||
* +pass:[<Shift-Delete>]+: +pass:[completion-item-del]+
|
||||
* +pass:[<Shift-Tab>]+: +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>>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1 @@
|
|||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# @develop#
|
||||
PyInstaller
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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: .*
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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!");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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", [
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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', [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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("""
|
||||
|
|
|
|||
Loading…
Reference in New Issue