Compare commits

...

24 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
Florian Bruhin
356eb7e5e7 Release v0.6.1 2016-04-10 21:13:33 +02:00
Florian Bruhin
bdd2afa1a2 Make sure the cheatsheet PNG is included in sdist
Fixes #1403
2016-04-10 21:01:41 +02:00
Florian Bruhin
e02ff26d0e Fix cheatsheet link URL in quickstart 2016-04-10 21:01:41 +02:00
Florian Bruhin
128fb2826a Add missing file 2016-04-10 20:35:07 +02:00
Florian Bruhin
3521ee16e4 Fix downloading of non-ascii files with LC_ALL=C
Fixes #908.
2016-04-10 20:35:07 +02:00
Florian Bruhin
af5cb36591 Rename test_cmdline_args to test_invocations 2016-04-10 20:35:07 +02:00
Florian Bruhin
8aaae5b78c Fix test_last_window_session_none 2016-04-10 18:23:10 +02:00
Florian Bruhin
ced87b163f Don't crash if data is None while saving session
Under some circumstances I can't reproduce (switching/turning off
monitors?) it seems it's possible that SessionManager.save gets called
with last_window=True, without on_last_window_closed being called.

This might be to one of the Qt screen management bugs fixed in Qt 5.6,
which would explain why I can't reproduce it.

Instead of crashing, let's log the error and not save the session.
2016-04-10 18:03:29 +02:00
Florian Bruhin
c5459abb65 Add some more logging for standarddir 2016-04-10 18:03:29 +02:00
Florian Bruhin
83b7c0dd6f Fix #1414 with a weird workaround 2016-04-10 18:03:29 +02:00
Florian Bruhin
93fff9a69c freeze: Add pkg_resources._vendor.six
Another package added by pkg_resources and not picked up by cx_Freeze...
2016-04-10 18:03:29 +02:00
Florian Bruhin
b2247fa406 tox: Update setuptools in cxfreeze-windows env
In 43a4a6a3e7 we added
pkg_resources._vendor.pyparsing to the frozen modules - however, older
setuptools (and thus pkg_resources) versions don't have that module yet,
so freezing would fail.
2016-04-10 18:03:29 +02:00
Florian Bruhin
fafba9af3f build_release: Don't call update_3rdparty.main
main tries to parse sys.argv which will fail when called from
build_release when e.g. --asciidoc is given.
2016-04-10 18:03:29 +02:00
37 changed files with 286 additions and 90 deletions

View File

@@ -14,6 +14,31 @@ 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
~~~~~~
- Fixed broken cheatsheet image which was missing from package
- Fixed occasional crash when switching/disconnecting monitors
- Fixed crash when downloading non-ascii files with a broken locale (`LC_ALL=C`)
- Added workaround for a Qt/PyQt bug which is too weird to describe here
v0.6.0
------

View File

@@ -1,8 +1,8 @@
recursive-include qutebrowser *.py
recursive-include qutebrowser/html *.html
recursive-include qutebrowser/img *.svg *.png
recursive-include qutebrowser/test *.py
recursive-include qutebrowser/javascript *.js
graft qutebrowser/html
graft qutebrowser/3rdparty
graft icons
graft doc/img

View File

@@ -8,7 +8,7 @@ time, use the `:help` command.
What to do now
--------------
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
* View the link:http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
to make yourself familiar with the key bindings: +
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
* Run `:adblock-update` to download adblock lists and activate adblocking.

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, 0)
__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

@@ -104,6 +104,11 @@ def create_full_filename(basename, filename):
Return:
The full absolute path, or None if filename creation was not possible.
"""
# Remove chars which can't be encoded in the filename encoding.
# See https://github.com/The-Compiler/qutebrowser/issues/427
encoding = sys.getfilesystemencoding()
filename = utils.force_encoding(filename, encoding)
basename = utils.force_encoding(basename, encoding)
if os.path.isabs(filename) and os.path.isdir(filename):
# We got an absolute directory from the user, so we save it under
# the default filename in that directory.
@@ -226,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.
@@ -523,10 +528,6 @@ class DownloadItem(QObject):
"existing: {}, fileobj {}".format(
filename, self._filename, self.fileobj))
filename = os.path.expanduser(filename)
# Remove chars which can't be encoded in the filename encoding.
# See https://github.com/The-Compiler/qutebrowser/issues/427
encoding = sys.getfilesystemencoding()
filename = utils.force_encoding(filename, encoding)
self._filename = create_full_filename(self.basename, filename)
if self._filename is None:
# We only got a filename (without directory) or a relative path
@@ -649,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

@@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QApplication, QStyleFactory
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
@@ -352,9 +352,14 @@ class WebView(QWebView):
frame = self.page().mainFrame()
frame.javaScriptWindowObjectCleared.connect(self.add_js_bridge)
@pyqtSlot()
def add_js_bridge(self):
"""Add the javascript bridge for qute:... pages."""
frame = self.sender()
if not isinstance(frame, QWebFrame):
log.webview.error("Got non-QWebFrame in add_js_bridge")
return
if frame.url().scheme() == 'qute':
bridge = objreg.get('js-bridge')
frame.addToJavaScriptWindowObject('qute', bridge)

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

@@ -241,7 +241,9 @@ class SessionManager(QObject):
log.sessions.debug("Saving session {} to {}...".format(name, path))
if last_window:
data = self._last_window_session
assert data is not None
if data is None:
log.sessions.error("last_window_session is None while saving!")
return
else:
data = self._save_all()
log.sessions.vdebug("Saving data: {}".format(data))

View File

@@ -25,7 +25,7 @@ import os.path
from PyQt5.QtCore import QCoreApplication, QStandardPaths
from qutebrowser.utils import log, qtutils
from qutebrowser.utils import log, qtutils, debug
# The argparse namespace passed to init()
@@ -124,6 +124,8 @@ def _writable_location(typ):
"""Wrapper around QStandardPaths.writableLocation."""
with qtutils.unset_organization():
path = QStandardPaths.writableLocation(typ)
typ_str = debug.qenum_key(QStandardPaths, typ)
log.misc.debug("writable location for {}: {}".format(typ_str, path))
if not path:
raise ValueError("QStandardPaths returned an empty value!")
# Qt seems to use '/' as path separator even on Windows...

View File

@@ -91,7 +91,7 @@ def smoke_test(executable):
def build_windows():
"""Build windows executables/setups."""
utils.print_title("Updating 3rdparty content")
update_3rdparty.main()
update_3rdparty.update_pdfjs()
utils.print_title("Building Windows binaries")
parts = str(sys.version_info.major), str(sys.version_info.minor)

View File

@@ -87,7 +87,8 @@ def get_build_exe_options(skip_html=False):
'includes': [],
'excludes': ['tkinter'],
'packages': ['pygments', 'pkg_resources._vendor.packaging',
'pkg_resources._vendor.pyparsing'],
'pkg_resources._vendor.pyparsing',
'pkg_resources._vendor.six'],
}

Binary file not shown.

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

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Test starting qutebrowser with various commandline arguments."""
"""Test starting qutebrowser with special arguments/environments."""
import pytest
@@ -51,3 +51,23 @@ def test_no_config(tmpdir, quteproc_new):
quteproc_new.start(args, env=env)
quteproc_new.send_cmd(':quit')
quteproc_new.wait_for_quit()
@pytest.mark.linux
def test_ascii_locale(httpbin, tmpdir, quteproc_new):
"""Test downloads with LC_ALL=C set.
https://github.com/The-Compiler/qutebrowser/issues/908
"""
args = ['--debug', '--no-err-windows', '--temp-basedir', 'about:blank']
quteproc_new.start(args, env={'LC_ALL': 'C'})
quteproc_new.set_setting('storage', 'download-directory', str(tmpdir))
quteproc_new.set_setting('storage', 'prompt-download-directory', 'false')
url = 'http://localhost:{port}/data/downloads/ä-issue908.bin'.format(
port=httpbin.port)
quteproc_new.send_cmd(':download {}'.format(url))
quteproc_new.send_cmd(':quit')
quteproc_new.wait_for_quit()
assert len(tmpdir.listdir()) == 1
assert (tmpdir / '?-issue908.bin').exists()

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.0'
assert qutebrowser.__version__ == '0.6.2'
@pytest.mark.parametrize('filename',
os.listdir(os.path.join(os.path.dirname(__file__), 'old_configs')),

View File

@@ -447,10 +447,14 @@ class TestSave:
sess_man.save(str(session_path))
assert 'session' not in state_config['general']
def test_last_window_session_none(self, sess_man, tmpdir):
def test_last_window_session_none(self, caplog, sess_man, tmpdir):
session_path = tmpdir / 'foo.yml'
with pytest.raises(AssertionError):
with caplog.at_level(logging.ERROR):
sess_man.save(str(session_path), last_window=True)
assert len(caplog.records) == 1
msg = "last_window_session is None while saving!"
assert caplog.records[0].msg == msg
assert not session_path.exists()
def test_last_window_session(self, sess_man, tmpdir):

View File

@@ -215,6 +215,7 @@ deps =
-r{toxinidir}/requirements.txt
cx_Freeze==4.3.4
commands =
{envpython} -m pip install -U setuptools
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/dev/freeze.py {posargs}