Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a90a894177 | ||
|
|
ce305b0fed | ||
|
|
b19b8163e1 | ||
|
|
203664a12c | ||
|
|
cfe7e7883a | ||
|
|
dfe74ff0ee | ||
|
|
18a45bbd5b | ||
|
|
30462b3839 | ||
|
|
1a7669c8a2 | ||
|
|
da5f8269c7 | ||
|
|
39353cdc00 | ||
|
|
6edfd1801d | ||
|
|
21364f135e | ||
|
|
6f6eeae902 | ||
|
|
c551922e2d | ||
|
|
f7585eb60e |
@@ -109,7 +109,7 @@ The following software and libraries are required to run qutebrowser:
|
||||
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
|
||||
supported
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
|
||||
(5.9 recommended) for Python 3
|
||||
(5.9.2 recommended) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
|
||||
@@ -15,6 +15,19 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v1.0.4
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- The `qute://gpl` page now works correctly again.
|
||||
- Trying to bind an empty command now doesn't crash anymore.
|
||||
- Fixed crash when `:config-write-py` fails to write to the given path.
|
||||
- Fixed crash for some users when selecting a file with Qt 5.9.3
|
||||
- Improved handling for various SQL errors
|
||||
- Fix crash when setting content.cache.size to a big value (> 2 GB)
|
||||
|
||||
v1.0.3
|
||||
------
|
||||
|
||||
|
||||
@@ -486,7 +486,9 @@ Start hinting.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
|
||||
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. With rapid hinting, the hint mode isn't left after a hint is followed, so you can easily
|
||||
open multiple links. This is only possible with targets
|
||||
`tab` (with `tabs.background_tabs=true`), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
|
||||
* +*-m*+, +*--mode*+: The hinting mode to use.
|
||||
|
||||
@@ -1407,6 +1407,7 @@ This setting is only available with the QtWebKit backend.
|
||||
[[content.cache.size]]
|
||||
=== content.cache.size
|
||||
Size of the HTTP network cache. Null to use the default value.
|
||||
With QtWebEngine, the maximum supported value is 2147483647 (~2 GB).
|
||||
|
||||
Type: <<types,Int>>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.9.1
|
||||
sip==4.19.4
|
||||
PyQt5==5.9.2
|
||||
sip==4.19.6
|
||||
|
||||
@@ -1 +1 @@
|
||||
PyQt5
|
||||
PyQt5
|
||||
|
||||
@@ -55,4 +55,6 @@ qt_log_ignore =
|
||||
^QQuickWidget::invalidateRenderControl could not make context current
|
||||
^libpng warning: iCCP: known incorrect sRGB profile
|
||||
^inotify_add_watch(".*") failed: "No space left on device"
|
||||
^QSettings::value: Empty key passed
|
||||
^Icon theme ".*" not found
|
||||
xfail_strict = true
|
||||
|
||||
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (1, 0, 3)
|
||||
__version_info__ = (1, 0, 4)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
||||
@@ -616,8 +616,10 @@ class HintManager(QObject):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
rapid: Whether to do rapid hinting. This is only possible with
|
||||
targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
|
||||
rapid: Whether to do rapid hinting. With rapid hinting, the hint
|
||||
mode isn't left after a hint is followed, so you can easily
|
||||
open multiple links. This is only possible with targets
|
||||
`tab` (with `tabs.background_tabs=true`), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
add_history: Whether to add the spawned or yanked link to the
|
||||
browsing history.
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
import contextlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer
|
||||
|
||||
@@ -87,6 +88,16 @@ class WebHistory(sql.SqlTable):
|
||||
def __contains__(self, url):
|
||||
return self._contains_query.run(val=url).value()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_sql_errors(self):
|
||||
try:
|
||||
yield
|
||||
except sql.SqlError as e:
|
||||
if e.environmental:
|
||||
message.error("Failed to write history: {}".format(e.text()))
|
||||
else:
|
||||
raise
|
||||
|
||||
def _rebuild_completion(self):
|
||||
data = {'url': [], 'title': [], 'last_atime': []}
|
||||
# select the latest entry for each url
|
||||
@@ -142,8 +153,9 @@ class WebHistory(sql.SqlTable):
|
||||
"history?")
|
||||
|
||||
def _do_clear(self):
|
||||
self.delete_all()
|
||||
self.completion.delete_all()
|
||||
with self._handle_sql_errors():
|
||||
self.delete_all()
|
||||
self.completion.delete_all()
|
||||
|
||||
def delete_url(self, url):
|
||||
"""Remove all history entries with the given url.
|
||||
@@ -191,7 +203,7 @@ class WebHistory(sql.SqlTable):
|
||||
|
||||
atime = int(atime) if (atime is not None) else int(time.time())
|
||||
|
||||
try:
|
||||
with self._handle_sql_errors():
|
||||
self.insert({'url': self._format_url(url),
|
||||
'title': title,
|
||||
'atime': atime,
|
||||
@@ -202,11 +214,6 @@ class WebHistory(sql.SqlTable):
|
||||
'title': title,
|
||||
'last_atime': atime
|
||||
}, replace=True)
|
||||
except sql.SqlError as e:
|
||||
if e.environmental:
|
||||
message.error("Failed to write history: {}".format(e.text()))
|
||||
else:
|
||||
raise
|
||||
|
||||
def _parse_entry(self, line):
|
||||
"""Parse a history line like '12345 http://example.com title'."""
|
||||
|
||||
@@ -307,7 +307,7 @@ def qute_log(url):
|
||||
@add_handler('gpl')
|
||||
def qute_gpl(_url):
|
||||
"""Handler for qute://gpl. Return HTML content as string."""
|
||||
return 'text/html', utils.read_file('html/LICENSE.html')
|
||||
return 'text/html', utils.read_file('html/license.html')
|
||||
|
||||
|
||||
@add_handler('help')
|
||||
|
||||
@@ -92,9 +92,10 @@ class DefaultProfileSetter(websettings.Base):
|
||||
|
||||
"""A setting set on the QWebEngineProfile."""
|
||||
|
||||
def __init__(self, setter, default=websettings.UNSET):
|
||||
def __init__(self, setter, converter=None, default=websettings.UNSET):
|
||||
super().__init__(default)
|
||||
self._setter = setter
|
||||
self._converter = converter
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, setter=self._setter, constructor=True)
|
||||
@@ -103,7 +104,11 @@ class DefaultProfileSetter(websettings.Base):
|
||||
if settings is not None:
|
||||
raise ValueError("'settings' may not be set with "
|
||||
"DefaultProfileSetters!")
|
||||
|
||||
setter = getattr(default_profile, self._setter)
|
||||
if self._converter is not None:
|
||||
value = self._converter(value)
|
||||
|
||||
setter(value)
|
||||
|
||||
|
||||
@@ -281,7 +286,9 @@ MAPPINGS = {
|
||||
Attribute(QWebEngineSettings.LocalStorageEnabled),
|
||||
'content.cache.size':
|
||||
# 0: automatically managed by QtWebEngine
|
||||
DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
|
||||
DefaultProfileSetter('setHttpCacheMaximumSize', default=0,
|
||||
converter=lambda val:
|
||||
qtutils.check_overflow(val, 'int', fatal=False)),
|
||||
'content.xss_auditing':
|
||||
Attribute(QWebEngineSettings.XSSAuditingEnabled),
|
||||
'content.default_encoding':
|
||||
|
||||
@@ -422,12 +422,13 @@ class WebKitScroller(browsertab.AbstractScroller):
|
||||
else:
|
||||
for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]:
|
||||
if val is not None:
|
||||
val = qtutils.check_overflow(val, 'int', fatal=False)
|
||||
frame = self._widget.page().mainFrame()
|
||||
m = frame.scrollBarMaximum(orientation)
|
||||
if m == 0:
|
||||
maximum = frame.scrollBarMaximum(orientation)
|
||||
if maximum == 0:
|
||||
continue
|
||||
frame.setScrollBarValue(orientation, int(m * val / 100))
|
||||
pos = int(maximum * val / 100)
|
||||
pos = qtutils.check_overflow(pos, 'int', fatal=False)
|
||||
frame.setScrollBarValue(orientation, pos)
|
||||
|
||||
def _key_press(self, key, count=1, getter_name=None, direction=None):
|
||||
frame = self._widget.page().mainFrame()
|
||||
|
||||
@@ -170,6 +170,11 @@ class KeyConfig:
|
||||
|
||||
def bind(self, key, command, *, mode, save_yaml=False):
|
||||
"""Add a new binding from key to command."""
|
||||
if command is not None and not command.strip():
|
||||
raise configexc.KeybindingError(
|
||||
"Can't add binding '{}' with empty command in {} "
|
||||
'mode'.format(key, mode))
|
||||
|
||||
key = self._prepare(key, mode)
|
||||
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
|
||||
key, command, mode))
|
||||
|
||||
@@ -287,4 +287,7 @@ class ConfigCommands:
|
||||
|
||||
writer = configfiles.ConfigPyWriter(options, bindings,
|
||||
commented=commented)
|
||||
writer.write(filename)
|
||||
try:
|
||||
writer.write(filename)
|
||||
except OSError as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
|
||||
@@ -181,7 +181,10 @@ content.cache.size:
|
||||
none_ok: true
|
||||
minval: 0
|
||||
maxval: maxint64
|
||||
desc: Size of the HTTP network cache. Null to use the default value.
|
||||
desc: >-
|
||||
Size of the HTTP network cache. Null to use the default value.
|
||||
|
||||
With QtWebEngine, the maximum supported value is 2147483647 (~2 GB).
|
||||
|
||||
# Defaults from QWebSettings::QWebSettings() in
|
||||
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
|
||||
|
||||
@@ -70,9 +70,21 @@ class SqliteError(SqlError):
|
||||
environmental_errors = [
|
||||
'5', # SQLITE_BUSY ("database is locked")
|
||||
'8', # SQLITE_READONLY
|
||||
'11', # SQLITE_CORRUPT
|
||||
'13', # SQLITE_FULL
|
||||
]
|
||||
self.environmental = error.nativeErrorCode() in environmental_errors
|
||||
# At least in init(), we can get errors like this:
|
||||
# type: ConnectionError
|
||||
# database text: out of memory
|
||||
# driver text: Error opening database
|
||||
# error code: -1
|
||||
environmental_strings = [
|
||||
"out of memory",
|
||||
]
|
||||
errcode = error.nativeErrorCode()
|
||||
self.environmental = (
|
||||
errcode in environmental_errors or
|
||||
(errcode == -1 and error.databaseText() in environmental_strings))
|
||||
|
||||
def text(self):
|
||||
return self.error.databaseText()
|
||||
|
||||
@@ -327,7 +327,8 @@ def change_console_formatter(level):
|
||||
console_handler.setFormatter(console_formatter)
|
||||
|
||||
|
||||
def qt_message_handler(msg_type, context, msg):
|
||||
def qt_message_handler(msg_type, context, # pylint: disable=too-many-branches
|
||||
msg):
|
||||
"""Qt message handler to redirect qWarning etc. to the logging system.
|
||||
|
||||
Args:
|
||||
@@ -421,6 +422,9 @@ def qt_message_handler(msg_type, context, msg):
|
||||
'with: -9805', # flake8: disable=E131
|
||||
]
|
||||
|
||||
if not msg:
|
||||
msg = "Logged empty message!"
|
||||
|
||||
if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
|
||||
@@ -209,3 +209,9 @@ Feature: Special qute:// pages
|
||||
Scenario: Open qute://version
|
||||
When I open qute://version
|
||||
Then the page should contain the plaintext "Version info"
|
||||
|
||||
# qute://gpl
|
||||
|
||||
Scenario: Open qute://gpl
|
||||
When I open qute://gpl
|
||||
Then the page should contain the plaintext "GNU GENERAL PUBLIC LICENSE"
|
||||
|
||||
@@ -141,6 +141,12 @@ def is_ignored_chromium_message(line):
|
||||
# channel message
|
||||
'Invalid node channel message',
|
||||
|
||||
# Qt 5.9.3
|
||||
# [30217:30229:1124/141512.682110:ERROR:
|
||||
# cert_verify_proc_openssl.cc(212)]
|
||||
# X509 Verification error self signed certificate : 18 : 0 : 4
|
||||
'X509 Verification error self signed certificate : 18 : 0 : 4',
|
||||
|
||||
# Not reproducible anymore?
|
||||
|
||||
'Running without the SUID sandbox! *',
|
||||
|
||||
@@ -285,6 +285,12 @@ class TestKeyConfig:
|
||||
key_config_stub.unbind('a')
|
||||
assert key_config_stub.get_command('a', mode='normal') is None
|
||||
|
||||
def test_empty_command(self, key_config_stub):
|
||||
"""Try binding a key to an empty command."""
|
||||
message = "Can't add binding 'x' with empty command in normal mode"
|
||||
with pytest.raises(configexc.KeybindingError, match=message):
|
||||
key_config_stub.bind('x', ' ', mode='normal')
|
||||
|
||||
|
||||
class TestConfig:
|
||||
|
||||
|
||||
@@ -406,6 +406,11 @@ class TestWritePy:
|
||||
lines = confpy.read_text('utf-8').splitlines()
|
||||
assert '# Autogenerated config.py' in lines
|
||||
|
||||
def test_oserror(self, commands, tmpdir):
|
||||
"""Test writing to a directory which does not exist."""
|
||||
with pytest.raises(cmdexc.CommandError):
|
||||
commands.config_write_py(str(tmpdir / 'foo' / 'config.py'))
|
||||
|
||||
|
||||
class TestBind:
|
||||
|
||||
|
||||
@@ -25,12 +25,15 @@ import itertools
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import attr
|
||||
import pytest
|
||||
import pytest_catchlog
|
||||
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.misc import utilcmds
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def restore_loggers():
|
||||
@@ -252,3 +255,22 @@ def test_ignore_py_warnings(caplog):
|
||||
assert len(caplog.records) == 1
|
||||
msg = caplog.records[0].message.splitlines()[0]
|
||||
assert msg.endswith("UserWarning: not hidden")
|
||||
|
||||
|
||||
class TestQtMessageHandler:
|
||||
|
||||
@attr.s
|
||||
class Context:
|
||||
|
||||
"""Fake QMessageLogContext."""
|
||||
|
||||
function = attr.ib(default=None)
|
||||
category = attr.ib(default=None)
|
||||
file = attr.ib(default=None)
|
||||
line = attr.ib(default=None)
|
||||
|
||||
def test_empty_message(self, caplog):
|
||||
"""Make sure there's no crash with an empty message."""
|
||||
log.qt_message_handler(QtCore.QtDebugMsg, self.Context(), "")
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].msg == "Logged empty message!"
|
||||
|
||||
8
tox.ini
8
tox.ini
@@ -86,7 +86,7 @@ setenv =
|
||||
passenv = {[testenv]passenv}
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
PyQt5==5.9
|
||||
PyQt5==5.9.2
|
||||
commands = {envpython} -bb -m pytest {posargs:tests}
|
||||
|
||||
[testenv:py36-pyqt59]
|
||||
@@ -97,7 +97,7 @@ setenv =
|
||||
passenv = {[testenv]passenv}
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
PyQt5==5.9
|
||||
PyQt5==5.9.2
|
||||
commands = {envpython} -bb -m pytest {posargs:tests}
|
||||
|
||||
# test envs with coverage
|
||||
@@ -110,7 +110,7 @@ setenv =
|
||||
passenv = {[testenv]passenv}
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
PyQt5==5.9
|
||||
PyQt5==5.9.2
|
||||
commands =
|
||||
{envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
||||
{envpython} scripts/dev/check_coverage.py {posargs}
|
||||
@@ -123,7 +123,7 @@ setenv =
|
||||
passenv = {[testenv]passenv}
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
PyQt5==5.9
|
||||
PyQt5==5.9.2
|
||||
commands =
|
||||
{envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
||||
{envpython} scripts/dev/check_coverage.py {posargs}
|
||||
|
||||
Reference in New Issue
Block a user