Merge branch 'master' into more-sophisticated-adblock

This commit is contained in:
Árni Dagur 2020-12-19 20:29:21 +00:00
commit bf4dbef806
68 changed files with 472 additions and 257 deletions

View File

@ -36,8 +36,7 @@ Changed
partially transparent qutebrowser window on a setup which supports doing so.
- If QtWebEngine is compiled with PipeWire support and libpipewire is
installed, qutebrowser will now support screen sharing on Wayland. Note that
QtWebEngine 5.15.1 (planned for August 2020) is needed, though the Archlinux
qt5-webengine package backports the patch.
QtWebEngine 5.15.1 is needed.
- When `:undo` is used with a count, it now reopens the count-th to last tab
instead of the last one. The depth can instead be passed as an argument,
which is also completed.
@ -52,6 +51,23 @@ Changed
- `: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.
- When `config.source(...)` is used with a `--config-py` argument given,
qutebrowser used to search relative files in the config basedir, leading to them
not being found when using a shared `config.py` for different basedirs. Instead,
they are now searched relative to the given `config.py` file.
- `navigate prev` (`[[`) and `navigate next` (`]]`) now recognize links with
`nav-prev` and `nav-next` classes, such as those used by the Hugo static site
generator.
- When `tabs.favicons` is disabled but `tabs.tabs_are_windows` is set, the
window icon is still set to the page's favicon now.
- The `--asciidoc` argument to `src2asciidoc.py` and `build_release.py` now
only takes the path to `asciidoc.py`, using the current Python interpreter by
default. To configure the Python interpreter as well, use
`--asciidoc-python path/to/python --asciidoc path/to/asciidoc.py`
instead of the former
`--asciidoc path/to/python path/to/asciidoc.py`.
- The `readability-js` userscript now adds some CSS to better deal
with images, similarly to what Firefox' reader mode does.
Added
~~~~~
@ -113,6 +129,14 @@ Fixed
- 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.
- When `:undo` to re-open a tab but `tabs.tabs_are_windows` was set between
closing and undoing the close, qutebrowser crashed. This is now fixed.
- Fixes for the `qute-pass` userscript:
* With newer `gopass` versions, a deprecation notice was copied as
password due to `qute-pass` using it in a deprecated way.
* The `--password-store` argument didn't actually set
`PASSWORD_STORE_DIR` for `pass`, resulting in `qute-pass` finding matches but the
underlying `pass` not finding matching passwords. This is now fixed.
v1.13.1 (2020-07-17)
--------------------
@ -1128,7 +1152,7 @@ Fixed
- `qute://` pages now work properly on Qt 5.11.2
- Error when passing a substring with spaces to `:tab-take`.
- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly.
- Greasemonkey scripts which start with a UTF-8 BOM are now handled correctly.
- When no documentation has been generated, the plaintext documentation now can
be shown for more files such as `qute://help/userscripts.html`.
- Crash when doing initial run on Wayland without XWayland.

View File

@ -226,7 +226,7 @@ Why does it take longer to open a URL in qutebrowser than in chromium?::
One workaround is to use this
https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script]
and place it in your $PATH with the name "qutebrowser". This
script passes the URL via an unix socket to qutebrowser (if its
script passes the URL via a unix socket to qutebrowser (if its
running already) using socat which is much faster and starts a new
qutebrowser if it is not running already.

View File

@ -763,7 +763,16 @@ Evaluate a JavaScript string.
* +*-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.
* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in. Predefined world names are:
- `main` (same world as the web page's JavaScript and
Greasemonkey, unless overridden via `@qute-js-world`)
- `application` (used for internal qutebrowser JS code,
should not be used via `:jseval` unless you know what
you're doing)
- `user` (currently unused)
- `jseval` (used for this command by default)
==== note

View File

@ -398,6 +398,7 @@ Pre-built colorschemes
- 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://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized]
- https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[gruvbox]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^

View File

@ -1737,6 +1737,8 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.prefers_color_scheme_dark
Force `prefers-color-scheme: dark` colors for websites.
This setting requires a restart.
Type: <<types,Bool>>
Default: +pass:[false]+

View File

@ -28,10 +28,20 @@ Debian/Ubuntu/...
^^^^^^^^^^^^^^^^^
For Debian based systems (Debian, Ubuntu, Linux Mint, ...), debug information
is available in the repositories:
is for QtWebEngine is available in a dedicated repository. Enable that repository
(https://wiki.debian.org/HowToGetABacktrace#Installing_the_debugging_symbols[Debian],
https://wiki.ubuntu.com/Debug%20Symbol%20Packages[Ubuntu],
https://www.linuxmint.com/rel_tessa_mate_whatsnew.php[Linux Mint]) and install
the debug packages:
----
# apt-get install python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg python3-dbg libqt5webkit5-dbg
# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebengine-dbg libqt5webengine5-dbgsym
----
or with the QtWebKit backend:
----
# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg libqt5webkit5-dbg
----
Fedora
@ -116,7 +126,7 @@ First install `gdb` on your system if it's not installed already.
Then run qutebrowser directly inside gdb like this:
----
$ gdb $(readlink -f $(which python3)) -ex 'run -m qutebrowser --debug'
$ gdb -ex r --args $(readlink -f $(which python3)) -m qutebrowser --debug --temp-basedir
----
Note qutebrowser/gdb will take a long time to start. After you reproduce the
@ -131,9 +141,10 @@ Program received signal SIGSEGV, Segmentation fault.
Now enter these commands at the gdb prompt:
----
(gdb) set pagination off
(gdb) set logging overwrite on
(gdb) set logging on
(gdb) bt full
# you might have to press enter a few times until you get the prompt back
(gdb) bt
(gdb) quit
----
@ -176,9 +187,10 @@ Getting the stack trace
Now enter these commands at the gdb prompt:
----
(gdb) set pagination off
(gdb) set logging overwrite on
(gdb) set logging on
(gdb) bt
# you might have to press enter a few times until you get the prompt back
(gdb) quit
----

View File

@ -29,7 +29,7 @@ install: man
install -Dm644 icons/qutebrowser.svg \
"$(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps/qutebrowser.svg"
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/userscripts/" \
$(wildcard misc/userscripts/*)
$(filter-out misc/userscripts/__pycache__,$(wildcard misc/userscripts/*))
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/scripts/" \
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \

View File

@ -1,4 +1,5 @@
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# encoding: iso-8859-1
#
# This file is part of qutebrowser.
#

View File

@ -5,10 +5,10 @@ certifi==2020.6.20
cffi==1.14.2
chardet==3.0.4
colorama==0.4.3
cryptography==3.0
cryptography==3.1
cssutils==1.0.2
github3.py==1.3.0
hunter==3.2.1
hunter==3.2.2
idna==2.10
jwcrypto==0.8
manhole==1.6.0
@ -16,10 +16,10 @@ packaging==20.4
pycparser==2.20
Pympler==0.8
pyparsing==2.4.7
PyQt-builder==1.4.0
PyQt-builder==1.5.0
python-dateutil==2.8.1
requests==2.24.0
sip==5.3.0
sip==5.4.0
six==1.15.0
toml==0.10.1
uritemplate==3.0.1

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==20.1.0
attrs==20.2.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.1.0
pydocstyle==5.1.1
pyflakes==2.2.0
six==1.15.0
snowballstemmer==2.0.0

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
diff-cover==3.0.1
diff-cover==4.0.0
inflect==4.1.0
Jinja2==2.11.2
jinja2-pluralize==0.3.0
@ -9,8 +9,7 @@ MarkupSafe==1.1.1
mypy==0.782
mypy-extensions==0.4.3
pluggy==0.13.1
Pygments==2.6.1
Pygments==2.7.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.3

View File

@ -2,4 +2,4 @@
altgraph==0.17
pyinstaller==4.0
pyinstaller-hooks-contrib==2020.7
pyinstaller-hooks-contrib==2020.8

View File

@ -4,7 +4,7 @@ astroid==2.3.3 # rq.filter: < 2.4
certifi==2020.6.20
cffi==1.14.2
chardet==3.0.4
cryptography==3.0
cryptography==3.1
github3.py==1.3.0
idna==2.10
isort==4.3.21

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.12.3 # rq.filter: < 5.13
PyQt5-sip==12.8.0
PyQt5-sip==12.8.1
PyQtWebEngine==5.12.1 # rq.filter: < 5.13

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.13.2 # rq.filter: < 5.14
PyQt5-sip==12.8.0
PyQt5-sip==12.8.1
PyQtWebEngine==5.13.2 # rq.filter: < 5.14

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.14.2 # rq.filter: < 5.15
PyQt5-sip==12.8.0
PyQt5-sip==12.8.1
PyQtWebEngine==5.14.0 # rq.filter: < 5.15

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.0 # rq.filter: < 6
PyQt5-sip==12.8.0
PyQtWebEngine==5.15.0 # rq.filter: < 6
PyQt5==5.15.1 # rq.filter: < 6
PyQt5-sip==12.8.1
PyQtWebEngine==5.15.1 # rq.filter: < 6

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.0
PyQt5-sip==12.8.0
PyQtWebEngine==5.15.0
PyQt5==5.15.1
PyQt5-sip==12.8.1
PyQtWebEngine==5.15.1

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.16
Pygments==2.6.1
Pygments==2.7.1
pyroma==2.6

View File

@ -10,7 +10,7 @@ imagesize==1.2.0
Jinja2==2.11.2
MarkupSafe==1.1.1
packaging==20.4
Pygments==2.6.1
Pygments==2.7.1
pyparsing==2.4.7
pytz==2020.1
requests==2.24.0

View File

@ -1,18 +1,20 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==20.1.0
apipkg==1.5
attrs==20.2.0
beautifulsoup4==4.9.1
certifi==2020.6.20
chardet==3.0.4
cheroot==8.4.5
click==7.1.2
# colorama==0.4.3
coverage==5.2.1
coverage==5.3
EasyProcess==0.3
execnet==1.7.1
Flask==1.1.2
glob2==0.7
hunter==3.2.1
hypothesis==5.29.0
hunter==3.2.2
hypothesis==5.35.3 ; python_version>="3.6"
idna==2.10
iniconfig==1.0.1
itsdangerous==1.1.0
@ -21,24 +23,26 @@ jaraco.functools==3.0.1 ; python_version>="3.6"
Mako==1.1.3
manhole==1.6.0
# MarkupSafe==1.1.1
more-itertools==8.4.0
more-itertools==8.5.0
packaging==20.4
parse==1.16.0
parse==1.18.0
parse-type==0.5.2
pluggy==0.13.1
py==1.9.0
py-cpuinfo==7.0.0
Pygments==2.6.1
Pygments==2.7.1
pyparsing==2.4.7
pytest==6.0.1
pytest-bdd==3.4.0
pytest==6.0.2
pytest-bdd==4.0.1
pytest-benchmark==3.2.3
pytest-cov==2.10.1
pytest-forked==1.3.0
pytest-instafail==0.4.2
pytest-mock==3.3.0
pytest-mock==3.3.1
pytest-qt==3.3.0
pytest-repeat==0.8.0
pytest-rerunfailures==9.0
pytest-rerunfailures==9.1
pytest-xdist==2.1.0
pytest-xvfb==2.0.0
PyVirtualDisplay==1.3.2
requests==2.24.0
@ -53,3 +57,4 @@ vulture==2.1 ; python_version>="3.6"
Werkzeug==1.0.1
jaraco.functools==2.0; python_version<"3.6"
vulture==1.6; python_version<"3.6"
hypothesis<5.34.0; python_version<"3.6"

View File

@ -25,6 +25,8 @@ pytest-cov
# To avoid windows from popping up
pytest-xvfb
PyVirtualDisplay
# To run on multiple cores with -n
pytest-xdist
# Needed to test misc/userscripts/qute-lastpass
tldextract
@ -35,4 +37,7 @@ tldextract
#@ markers: vulture python_version>="3.6"
#@ add: vulture==1.6; python_version<"3.6"
#@ markers: hypothesis python_version>="3.6"
#@ add: hypothesis<5.34.0; python_version<"3.6"
#@ ignore: Jinja2, MarkupSafe, colorama

View File

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

View File

@ -40,7 +40,7 @@ The following userscripts are included in the current directory.
The following userscripts can be found on their own repositories.
- [qurlshare](https://github.com/sim590/qurlshare): *secure* sharing of an URL between qutebrowser
- [qurlshare](https://github.com/sim590/qurlshare): *secure* sharing of a URL between qutebrowser
instances using a distributed hash table.
- [qutebrowser-userscripts](https://github.com/cryzed/qutebrowser-userscripts):
a small pack of userscripts.

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Behaviour
# Behavior
# Userscript for qutebrowser which casts the url passed in $1 to the default
# ChromeCast device in the network using the program `castnow`
#

View File

@ -281,7 +281,7 @@ def main(arguments):
qute_command('enter-mode insert')
# If it finds a TOTP code, it copies it to the clipboard,
# which is the same behaviour as the Firefox add-on.
# which is the same behavior as the Firefox add-on.
if not arguments.totp_only and totp and arguments.totp:
# The import is done here, to make pyperclip an optional dependency
import pyperclip

View File

@ -61,11 +61,19 @@ import sys
import tldextract
def expanded_path(path):
# Expand potential ~ in paths, since this script won't be called from a shell that does it for us
expanded = os.path.expanduser(path)
# Add trailing slash if not present
return os.path.join(expanded, '')
argument_parser = argparse.ArgumentParser(description=__doc__, usage=USAGE, epilog=EPILOG)
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
argument_parser.add_argument('--password-store', '-p',
default=os.getenv('PASSWORD_STORE_DIR', default=os.path.expanduser('~/.password-store')),
help='Path to your pass password-store (only used in pass-mode)')
default=expanded_path(os.getenv('PASSWORD_STORE_DIR', default='~/.password-store')),
help='Path to your pass password-store (only used in pass-mode)', type=expanded_path)
argument_parser.add_argument('--mode', '-M', choices=['pass', 'gopass'], default="pass",
help='Select mode [gopass] to use gopass instead of the standard pass.')
argument_parser.add_argument('--username-pattern', '-u', default=r'.*/(.+)',
@ -107,7 +115,7 @@ def qute_command(command):
fifo.flush()
def find_pass_candidates(domain, password_store_path):
def find_pass_candidates(domain):
candidates = []
if arguments.mode == "gopass":
@ -117,13 +125,13 @@ def find_pass_candidates(domain, password_store_path):
if domain in password:
candidates.append(password)
else:
for path, directories, file_names in os.walk(password_store_path, followlinks=True):
for path, directories, file_names in os.walk(arguments.password_store, followlinks=True):
secrets = fnmatch.filter(file_names, '*.gpg')
if not secrets:
continue
# Strip password store path prefix to get the relative pass path
pass_path = path[len(password_store_path):]
pass_path = path[len(arguments.password_store):]
split_path = pass_path.split(os.path.sep)
for secret in secrets:
secret_base = os.path.splitext(secret)[0]
@ -134,25 +142,27 @@ def find_pass_candidates(domain, password_store_path):
return candidates
def _run_pass(pass_arguments, encoding):
def _run_pass(pass_arguments):
# The executable is conveniently named after it's mode [pass|gopass].
pass_command = [arguments.mode]
process = subprocess.run(pass_command + pass_arguments, stdout=subprocess.PIPE)
return process.stdout.decode(encoding).strip()
env = os.environ.copy()
env['PASSWORD_STORE_DIR'] = arguments.password_store
process = subprocess.run(pass_command + pass_arguments, env=env, stdout=subprocess.PIPE)
return process.stdout.decode(arguments.io_encoding).strip()
def pass_(path, encoding):
return _run_pass([path], encoding)
def pass_(path):
return _run_pass(['show', path])
def pass_otp(path, encoding):
return _run_pass(['otp', path], encoding)
def pass_otp(path):
return _run_pass(['otp', path])
def dmenu(items, invocation, encoding):
def dmenu(items, invocation):
command = shlex.split(invocation)
process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE)
return process.stdout.decode(encoding).strip()
process = subprocess.run(command, input='\n'.join(items).encode(arguments.io_encoding), stdout=subprocess.PIPE)
return process.stdout.decode(arguments.io_encoding).strip()
def fake_key_raw(text):
@ -170,11 +180,6 @@ def main(arguments):
extractor = tldextract.TLDExtract(extra_suffixes=arguments.extra_url_suffixes.split(','))
extract_result = extractor(arguments.url)
# Expand potential ~ in paths, since this script won't be called from a shell that does it for us
password_store_path = os.path.expanduser(arguments.password_store)
# Add trailing slash if not present
password_store_path = os.path.join(password_store_path, '')
# Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
# the registered domain name, the IPv4 address if that's what the URL represents and finally the private domain
# (if a non-public suffix was used).
@ -188,7 +193,7 @@ def main(arguments):
for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.ipv4, private_domain]):
attempted_targets.append(target)
target_candidates = find_pass_candidates(target, password_store_path)
target_candidates = find_pass_candidates(target)
if not target_candidates:
continue
@ -200,8 +205,7 @@ def main(arguments):
stderr('No pass candidates for URL {!r} found! (I tried {!r})'.format(arguments.url, attempted_targets))
return ExitCodes.NO_PASS_CANDIDATES
selection = candidates.pop() if len(candidates) == 1 else dmenu(sorted(candidates), arguments.dmenu_invocation,
arguments.io_encoding)
selection = candidates.pop() if len(candidates) == 1 else dmenu(sorted(candidates), arguments.dmenu_invocation)
# Nothing was selected, simply return
if not selection:
return ExitCodes.SUCCESS
@ -209,7 +213,7 @@ def main(arguments):
# If username-target is path and user asked for username-only, we don't need to run pass
secret = None
if not (arguments.username_target == 'path' and arguments.username_only):
secret = pass_(selection, arguments.io_encoding)
secret = pass_(selection)
# Match password
match = re.match(arguments.password_pattern, secret)
@ -231,7 +235,7 @@ def main(arguments):
elif arguments.password_only:
fake_key_raw(password)
elif arguments.otp_only:
otp = pass_otp(selection, arguments.io_encoding)
otp = pass_otp(selection)
fake_key_raw(otp)
else:
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch

View File

@ -44,6 +44,16 @@ const HEADER = `
h1, h2, h3 {
line-height: 1.2;
}
img {
max-width:100%;
height:auto;
}
p > img:only-child,
p > a:only-child > img:only-child,
.wp-caption img,
figure img {
display: block;
}
</style>
<!-- This icon is licensed under the Mozilla Public License 2.0 (available at: https://www.mozilla.org/en-US/MPL/2.0/).
The original icon can be found here: https://dxr.mozilla.org/mozilla-central/source/browser/themes/shared/reader/readerMode.svg -->

View File

@ -35,7 +35,7 @@ markers =
no_invalid_lines: Don't fail on unparseable lines in end2end tests
qtbug60673: Tests which are broken if the conversion from orange selection to real selection is flaky
fake_os: Fake utils.is_* to a fake operating system
unicode_locale: Tests which need an unicode locale to work
unicode_locale: Tests which need a 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

View File

@ -897,6 +897,8 @@ class AbstractTab(QWidget):
icon_changed = pyqtSignal(QIcon)
#: Signal emitted when a page's title changed (new title as str)
title_changed = pyqtSignal(str)
#: Signal emitted when this tab was pinned/unpinned (new pinned state as bool)
pinned_changed = pyqtSignal(bool)
#: Signal emitted when a new tab should be opened (url as QUrl)
new_tab_requested = pyqtSignal(QUrl)
#: Signal emitted when a page's URL changed (url as QUrl)
@ -1191,6 +1193,10 @@ class AbstractTab(QWidget):
def set_html(self, html: str, base_url: QUrl = QUrl()) -> None:
raise NotImplementedError
def set_pinned(self, pinned: bool) -> None:
self.data.pinned = pinned
self.pinned_changed.emit(pinned)
def __repr__(self) -> str:
try:
qurl = self.url()

View File

@ -278,7 +278,7 @@ class CommandDispatcher:
return
to_pin = not tab.data.pinned
self._tabbed_browser.widget.set_tab_pinned(tab, to_pin)
tab.set_pinned(to_pin)
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window')
@ -421,7 +421,8 @@ class CommandDispatcher:
newtab.data.keep_icon = True
newtab.history.private_api.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor())
new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
newtab.set_pinned(curtab.data.pinned)
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window',
@ -1663,7 +1664,15 @@ class CommandDispatcher:
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.
run the snippet in. Predefined world names are:
- `main` (same world as the web page's JavaScript and
Greasemonkey, unless overridden via `@qute-js-world`)
- `application` (used for internal qutebrowser JS code,
should not be used via `:jseval` unless you know what
you're doing)
- `user` (currently unused)
- `jseval` (used for this command by default)
"""
cmdutils.check_exclusive((file, url), 'fu')

View File

@ -154,14 +154,19 @@ def strip(url, count):
def _find_prevnext(prev, elems):
"""Find a prev/next element in the given list of elements."""
# First check for <link rel="prev(ious)|next">
# First check for <link rel="prev(ious)|next"> as well as
# e.g. <a class="nav-(prev|next)"> (Hugo)
rel_values = {'prev', 'previous'} if prev else {'next'}
classes = {'nav-prev'} if prev else {'nav-next'}
for e in elems:
if e.tag_name() not in ['link', 'a'] or 'rel' not in e:
if e.tag_name() not in ['link', 'a']:
continue
if set(e['rel'].split(' ')) & rel_values:
if 'rel' in e and set(e['rel'].split(' ')) & rel_values:
log.hints.debug("Found {!r} with rel={}".format(e, e['rel']))
return e
elif e.classes() & classes:
log.hints.debug("Found {!r} with class={}".format(e, e.classes()))
return e
# Then check for regular links/buttons.
elems = [e for e in elems if e.tag_name() != 'link']

View File

@ -102,8 +102,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Get the geometry for this element."""
raise NotImplementedError
def classes(self) -> typing.List[str]:
"""Get a list of classes assigned to this element."""
def classes(self) -> typing.Set[str]:
"""Get a set of classes assigned to this element."""
raise NotImplementedError
def tag_name(self) -> str:

View File

@ -118,9 +118,9 @@ class WebEngineElement(webelem.AbstractWebElement):
log.stub()
return QRect()
def classes(self) -> typing.List[str]:
def classes(self) -> typing.Set[str]:
"""Get a list of classes assigned to this element."""
return self._js_dict['class_name'].split()
return set(self._js_dict['class_name'].split())
def tag_name(self) -> str:
"""Get the tag name of this element.

View File

@ -390,9 +390,14 @@ def init_private_profile():
def _init_site_specific_quirks():
"""Add custom user-agent settings for problematic sites.
See https://github.com/qutebrowser/qutebrowser/issues/4810
"""
if not config.val.content.site_specific_quirks:
return
# Please leave this here as a template for new UAs.
# default_ua = ("Mozilla/5.0 ({os_info}) "
# "AppleWebKit/{webkit_version} (KHTML, like Gecko) "
# "{qt_key}/{qt_version} "
@ -402,7 +407,6 @@ def _init_site_specific_quirks():
"AppleWebKit/{webkit_version} (KHTML, like Gecko) "
"{upstream_browser_key}/{upstream_browser_version} "
"Safari/{webkit_version}")
firefox_ua = "Mozilla/5.0 ({os_info}; rv:71.0) Gecko/20100101 Firefox/71.0"
new_chrome_ua = ("Mozilla/5.0 ({os_info}) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/99 "
@ -414,14 +418,26 @@ def _init_site_specific_quirks():
"Edg/{upstream_browser_version}")
user_agents = {
# Needed to avoid a ""WhatsApp works with Google Chrome 36+" error
# page which doesn't allow to use WhatsApp Web at all. Also see the
# additional JS quirk: qutebrowser/javascript/whatsapp_web_quirk.user.js
# https://github.com/qutebrowser/qutebrowser/issues/4445
'https://web.whatsapp.com/': no_qtwe_ua,
# Needed to avoid a "you're using a browser [...] that doesn't allow us
# to keep your account secure" error.
# https://github.com/qutebrowser/qutebrowser/issues/5182
'https://accounts.google.com/*': edge_ua,
# Needed because Slack adds an error which prevents using it relatively
# aggressively, despite things actually working fine.
# September 2020: Qt 5.12 works, but Qt <= 5.11 shows the error.
# https://github.com/qutebrowser/qutebrowser/issues/4669
'https://*.slack.com/*': new_chrome_ua,
'https://docs.google.com/*': firefox_ua,
'https://drive.google.com/*': firefox_ua,
}
if not qtutils.version_check('5.9'):
# Shows 502 Bad Gateway with the Qt 5.7 UA.
user_agents['https://www.dell.com/support/*'] = new_chrome_ua
for pattern, ua in user_agents.items():

View File

@ -101,9 +101,9 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished()
return self._elem.geometry()
def classes(self) -> typing.List[str]:
def classes(self) -> typing.Set[str]:
self._check_vanished()
return self._elem.classes()
return set(self._elem.classes())
def tag_name(self) -> str:
"""Get the tag name for the current element."""

View File

@ -181,9 +181,9 @@ class WebView(QWebView):
This is not needed for QtWebEngine, so it's in here.
"""
menu = self.page().createStandardContextMenu()
self.shutting_down.connect(menu.close) # type: ignore[arg-type]
self.shutting_down.connect(menu.close)
mm = modeman.instance(self.win_id)
mm.entered.connect(menu.close) # type: ignore[arg-type]
mm.entered.connect(menu.close)
menu.exec_(e.globalPos())
def showEvent(self, e):

View File

@ -39,11 +39,11 @@ def command(*, info):
def helptopic(*, info):
"""A CompletionModel filled with help topics."""
model = completionmodel.CompletionModel()
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
cmdlist = util.get_cmd_completions(info, include_aliases=False,
include_hidden=True, prefix=':')
settings = ((opt.name, opt.description)
settings = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Commands", cmdlist))

View File

@ -2714,6 +2714,7 @@ colors.webpage.prefers_color_scheme_dark:
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
restart: true
## dark mode

View File

@ -606,7 +606,9 @@ class ConfigAPI:
def source(self, filename: str) -> None:
"""Read the given config file from disk."""
if not os.path.isabs(filename):
filename = str(self.configdir / filename)
# We don't use self.configdir here so we get the proper file when starting
# with a --config-py argument given.
filename = os.path.join(os.path.dirname(standarddir.config_py()), filename)
try:
read_config_py(filename)

View File

@ -9,7 +9,7 @@ qute://warning/sessions</span> to show it again at a later time.</span>
<p>Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.</p>
<p>At the time of writing (April 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v1.14.0.</p>
<p>At the time of writing (September 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v1.15.0.</p>
<p>As a stop-gap measure:</p>

View File

@ -28,7 +28,6 @@ import datetime
import attr
from PyQt5.QtWidgets import QSizePolicy, QWidget, QApplication
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
from PyQt5.QtGui import QIcon
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
@ -212,8 +211,7 @@ class TabbedBrowser(QWidget):
self._tab_insert_idx_right = -1
self.is_shutting_down = False
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
self.widget.new_tab_requested.connect(
self.tabopen) # type: ignore[arg-type]
self.widget.new_tab_requested.connect(self.tabopen)
self.widget.currentChanged.connect(self._on_current_changed)
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@ -351,6 +349,8 @@ class TabbedBrowser(QWidget):
functools.partial(self._on_title_changed, tab))
tab.icon_changed.connect(
functools.partial(self._on_icon_changed, tab))
tab.pinned_changed.connect(
functools.partial(self._on_pinned_changed, tab))
tab.load_progress.connect(
functools.partial(self._on_load_progress, tab))
tab.load_finished.connect(
@ -530,7 +530,7 @@ class TabbedBrowser(QWidget):
newtab = self.tabopen(background=False, idx=entry.index)
newtab.history.private_api.deserialize(entry.history)
self.widget.set_tab_pinned(newtab, entry.pinned)
newtab.set_pinned(entry.pinned)
@pyqtSlot('QUrl', bool)
def load_url(self, url, newtab):
@ -788,26 +788,21 @@ class TabbedBrowser(QWidget):
if not self.widget.page_title(idx):
self.widget.set_page_title(idx, url.toDisplayString())
@pyqtSlot(browsertab.AbstractTab, QIcon)
def _on_icon_changed(self, tab, icon):
@pyqtSlot(browsertab.AbstractTab)
def _on_icon_changed(self, tab):
"""Set the icon of a tab.
Slot for the iconChanged signal of any tab.
Args:
tab: The WebView where the title was changed.
icon: The new icon
"""
if not tab.data.should_show_icon():
return
try:
idx = self._tab_index(tab)
self._tab_index(tab)
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self.widget.setTabIcon(idx, icon)
if config.val.tabs.tabs_are_windows:
self.widget.window().setWindowIcon(icon)
self.widget.update_tab_favicon(tab)
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
@ -922,6 +917,12 @@ class TabbedBrowser(QWidget):
self._update_window_title('scroll_pos')
self.widget.update_tab_title(idx, 'scroll_pos')
def _on_pinned_changed(self, tab):
"""Update the tab's pinned status."""
idx = self.widget.indexOf(tab)
self.widget.update_tab_favicon(tab)
self.widget.update_tab_title(idx)
def _on_audio_changed(self, tab, _muted):
"""Update audio field in tab when mute or recentlyAudible changed."""
try:

View File

@ -99,19 +99,6 @@ class TabWidget(QTabWidget):
bar.set_tab_data(idx, 'indicator-color', color)
bar.update(bar.tabRect(idx))
def set_tab_pinned(self, tab: QWidget,
pinned: bool) -> None:
"""Set the tab status as pinned.
Args:
tab: The tab to pin
pinned: Pinned tab state to set.
"""
idx = self.indexOf(tab)
tab.data.pinned = pinned
self.update_tab_favicon(tab)
self.update_tab_title(idx)
def tab_indicator_color(self, idx):
"""Get the tab indicator color for the given index."""
return self.tabBar().tab_indicator_color(idx)
@ -139,6 +126,7 @@ class TabWidget(QTabWidget):
field: A field name which was updated. If given, the title
is only set if the given field is in the template.
"""
assert idx != -1
tab = self.widget(idx)
if tab.data.pinned:
fmt = config.cache['tabs.title.format_pinned']
@ -344,14 +332,11 @@ class TabWidget(QTabWidget):
"""Update favicon of the given tab."""
idx = self.indexOf(tab)
if tab.data.should_show_icon():
self.setTabIcon(idx, tab.icon())
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(tab.icon())
else:
self.setTabIcon(idx, QIcon())
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(self.window().windowIcon())
icon = tab.icon() if tab.data.should_show_icon() else QIcon()
self.setTabIcon(idx, icon)
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(tab.icon())
def setTabIcon(self, idx: int, icon: QIcon) -> None:
"""Always show tab icons for pinned tabs in some circumstances."""

View File

@ -635,7 +635,7 @@ class ReportErrorDialog(QDialog):
hbox = QHBoxLayout()
hbox.addStretch()
btn = QPushButton("Close")
btn.clicked.connect(self.close) # type: ignore[arg-type]
btn.clicked.connect(self.close)
hbox.addWidget(btn)
vbox.addLayout(hbox)

View File

@ -470,8 +470,7 @@ class SessionManager(QObject):
if tab.get('active', False):
tab_to_focus = i
if new_tab.data.pinned:
tabbed_browser.widget.set_tab_pinned(new_tab,
new_tab.data.pinned)
new_tab.set_pinned(True)
if tab_to_focus is not None:
tabbed_browser.widget.setCurrentIndex(tab_to_focus)
if win.get('active', False):

View File

@ -644,12 +644,10 @@ def parse_javascript_url(url: QUrl) -> str:
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)
urlstr = url.toString(QUrl.FullyEncoded) # type: ignore[arg-type]
urlstr = urllib.parse.unquote(urlstr)
code = urlstr[len('javascript:'):]
if not code:
raise Error("Resulted in empty JavaScript code")

View File

@ -1,11 +1,11 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
adblock==0.3.1
attrs==20.1.0
attrs==20.2.0
colorama==0.4.3
cssutils==1.0.2
Jinja2==2.11.2
MarkupSafe==1.1.1
Pygments==2.6.1
Pygments==2.7.1
pyPEG2==2.15.2
PyYAML==5.3.1

View File

@ -46,10 +46,12 @@ class AsciiDoc:
FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts']
def __init__(self,
asciidoc: Optional[List[str]],
asciidoc: Optional[str],
asciidoc_python: Optional[str],
website: Optional[str]) -> None:
self._cmd = None # type: Optional[List[str]]
self._asciidoc = asciidoc
self._asciidoc_python = asciidoc_python
self._website = website
self._homedir = None # type: Optional[pathlib.Path]
self._themedir = None # type: Optional[pathlib.Path]
@ -218,7 +220,9 @@ class AsciiDoc:
def _get_asciidoc_cmd(self) -> List[str]:
"""Try to find out what commandline to use to invoke asciidoc."""
if self._asciidoc is not None:
return self._asciidoc
python = (sys.executable if self._asciidoc_python is None
else self._asciidoc_python)
return [python, self._asciidoc]
for executable in ['asciidoc', 'asciidoc.py']:
try:
@ -270,10 +274,12 @@ def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument('--website', help="Build website into a given "
"directory.")
parser.add_argument('--asciidoc', help="Full path to python and "
"asciidoc.py. If not given, it's searched in PATH.",
nargs=2, required=False,
metavar=('PYTHON', 'ASCIIDOC'))
parser.add_argument('--asciidoc', help="Full path to asciidoc.py. "
"If not given, it's searched in PATH.",
nargs='?')
parser.add_argument('--asciidoc-python', help="Python to use for asciidoc."
"If not given, the current Python interpreter is used.",
nargs='?')
return parser.parse_args()
@ -301,7 +307,8 @@ def main(colors: bool = False) -> None:
utils.change_cwd()
utils.use_color = colors
args = parse_args()
run(asciidoc=args.asciidoc, website=args.website)
run(asciidoc=args.asciidoc, asciidoc_python=args.asciidoc_python,
website=args.website)
if __name__ == '__main__':

View File

@ -78,10 +78,11 @@ def call_tox(toxenv, *args, python=sys.executable):
def run_asciidoc2html(args):
"""Common buildsteps used for all OS'."""
utils.print_title("Running asciidoc2html.py")
a2h_args = []
if args.asciidoc is not None:
a2h_args = ['--asciidoc'] + args.asciidoc
else:
a2h_args = []
a2h_args += ['--asciidoc', args.asciidoc]
if args.asciidoc_python is not None:
a2h_args += ['--asciidoc-python', args.asciidoc_python]
call_script('asciidoc2html.py', *a2h_args)
@ -201,7 +202,7 @@ def build_mac():
utils.print_title("Updating 3rdparty content")
update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
utils.print_title("Building .app via pyinstaller")
call_tox('pyinstaller', '-r')
call_tox('pyinstaller-64', '-r')
utils.print_title("Patching .app")
patch_mac_app()
utils.print_title("Building .dmg")
@ -457,10 +458,12 @@ def main():
parser = argparse.ArgumentParser()
parser.add_argument('--no-asciidoc', action='store_true',
help="Don't generate docs")
parser.add_argument('--asciidoc', help="Full path to python and "
"asciidoc.py. If not given, it's searched in PATH.",
nargs=2, required=False,
metavar=('PYTHON', 'ASCIIDOC'))
parser.add_argument('--asciidoc', help="Full path to asciidoc.py. "
"If not given, it's searched in PATH.",
nargs='?')
parser.add_argument('--asciidoc-python', help="Python to use for asciidoc."
"If not given, the current Python interpreter is used.",
nargs='?')
parser.add_argument('--upload', action='store_true', required=False,
help="Toggle to upload the release to GitHub")
args = parser.parse_args()

View File

@ -30,33 +30,55 @@ import tokenize
import traceback
import collections
import pathlib
from typing import List, Iterator, Optional
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir))
from scripts import utils
def _get_files(only_py=False):
"""Iterate over all python files and yield filenames."""
for (dirpath, _dirnames, filenames) in os.walk('.'):
parts = dirpath.split(os.sep)
if len(parts) >= 2:
rootdir = parts[1]
if rootdir.startswith('.') or rootdir == 'htmlcov':
# ignore hidden dirs and htmlcov
continue
if only_py:
endings = {'.py'}
else:
endings = {'.py', '.asciidoc', '.js', '.feature'}
files = (e for e in filenames if os.path.splitext(e)[1] in endings)
for name in files:
yield os.path.join(dirpath, name)
BINARY_EXTS = {'.png', '.icns', '.ico', '.bmp', '.gz', '.bin', '.pdf',
'.sqlite', '.woff2', '.whl'}
def check_git():
def _get_files(
*,
verbose: bool,
ignored: List[pathlib.Path] = None
) -> Iterator[pathlib.Path]:
"""Iterate over all files and yield filenames."""
filenames = subprocess.run(
['git', 'ls-files', '--cached', '--others', '--exclude-standard', '-z'],
stdout=subprocess.PIPE,
universal_newlines=True,
check=True,
)
all_ignored = ignored or []
all_ignored.append(
pathlib.Path('tests', 'unit', 'scripts', 'importer_sample', 'chrome'))
for filename in filenames.stdout.split('\0'):
path = pathlib.Path(filename)
is_ignored = any(path == p or p in path.parents for p in all_ignored)
if not filename or path.suffix in BINARY_EXTS or is_ignored:
continue
try:
with tokenize.open(str(path)):
pass
except SyntaxError as e:
# Could not find encoding
utils.print_col("{} - maybe {} should be added to BINARY_EXTS?".format(
str(e).capitalize(), path.suffix), 'yellow')
continue
if verbose:
print(path)
yield path
def check_git(_args: argparse.Namespace = None) -> bool:
"""Check for uncommitted git files.."""
if not os.path.isdir(".git"):
print("No .git dir, ignoring")
@ -79,7 +101,7 @@ def check_git():
return status
def check_spelling():
def check_spelling(args: argparse.Namespace) -> Optional[bool]:
"""Check commonly misspelled words."""
# Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
@ -90,37 +112,36 @@ def check_spelling():
'exitted', 'mininum', 'resett?ed', 'recieved', 'regularily',
'underlaying', 'inexistant', 'elipsis', 'commiting', 'existant',
'resetted', 'similarily', 'informations', 'an url', 'treshold',
'artefact'}
'artefact', 'an unix', 'an utf', 'an unicode'}
# Words which look better when splitted, but might need some fine tuning.
words |= {'webelements', 'mouseevent', 'keysequence', 'normalmode',
'eventloops', 'sizehint', 'statemachine', 'metaobject',
'logrecord', 'filetype'}
'logrecord'}
# Files which should be ignored, e.g. because they come from another
# package
hint_data = pathlib.Path('tests', 'end2end', 'data', 'hints')
ignored = [
os.path.join('.', 'scripts', 'dev', 'misc_checks.py'),
os.path.join('.', 'qutebrowser', '3rdparty', 'pdfjs'),
os.path.join('.', 'tests', 'end2end', 'data', 'hints', 'ace',
'ace.js'),
pathlib.Path('scripts', 'dev', 'misc_checks.py'),
pathlib.Path('qutebrowser', '3rdparty', 'pdfjs'),
hint_data / 'ace' / 'ace.js',
hint_data / 'bootstrap' / 'bootstrap.css',
]
seen = collections.defaultdict(list)
try:
ok = True
for fn in _get_files():
with tokenize.open(fn) as f:
if any(fn.startswith(i) for i in ignored):
continue
for path in _get_files(verbose=args.verbose, ignored=ignored):
with tokenize.open(str(path)) as f:
for line in f:
for w in words:
pattern = '[{}{}]{}'.format(w[0], w[0].upper(), w[1:])
if (re.search(pattern, line) and
fn not in seen[w] and
path not in seen[w] and
'# pragma: no spellcheck' not in line):
print('Found "{}" in {}!'.format(w, fn))
seen[w].append(fn)
print('Found "{}" in {}!'.format(w, path))
seen[w].append(path)
ok = False
print()
return ok
@ -129,15 +150,18 @@ def check_spelling():
return None
def check_vcs_conflict():
def check_vcs_conflict(args: argparse.Namespace) -> Optional[bool]:
"""Check VCS conflict markers."""
try:
ok = True
for fn in _get_files(only_py=True):
with tokenize.open(fn) as f:
for path in _get_files(verbose=args.verbose):
if path.suffix in {'.rst', '.asciidoc'}:
# False positives
continue
with tokenize.open(str(path)) as f:
for line in f:
if any(line.startswith(c * 7) for c in '<>=|'):
print("Found conflict marker in {}".format(fn))
print("Found conflict marker in {}".format(path))
ok = False
print()
return ok
@ -146,7 +170,7 @@ def check_vcs_conflict():
return None
def check_userscripts_descriptions():
def check_userscripts_descriptions(_args: argparse.Namespace = None) -> bool:
"""Make sure all userscripts are described properly."""
folder = pathlib.Path('misc/userscripts')
readme = folder / 'README.md'
@ -178,20 +202,21 @@ def check_userscripts_descriptions():
return ok
def main():
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='store_true', help='Show checked filenames')
parser.add_argument('checker',
choices=('git', 'vcs', 'spelling', 'userscripts'),
help="Which checker to run.")
args = parser.parse_args()
if args.checker == 'git':
ok = check_git()
ok = check_git(args)
elif args.checker == 'vcs':
ok = check_vcs_conflict()
ok = check_vcs_conflict(args)
elif args.checker == 'spelling':
ok = check_spelling()
ok = check_spelling(args)
elif args.checker == 'userscripts':
ok = check_userscripts_descriptions()
ok = check_userscripts_descriptions(args)
return 0 if ok else 1

View File

@ -43,6 +43,11 @@ CHANGELOG_URLS = {
'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/blob/master/CHANGELOG.rst',
'pytest-xdist': 'https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst',
'pytest-forked': 'https://github.com/pytest-dev/pytest-forked/blob/master/CHANGELOG',
'execnet': 'https://execnet.readthedocs.io/en/latest/changelog.html',
'apipkg': 'https://github.com/pytest-dev/apipkg/blob/master/CHANGELOG',
'pytest-rerunfailures': 'https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.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',
@ -101,7 +106,12 @@ CHANGELOG_URLS = {
'pep517': 'https://github.com/pypa/pep517/commits/master',
'cryptography': 'https://cryptography.io/en/latest/changelog/',
'toml': 'https://github.com/uiri/toml/releases',
'pyqt': 'https://www.riverbankcomputing.com/',
'PyQt5': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine': 'https://www.riverbankcomputing.com/news',
'PyQt-builder': 'https://www.riverbankcomputing.com/news',
'PyQt5-sip': 'https://www.riverbankcomputing.com/news',
'sip': 'https://www.riverbankcomputing.com/news',
'Pygments': 'https://pygments.org/docs/changelog/',
'vulture': 'https://github.com/jendrikseipp/vulture/blob/master/CHANGELOG.md',
'distlib': 'https://bitbucket.org/pypa/distlib/src/master/CHANGES.rst',
'py-cpuinfo': 'https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog',
@ -111,6 +121,7 @@ CHANGELOG_URLS = {
'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',
'diff_cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG',
}
# PyQt versions which need SIP v4
@ -265,8 +276,8 @@ class Change:
self.name = name
self.old = None
self.new = None
if name.lower() in CHANGELOG_URLS:
self.url = CHANGELOG_URLS[name.lower()]
if name in CHANGELOG_URLS:
self.url = CHANGELOG_URLS[name]
self.link = '[{}]({})'.format(self.name, self.url)
else:
self.url = '(no changelog)'
@ -313,6 +324,8 @@ def print_changed_files():
if '==' in line:
name, version = line[1:].split('==')
if ';' in version: # pip environment markers
version = version.split(';')[0].strip()
else:
name = line[1:]
version = '?'

View File

@ -78,13 +78,12 @@ if __name__ == "__main__":
print("* Create new release via GitHub (required to upload release "
"artifacts)")
print("* Linux: git fetch && git checkout v{v} && "
"./.venv/bin/python3 scripts/dev/build_release.py --upload"
"tox -e build-release -- --upload"
.format(v=version))
print("* Windows: git fetch; git checkout v{v}; "
"py -3 scripts\\dev\\build_release.py --asciidoc "
"C:\\Python27\\python "
"$env:userprofile\\bin\\asciidoc-8.6.10\\asciidoc.py --upload"
"py -3.7 -m tox -e build-release -- --asciidoc "
"$env:userprofile\\bin\\asciidoc-9.9.2\\asciidoc.py --upload"
.format(v=version))
print("* macOS: git fetch && git checkout v{v} && "
"python3 scripts/dev/build_release.py --upload"
"tox -e build-release -- --upload"
.format(v=version))

View File

@ -1613,3 +1613,12 @@ Feature: Tab management
And I open data/hello.txt in a new tab
And I run :fake-key -g hello-world<enter>
Then the message "hello-world" should be shown
Scenario: Undo after changing tabs_are_windows
When I open data/hello.txt
And I open data/hello.txt in a new tab
And I set tabs.tabs_are_windows to true
And I run :tab-close
And I run :undo
And I run :message-info "Still alive!"
Then the message "Still alive!" should be shown

View File

@ -7,7 +7,7 @@
<body>
<p>When <code>hints.hide_unmatched_rapid_hints</code> is set to true (default), rapid hints behave like normal hints, i.e. unmatched hints will be hidden as you type. Setting the option to false will disable hiding in rapid mode, which is sometimes useful (see <a href="https://github.com/qutebrowser/qutebrowser/issues/1799">#1799</a>).</p>
<p>Note that when hinting in number mode, the <code>hints.hide_unmatched_rapid_hints</code> option affects typing the hint string (number), but not the filter (letters).</p>
<p>Here is couple of invalid links to test the behaviour:</p>
<p>Here is couple of invalid links to test the behavior:</p>
<p><a href="#foo">one</a></p>
<p><a href="#foo">two</a></p>
<p><a href="#foo">three</a></p>

View File

@ -52,8 +52,9 @@ class TestFixedDataNetworkReply:
b'Hello World! This is a test.'])
def test_data(self, qtbot, req, data):
reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo')
with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead,
reply.finished], order='strict'):
with qtbot.waitSignal(reply.metaDataChanged), \
qtbot.waitSignal(reply.readyRead), \
qtbot.waitSignal(reply.finished):
pass
assert reply.bytesAvailable() == len(data)
@ -78,7 +79,7 @@ def test_error_network_reply(qtbot, req):
reply = networkreply.ErrorNetworkReply(
req, "This is an error", QNetworkReply.UnknownNetworkError)
with qtbot.waitSignals([reply.error, reply.finished], order='strict'):
with qtbot.waitSignal(reply.error), qtbot.waitSignal(reply.finished):
pass
reply.abort() # shouldn't do anything

View File

@ -77,8 +77,8 @@ def test_set_pattern(pat, qtbot):
for c in cats:
c.set_pattern = mock.Mock(spec=[])
model.add_category(c)
with qtbot.waitSignals([model.layoutAboutToBeChanged, model.layoutChanged],
order='strict'):
with qtbot.waitSignal(model.layoutAboutToBeChanged), \
qtbot.waitSignal(model.layoutChanged):
model.set_pattern(pat)
for c in cats:
c.set_pattern.assert_called_with(pat)

View File

@ -124,7 +124,7 @@ def configdata_stub(config_stub, monkeypatch, configdata_init):
no_autoconfig=True)),
('bindings.commands', configdata.Option(
name='bindings.commands',
description='Default keybindings',
description='Custom keybindings',
typ=configtypes.Dict(
keytype=configtypes.String(),
valtype=configtypes.Dict(
@ -269,13 +269,38 @@ def test_help_completion(qtmodeltester, cmdutils_stub, key_config_stub,
(':tab-close', 'Close the current tab.', ''),
],
"Settings": [
('aliases', 'Aliases for commands.', None),
('bindings.commands', 'Default keybindings', None),
('bindings.default', 'Default keybindings', None),
('completion.open_categories', 'Which categories to show (in '
'which order) in the :open completion.', None),
('content.javascript.enabled', 'Enable/Disable JavaScript', None),
('url.searchengines', 'searchengines list', None),
(
'aliases',
'Aliases for commands.',
'{"q": "quit"}',
),
(
'bindings.commands',
'Custom keybindings',
('{"normal": {"<Ctrl+q>": "quit", "I": "invalid", "ZQ": "quit", '
'"d": "scroll down"}}'),
),
(
'bindings.default',
'Default keybindings',
'{"normal": {"<Ctrl+q>": "quit", "d": "tab-close"}}',
),
(
'completion.open_categories',
'Which categories to show (in which order) in the :open completion.',
'["searchengines", "quickmarks", "bookmarks", "history"]',
),
(
'content.javascript.enabled',
'Enable/Disable JavaScript',
'true'
),
(
'url.searchengines',
'searchengines list',
('{"DEFAULT": "https://duckduckgo.com/?q={}", '
'"google": "https://google.com/?q={}"}'),
),
],
})
@ -909,7 +934,7 @@ def test_setting_option_completion(qtmodeltester, config_stub,
_check_completions(model, {
"Options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
('bindings.commands', 'Default keybindings', (
('bindings.commands', 'Custom keybindings', (
'{"normal": {"<Ctrl+q>": "quit", "I": "invalid", '
'"ZQ": "quit", "d": "scroll down"}}')),
('completion.open_categories', 'Which categories to show (in '
@ -933,7 +958,7 @@ def test_setting_dict_option_completion(qtmodeltester, config_stub,
_check_completions(model, {
"Dict options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
('bindings.commands', 'Default keybindings', (
('bindings.commands', 'Custom keybindings', (
'{"normal": {"<Ctrl+q>": "quit", "I": "invalid", '
'"ZQ": "quit", "d": "scroll down"}}')),
('url.searchengines', 'searchengines list',

View File

@ -432,11 +432,12 @@ class TestConfig:
assert conf.get_obj(name1) == 'never'
assert conf.get_obj(name2) is True
with qtbot.waitSignals([conf.changed, conf.changed]) as blocker:
with qtbot.waitSignal(conf.changed), qtbot.waitSignal(conf.changed):
conf.clear(save_yaml=save_yaml)
options = {e.args[0] for e in blocker.all_signals_and_args}
assert options == {name1, name2}
# Doesn't work with PyQt 5.15.1 workaround
# options = {blocker1.args[0], blocker2.args[0]}
# assert options == {name1, name2}
if save_yaml:
assert yaml_value(name1) is usertypes.UNSET

View File

@ -28,7 +28,7 @@ from PyQt5.QtCore import QSettings
from qutebrowser.config import (config, configfiles, configexc, configdata,
configtypes)
from qutebrowser.utils import utils, usertypes, urlmatch
from qutebrowser.utils import utils, usertypes, urlmatch, standarddir
from qutebrowser.keyinput import keyutils
@ -1064,6 +1064,24 @@ class TestConfigPy:
assert not config.instance.get_obj('content.javascript.enabled')
def test_source_configpy_arg(self, tmpdir, data_tmpdir, monkeypatch):
alt_filename = 'alt-config.py'
alt_confpy_dir = tmpdir / 'alt-confpy-dir'
alt_confpy_dir.ensure(dir=True)
monkeypatch.setattr(standarddir, 'config_py',
lambda: str(alt_confpy_dir / alt_filename))
subfile = alt_confpy_dir / 'subfile.py'
subfile.write_text("c.content.javascript.enabled = False",
encoding='utf-8')
alt_confpy = ConfPy(alt_confpy_dir, alt_filename)
alt_confpy.write("config.source('subfile.py')")
alt_confpy.read()
assert not config.instance.get_obj('content.javascript.enabled')
def test_source_errors(self, tmpdir, confpy):
subfile = tmpdir / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')

View File

@ -21,7 +21,7 @@
from unittest import mock
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, PYQT_VERSION
import pytest
from qutebrowser.keyinput import basekeyparser, keyutils
@ -309,6 +309,8 @@ class TestCount:
# https://github.com/qutebrowser/qutebrowser/issues/3743
handle_text(prompt_keyparser, Qt.Key_twosuperior, Qt.Key_B, Qt.Key_A)
@pytest.mark.skipif(PYQT_VERSION == 0x050F01,
reason='waitSignals is broken in PyQt 5.15.1')
def test_count_keystring_update(self, qtbot,
handle_text, prompt_keyparser):
"""Make sure the keystring is updated correctly when entering count."""

View File

@ -94,8 +94,9 @@ class TestTabWidget:
config_stub.val.tabs.position = "left"
pinned_num = [1, num_tabs - 1]
for tab in pinned_num:
widget.set_tab_pinned(widget.widget(tab), True)
for num in pinned_num:
tab = widget.widget(num)
tab.set_pinned(True)
first_size = widget.tabBar().tabSizeHint(0)
first_size_min = widget.tabBar().minimumTabSizeHint(0)

View File

@ -54,8 +54,8 @@ def fake_proc(monkeypatch, stubs):
def test_start(proc, qtbot, message_mock, py_proc):
"""Test simply starting a process."""
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
order='strict'):
with qtbot.waitSignal(proc.started, timeout=10000), \
qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv)
@ -70,8 +70,8 @@ def test_start_verbose(proc, qtbot, message_mock, py_proc):
"""Test starting a process verbosely."""
proc.verbose = True
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
order='strict'):
with qtbot.waitSignal(proc.started, timeout=10000), \
qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv)
@ -99,9 +99,8 @@ def test_start_output_message(proc, qtbot, caplog, message_mock, py_proc,
code.append("sys.exit(0)")
with caplog.at_level(logging.ERROR, 'message'):
with qtbot.waitSignals([proc.started, proc.finished],
timeout=10000,
order='strict'):
with qtbot.waitSignal(proc.started, timeout=10000), \
qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc(';'.join(code))
proc.start(*argv)
@ -147,8 +146,8 @@ def test_start_env(monkeypatch, qtbot, py_proc):
sys.exit(0)
""")
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
order='strict'):
with qtbot.waitSignal(proc.started, timeout=10000), \
qtbot.waitSignal(proc.finished, timeout=10000):
proc.start(*argv)
data = qutescheme.spawn_output
@ -187,12 +186,12 @@ def test_double_start(qtbot, proc, py_proc):
def test_double_start_finished(qtbot, proc, py_proc):
"""Test starting a GUIProcess twice (with the first call finished)."""
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
order='strict'):
with qtbot.waitSignal(proc.started, timeout=10000), \
qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc("import sys; sys.exit(0)")
proc.start(*argv)
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
order='strict'):
with qtbot.waitSignal(proc.started, timeout=10000), \
qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc("import sys; sys.exit(0)")
proc.start(*argv)
@ -267,8 +266,8 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream):
def test_stdout_not_decodable(proc, qtbot, message_mock, py_proc):
"""Test handling malformed utf-8 in stdout."""
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000,
order='strict'):
with qtbot.waitSignal(proc.started, timeout=10000), \
qtbot.waitSignal(proc.finished, timeout=10000):
argv = py_proc(r"""
import sys
# Using \x81 because it's invalid in UTF-8 and CP1252

View File

@ -493,10 +493,10 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8')
(b'{"args": [], "target_arg": null}\n', 'invalid version'),
])
def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
with caplog.at_level(logging.ERROR):
with qtbot.assertNotEmitted(ipc_server.got_args):
with qtbot.waitSignals(signals, order='strict'):
with qtbot.waitSignal(ipc_server.got_invalid_data), \
qtbot.waitSignal(connected_socket.disconnected):
connected_socket.write(data)
invalid_msg = 'Ignoring invalid IPC data from socket '
@ -514,8 +514,8 @@ def test_multiline(qtbot, ipc_server, connected_socket):
version=ipc.PROTOCOL_VERSION))
with qtbot.assertNotEmitted(ipc_server.got_invalid_data):
with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args],
order='strict'):
with qtbot.waitSignal(ipc_server.got_args), \
qtbot.waitSignal(ipc_server.got_args):
connected_socket.write(data.encode('utf-8'))
assert len(spy) == 2

View File

@ -115,7 +115,7 @@ def test_not_found(caplog):
def test_utf8():
"""Test rendering with an UTF8 template.
"""Test rendering with a UTF8 template.
This was an attempt to get a failing test case for #127 but it seems
the issue is elsewhere.

View File

@ -606,9 +606,9 @@ URL_TEXT = hst.text(alphabet=string.ascii_letters)
@hypothesis.given(pattern=hst.builds(
lambda *a: ''.join(a),
# Scheme
hst.one_of(hst.just('*'), hst.just('http'), hst.just('file')),
hst.sampled_from(['*', 'http', 'file']),
# Separator
hst.one_of(hst.just(':'), hst.just('://')),
hst.sampled_from([':', '://']),
# Host
hst.one_of(hst.just('*'),
hst.builds(lambda *a: ''.join(a), hst.just('*.'), URL_TEXT),

View File

@ -781,6 +781,8 @@ class TestParseJavascriptUrl:
@pytest.mark.parametrize('url, source', [
(QUrl('javascript:"hello" %0a "world"'), '"hello" \n "world"'),
(QUrl('javascript:/'), '/'),
(QUrl('javascript:///'), '///'),
# 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";'),

View File

@ -53,23 +53,25 @@ def test_done(mode, answer, signal_names, question, qtbot):
question.mode = mode
question.answer = answer
signals = [getattr(question, name) for name in signal_names]
with qtbot.waitSignals(signals, order='strict'):
question.done()
blockers = [qtbot.waitSignal(signal) for signal in signals]
question.done()
for blocker in blockers:
blocker.wait()
assert not question.is_aborted
def test_cancel(question, qtbot):
"""Test Question.cancel()."""
with qtbot.waitSignals([question.cancelled, question.completed],
order='strict'):
with qtbot.waitSignal(question.cancelled), qtbot.waitSignal(question.completed):
question.cancel()
assert not question.is_aborted
def test_abort(question, qtbot):
"""Test Question.abort()."""
with qtbot.waitSignals([question.aborted, question.completed],
order='strict'):
with qtbot.waitSignal(question.aborted), qtbot.waitSignal(question.completed):
question.abort()
assert question.is_aborted

14
tox.ini
View File

@ -224,3 +224,17 @@ deps =
-r{toxinidir}/misc/requirements/requirements-sphinx.txt
commands =
{envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/
[testenv:build-release]
basepython = {env:PYTHON:python3}
pip_version = pip
passenv = *
usedevelop = true
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-tox.txt
-r{toxinidir}/misc/requirements/requirements-pyqt.txt
-r{toxinidir}/misc/requirements/requirements-dev.txt
-r{toxinidir}/misc/requirements/requirements-pyinstaller.txt
commands =
{envpython} {toxinidir}/scripts/dev/build_release.py {posargs}