Compare commits

...

11 Commits

Author SHA1 Message Date
Florian Bruhin
293e322905 Release v0.6.2 2016-04-30 15:05:19 +02:00
Florian Bruhin
354bd5d606 Update changelog for v0.6.2 2016-04-30 14:50:04 +02:00
Florian Bruhin
8e619fa74e Fix dictionary hints crash 2016-04-30 14:44:25 +02:00
Florian Bruhin
5b944fb272 Add @pyqtSlot for qApp.focusChanged slot 2016-04-30 14:43:11 +02:00
Daniel Schadt
36d2fc4b92 cache: fix crash when cache_dir is None
Issue #1412

When passing --cachedir="" on the command line, standarddir.cache()
returns None, which stands for "deactivate cache" and has to be
properly handled in DiskCache.__init__() (i.e. don't pass it to
os.path.join)
2016-04-30 14:42:52 +02:00
Florian Bruhin
26f4acb10a Split IPCServer.on_ready_read into two methods 2016-04-30 14:42:44 +02:00
Florian Bruhin
991277e9de Work around PyQt 5.6 segfault when using IPC
PyQt 5.6 seems to segfault when emitting None with a signal which is
declared as emitting a string:

https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html

We now avoid this by using an empty string explicitly instead of None.
2016-04-30 14:42:17 +02:00
Florian Bruhin
133e959ecc Remove @pyqtSlot for on_new_window
This worked fine with Python 3.5 but causes a circular import which is
hard to break with Python 3.4.

The original solution was to do @pyqtSlot(object), but that doesn't work
with PyQt 5.6 anymore...
2016-04-30 14:42:17 +02:00
Florian Bruhin
be1630f7d0 Fix types in @pyqtSlot decorations
PyQt 5.5 enforces correct type signatures, and there were a lot of
places where we were simply wrong, causing qutebrowser to not start at
all...
2016-04-30 14:42:00 +02:00
Ryan Roden-Corrent
c57bf8701e Don't crash when undoing twice on default page.
Avoid a crash when undoing twice on the default page with last-close set to
default-page.
This was caused by logic to reuse the current tab if it is on the default page
and has no history. The fix is using openurl rather than removeTab/tabopen.
2016-04-30 14:41:48 +02:00
Florian Bruhin
ea2ae94cd0 Fix crash with :tab-{prev,next,focus} with 0 tabs
When using :tab-prev/:tab-next (or :tab-focus which uses :tab-next
internally) immediately after the last tab, those functions could be
called with 0 tabs open, which caused a ZeroDivisionError when trying to
do % 0.

Fixes #1448.
2016-04-30 14:41:26 +02:00
27 changed files with 227 additions and 78 deletions

View File

@@ -14,8 +14,22 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v0.6.2
------
Fixed
~~~~~
- Fixed crash when using `:tab-{prev,next,focus}` right after closing the last
tab with `last-close` set to `close`.
- Fixed crash when doing `:undo` in a new instance with `tabs -> last-close` set
to `default-page`.
- Fixed crash when starting with --cachedir=""
- Fixed crash in some circumstances when using dictionary hints
- Fixed various crashes related to PyQt 5.6
v0.6.1
-----
------
Fixed
~~~~~~

View File

@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (0, 6, 1)
__version_info__ = (0, 6, 2)
__version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."

View File

@@ -244,12 +244,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
cwd: The cwd to use for fuzzy_url.
target_arg: Command line argument received by a running instance via
ipc. If the --target argument was not specified, target_arg
will be an empty string instead of None. This behavior is
caused by the PyQt signal
``got_args = pyqtSignal(list, str, str)``
used in the misc.ipc.IPCServer class. PyQt converts the
None value into a null QString and then back to an empty
python string
will be an empty string.
"""
if via_ipc and not args:
win_id = mainwindow.get_window(via_ipc, force_window=True)
@@ -275,6 +270,8 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
log.init.debug("Startup URL {}".format(cmd))
if not cwd: # could also be an empty string due to the PyQt signal
cwd = None
try:
url = urlutils.fuzzy_url(cmd, cwd, relative=True)
except urlutils.InvalidUrlError as e:
@@ -342,6 +339,7 @@ def _save_version():
state_config['general']['version'] = qutebrowser.__version__
@pyqtSlot('QWidget*', 'QWidget*')
def on_focus_changed(_old, new):
"""Register currently focused main window in the object registry."""
if new is None:

View File

@@ -32,16 +32,23 @@ class DiskCache(QNetworkDiskCache):
"""Disk cache which sets correct cache dir and size.
If the cache is deactivated via the command line argument --cachedir="",
both attributes _cache_dir and _http_cache_dir are set to None.
Attributes:
_activated: Whether the cache should be used.
_cache_dir: The base directory for cache files (standarddir.cache())
_http_cache_dir: the HTTP subfolder in _cache_dir.
_cache_dir: The base directory for cache files (standarddir.cache()) or
None.
_http_cache_dir: the HTTP subfolder in _cache_dir or None.
"""
def __init__(self, cache_dir, parent=None):
super().__init__(parent)
self._cache_dir = cache_dir
self._http_cache_dir = os.path.join(cache_dir, 'http')
if cache_dir is None:
self._http_cache_dir = None
else:
self._http_cache_dir = os.path.join(cache_dir, 'http')
self._maybe_activate()
objreg.get('config').changed.connect(self.on_config_changed)

View File

@@ -774,6 +774,10 @@ class CommandDispatcher:
Args:
count: How many tabs to switch back.
"""
if self._count() == 0:
# Running :tab-prev after last tab was closed
# See https://github.com/The-Compiler/qutebrowser/issues/1448
return
newidx = self._current_index() - count
if newidx >= 0:
self._set_current_index(newidx)
@@ -790,6 +794,10 @@ class CommandDispatcher:
Args:
count: How many tabs to switch forward.
"""
if self._count() == 0:
# Running :tab-next after last tab was closed
# See https://github.com/The-Compiler/qutebrowser/issues/1448
return
newidx = self._current_index() + count
if newidx < self._count():
self._set_current_index(newidx)

View File

@@ -231,7 +231,7 @@ class DownloadItemStats(QObject):
else:
return remaining_bytes / avg
@pyqtSlot(int, int)
@pyqtSlot('qint64', 'qint64')
def on_download_progress(self, bytes_done, bytes_total):
"""Update local variables when the download progress changed.
@@ -650,7 +650,7 @@ class DownloadItem(QObject):
except OSError as e:
self._die(e.strerror)
@pyqtSlot(int)
@pyqtSlot('QNetworkReply::NetworkError')
def on_reply_error(self, code):
"""Handle QNetworkReply errors."""
if code == QNetworkReply.OperationCanceledError:

View File

@@ -997,11 +997,14 @@ class WordHinter:
def __init__(self):
# will be initialized on first use.
self.words = set()
self.dictionary = None
def ensure_initialized(self):
"""Generate the used words if yet uninialized."""
if not self.words:
dictionary = config.get("hints", "dictionary")
dictionary = config.get("hints", "dictionary")
if not self.words or self.dictionary != dictionary:
self.words.clear()
self.dictionary = dictionary
try:
with open(dictionary, encoding="UTF-8") as wordfile:
alphabet = set(string.ascii_lowercase)
@@ -1061,12 +1064,17 @@ class WordHinter:
return any(hint.startswith(e) or e.startswith(hint)
for e in existing)
def new_hint_for(self, elem, existing):
def filter_prefixes(self, hints, existing):
return (h for h in hints if not self.any_prefix(h, existing))
def new_hint_for(self, elem, existing, fallback):
"""Return a hint for elem, not conflicting with the existing."""
new = self.tag_words_to_hints(self.extract_tag_words(elem))
no_prefixes = (h for h in new if not self.any_prefix(h, existing))
new_no_prefixes = self.filter_prefixes(new, existing)
fallback_no_prefixes = self.filter_prefixes(fallback, existing)
# either the first good, or None
return next(no_prefixes, None)
return (next(new_no_prefixes, None) or
next(fallback_no_prefixes, None))
def hint(self, elems):
"""Produce hint labels based on the html tags.
@@ -1086,7 +1094,9 @@ class WordHinter:
used_hints = set()
words = iter(self.words)
for elem in elems:
hint = self.new_hint_for(elem, used_hints) or next(words)
hint = self.new_hint_for(elem, used_hints, words)
if not hint:
raise WordHintingError("Not enough words in the dictionary.")
used_hints.add(hint)
hints.append(hint)
return hints

View File

@@ -253,7 +253,7 @@ class NetworkManager(QNetworkAccessManager):
except KeyError:
pass
@pyqtSlot('QNetworkReply', 'QAuthenticator')
@pyqtSlot('QNetworkReply*', 'QAuthenticator*')
def on_authentication_required(self, reply, authenticator):
"""Called when a website needs authentication."""
user, password = None, None
@@ -286,7 +286,7 @@ class NetworkManager(QNetworkAccessManager):
authenticator.setUser(user)
authenticator.setPassword(password)
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
@pyqtSlot('QNetworkProxy', 'QAuthenticator*')
def on_proxy_authentication_required(self, proxy, authenticator):
"""Called when a proxy needs authentication."""
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())

View File

@@ -288,7 +288,7 @@ class BrowserPage(QWebPage):
window=self._win_id)
download_manager.get_request(req, page=self)
@pyqtSlot('QNetworkReply')
@pyqtSlot('QNetworkReply*')
def on_unsupported_content(self, reply):
"""Handle an unsupportedContent signal.
@@ -334,7 +334,7 @@ class BrowserPage(QWebPage):
else:
self.error_occurred = False
@pyqtSlot('QWebFrame', 'QWebPage::Feature')
@pyqtSlot('QWebFrame*', 'QWebPage::Feature')
def on_feature_permission_requested(self, frame, feature):
"""Ask the user for approval for geolocation/notifications."""
options = {
@@ -439,7 +439,7 @@ class BrowserPage(QWebPage):
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
frame.setScrollPosition(data['scroll-pos'])
@pyqtSlot(str)
@pyqtSlot(usertypes.ClickTarget)
def on_start_hinting(self, hint_target):
"""Emitted before a hinting-click takes place.

View File

@@ -352,7 +352,7 @@ class WebView(QWebView):
frame = self.page().mainFrame()
frame.javaScriptWindowObjectCleared.connect(self.add_js_bridge)
@pyqtSlot(QWebFrame)
@pyqtSlot()
def add_js_bridge(self):
"""Add the javascript bridge for qute:... pages."""
frame = self.sender()

View File

@@ -249,6 +249,7 @@ class CommandRunner(QObject):
result.cmd.run(self._win_id, args)
@pyqtSlot(str, int)
@pyqtSlot(str)
def run_safely(self, text, count=None):
"""Run a command and display exceptions in the statusbar."""
try:

View File

@@ -175,9 +175,6 @@ class TabCompletionModel(base.BaseCompletionModel):
objreg.get("app").new_window.connect(self.on_new_window)
self.rebuild()
# slot argument should be mainwindow.MainWindow but can't import
# that at module level because of import loops.
@pyqtSlot(object)
def on_new_window(self, window):
"""Add hooks to new windows."""
window.tabbed_browser.new_tab.connect(self.on_new_tab)

View File

@@ -95,6 +95,7 @@ class change_filter: # pylint: disable=invalid-name
"""
if self._function:
@pyqtSlot(str, str)
@pyqtSlot()
@functools.wraps(func)
def wrapper(sectname=None, optname=None):
if sectname is None and optname is None:
@@ -108,6 +109,7 @@ class change_filter: # pylint: disable=invalid-name
return func()
else:
@pyqtSlot(str, str)
@pyqtSlot()
@functools.wraps(func)
def wrapper(wrapper_self, sectname=None, optname=None):
if sectname is None and optname is None:

View File

@@ -22,6 +22,7 @@
from PyQt5.QtCore import pyqtSlot
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.browser import webview
class Percentage(textbase.TextBase):
@@ -48,7 +49,7 @@ class Percentage(textbase.TextBase):
else:
self.setText('[{:2}%]'.format(y))
@pyqtSlot(object)
@pyqtSlot(webview.WebView)
def on_tab_changed(self, tab):
"""Update scroll position when tab changed."""
self.set_perc(*tab.scroll_pos)

View File

@@ -59,7 +59,7 @@ class Progress(QProgressBar):
self.setValue(0)
self.show()
@pyqtSlot(int)
@pyqtSlot(webview.WebView)
def on_tab_changed(self, tab):
"""Set the correct value when the current tab changed."""
if self is None: # pragma: no branch

View File

@@ -24,6 +24,7 @@ from PyQt5.QtCore import pyqtSlot
from qutebrowser.config import config
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.utils import usertypes, log, objreg
from qutebrowser.browser import webview
class Text(textbase.TextBase):
@@ -98,7 +99,7 @@ class Text(textbase.TextBase):
"""Clear jstext when page loading started."""
self._jstext = ''
@pyqtSlot(int)
@pyqtSlot(webview.WebView)
def on_tab_changed(self, tab):
"""Set the correct jstext when the current tab changed."""
self._jstext = tab.statusbar_message

View File

@@ -158,7 +158,7 @@ class UrlText(textbase.TextBase):
self._hover_url = None
self._update_url()
@pyqtSlot(int)
@pyqtSlot(webview.WebView)
def on_tab_changed(self, tab):
"""Update URL if the tab changed."""
self._hover_url = None

View File

@@ -305,6 +305,7 @@ class TabbedBrowser(tabwidget.TabWidget):
"""Undo removing of a tab."""
# Remove unused tab which may be created after the last tab is closed
last_close = config.get('tabs', 'last-close')
use_current_tab = False
if last_close in ['blank', 'startpage', 'default-page']:
only_one_tab_open = self.count() == 1
no_history = self.widget(0).history().count() == 1
@@ -317,12 +318,17 @@ class TabbedBrowser(tabwidget.TabWidget):
last_close_urlstr = urls[last_close].toString().rstrip('/')
first_tab_urlstr = first_tab_url.toString().rstrip('/')
last_close_url_used = first_tab_urlstr == last_close_urlstr
if only_one_tab_open and no_history and last_close_url_used:
self.removeTab(0)
use_current_tab = (only_one_tab_open and no_history and
last_close_url_used)
url, history_data = self._undo_stack.pop()
newtab = self.tabopen(url, background=False)
if use_current_tab:
self.openurl(url, newtab=False)
newtab = self.widget(0)
else:
newtab = self.tabopen(url, background=False)
qtutils.deserialize(history_data, newtab.history())
@pyqtSlot('QUrl', bool)

View File

@@ -221,7 +221,7 @@ class IPCServer(QObject):
# This means we only use setSocketOption on Windows...
os.chmod(self._server.fullServerName(), 0o700)
@pyqtSlot(int)
@pyqtSlot('QLocalSocket::LocalSocketError')
def on_error(self, err):
"""Raise SocketError on fatal errors."""
if self._socket is None:
@@ -288,6 +288,55 @@ class IPCServer(QObject):
self._socket.error.connect(self.on_error)
self._socket.disconnectFromServer()
def _handle_data(self, data):
"""Handle data (as bytes) we got from on_ready_ready_read."""
try:
decoded = data.decode('utf-8')
except UnicodeDecodeError:
log.ipc.error("invalid utf-8: {}".format(
binascii.hexlify(data)))
self._handle_invalid_data()
return
log.ipc.debug("Processing: {}".format(decoded))
try:
json_data = json.loads(decoded)
except ValueError:
log.ipc.error("invalid json: {}".format(decoded.strip()))
self._handle_invalid_data()
return
for name in ('args', 'target_arg'):
if name not in json_data:
log.ipc.error("Missing {}: {}".format(name, decoded.strip()))
self._handle_invalid_data()
return
try:
protocol_version = int(json_data['protocol_version'])
except (KeyError, ValueError):
log.ipc.error("invalid version: {}".format(decoded.strip()))
self._handle_invalid_data()
return
if protocol_version != PROTOCOL_VERSION:
log.ipc.error("incompatible version: expected {}, got {}".format(
PROTOCOL_VERSION, protocol_version))
self._handle_invalid_data()
return
args = json_data['args']
target_arg = json_data['target_arg']
if target_arg is None:
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html
target_arg = ''
cwd = json_data.get('cwd', '')
assert cwd is not None
self.got_args.emit(args, target_arg, cwd)
@pyqtSlot()
def on_ready_read(self):
"""Read json data from the client."""
@@ -302,46 +351,7 @@ class IPCServer(QObject):
self.got_raw.emit(data)
log.ipc.debug("Read from socket 0x{:x}: {}".format(
id(self._socket), data))
try:
decoded = data.decode('utf-8')
except UnicodeDecodeError:
log.ipc.error("invalid utf-8: {}".format(
binascii.hexlify(data)))
self._handle_invalid_data()
return
log.ipc.debug("Processing: {}".format(decoded))
try:
json_data = json.loads(decoded)
except ValueError:
log.ipc.error("invalid json: {}".format(decoded.strip()))
self._handle_invalid_data()
return
for name in ('args', 'target_arg'):
if name not in json_data:
log.ipc.error("Missing {}: {}".format(name,
decoded.strip()))
self._handle_invalid_data()
return
try:
protocol_version = int(json_data['protocol_version'])
except (KeyError, ValueError):
log.ipc.error("invalid version: {}".format(decoded.strip()))
self._handle_invalid_data()
return
if protocol_version != PROTOCOL_VERSION:
log.ipc.error("incompatible version: expected {}, "
"got {}".format(
PROTOCOL_VERSION, protocol_version))
self._handle_invalid_data()
return
cwd = json_data.get('cwd', None)
self.got_args.emit(json_data['args'], json_data['target_arg'], cwd)
self._handle_data(data)
self._timer.start()
@pyqtSlot()

View File

@@ -0,0 +1 @@
hinting

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Let's Hint some words</title>
</head>
<body>
<h1>Word hints</h1>
<h2>Smart hints</h2>
<p>In qutebrowser, urls can not only be hinted with letters and
numbers, but also with <a href="../words.txt">words</a>. When there is
a sensible url text available, qutebrowser will even use that
text to create a <a href="../smart.txt">smart</a> hint.</p>
<h2>Filled hints</h2>
<p>When no smart hints are available, because the hint text is
<a href="../l33t.txt">too</a> short or <a href="../l33t.txt">l33t</a> to
use, words from a dictionary will be used.</p>
<h2>Hint conflicts</h2>
<p>Of course, hints have to be unique. For instance, all hints
below should get a different hint, whether they're smart or
not:</p>
<ul>
<li><a href="../hinting.txt">hinting</a> should be a smart hint</li>
<li><a href="../l33t.txt">word</a> is a prefix of words</li>
<li><a href="../l33t.txt">3</a> is too 1337</li>
<li><a href="../l33t.txt">4</a> is too 1337</li>
</ul>
</body>
</html>

View File

@@ -0,0 +1 @@
l33t

View File

@@ -0,0 +1 @@
smart

View File

@@ -0,0 +1 @@
words

View File

@@ -607,6 +607,16 @@ Feature: Tab management
Then the following tabs should be open:
- data/hello.txt (active)
Scenario: Double-undo with single tab on last-close default page
Given I have a fresh instance
When I open about:blank
And I set tabs -> last-close to default-page
And I set general -> default-page to about:blank
And I run :undo
Then the error "Nothing to undo!" should be shown
And the following tabs should be open:
- about:blank (active)
# last-close
Scenario: last-close = blank
@@ -822,3 +832,17 @@ Feature: Tab management
When I open data/title.html
And I run :buffer "1/2/3"
Then the error "No matching tab for: 1/2/3" should be shown
Scenario: Using :tab-next after closing last tab (#1448)
When I set tabs -> last-close to close
And I run :tab-only
And I run :tab-close ;; :tab-next
Then qutebrowser should quit
And no crash should happen
Scenario: Using :tab-prev after closing last tab (#1448)
When I set tabs -> last-close to close
And I run :tab-only
And I run :tab-close ;; :tab-prev
Then qutebrowser should quit
And no crash should happen

View File

@@ -1,3 +1,4 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
@@ -25,6 +26,7 @@ import os.path
import yaml
import pytest
import bs4
import textwrap
def collect_tests():
@@ -54,3 +56,35 @@ def test_hints(test_name, quteproc):
quteproc.wait_for(message='hints: a', category='hints')
quteproc.send_cmd(':follow-hint a')
quteproc.wait_for_load_finished('data/' + parsed['target'])
def test_word_hints_issue1393(quteproc, tmpdir):
dict_file = tmpdir / 'dict'
dict_file.write(textwrap.dedent("""
alph
beta
gamm
delt
epsi
"""))
targets = [
('words', 'words.txt'),
('smart', 'smart.txt'),
('hinting', 'hinting.txt'),
('alph', 'l33t.txt'),
('beta', 'l33t.txt'),
('gamm', 'l33t.txt'),
('delt', 'l33t.txt'),
('epsi', 'l33t.txt'),
]
quteproc.set_setting('hints', 'mode', 'word')
quteproc.set_setting('hints', 'dictionary', str(dict_file))
for hint, target in targets:
quteproc.open_path('data/hints/issue1393.html')
quteproc.wait_for_load_finished('data/hints/issue1393.html')
quteproc.send_cmd(':hint')
quteproc.wait_for(message='hints: *', category='hints')
quteproc.send_cmd(':follow-hint {}'.format(hint))
quteproc.wait_for_load_finished('data/{}'.format(target))

View File

@@ -321,7 +321,7 @@ class TestDefaultConfig:
If it did change, place a new qutebrowser-vx.y.z.conf in old_configs
and then increment the version.
"""
assert qutebrowser.__version__ == '0.6.1'
assert qutebrowser.__version__ == '0.6.2'
@pytest.mark.parametrize('filename',
os.listdir(os.path.join(os.path.dirname(__file__), 'old_configs')),