From 8d15bbdded8eece4c90eed6fe9a08fec02d37e7a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 24 May 2015 21:00:46 +0200 Subject: [PATCH 01/30] utils.version: Add SIP line on ImportError. --- qutebrowser/utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 6823685d7..827762af4 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -130,7 +130,7 @@ def _module_versions(): try: import sipconfig # pylint: disable=import-error,unused-variable except ImportError: - pass + lines.append('SIP: ?') else: try: lines.append('SIP: {}'.format( From 120d2e12b058659075ab11536d96a981682a09fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 01:21:57 +0200 Subject: [PATCH 02/30] Improve QtValueError wording for ensure_not_null. --- qutebrowser/utils/qtutils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 939e1ed81..f175dc202 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -131,7 +131,7 @@ def ensure_valid(obj): def ensure_not_null(obj): """Ensure a Qt object with an .isNull() method is not null.""" if obj.isNull(): - raise QtValueError(obj) + raise QtValueError(obj, null=True) def check_qdatastream(stream): @@ -322,12 +322,15 @@ class QtValueError(ValueError): """Exception which gets raised by ensure_valid.""" - def __init__(self, obj): + def __init__(self, obj, null=False): try: self.reason = obj.errorString() except AttributeError: self.reason = None - err = "{} is not valid".format(obj) + if null: + err = "{} is null".format(obj) + else: + err = "{} is not valid".format(obj) if self.reason: err += ": {}".format(self.reason) super().__init__(err) From 0f13d9325b59ff7ed60b564e30532f8bce7b33a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 01:26:52 +0200 Subject: [PATCH 03/30] Don't use parametrization for deprecated keys. This showed up as 2400 tests for what basically is one. --- tests/config/test_config.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 123b2a412..d5fab2ed1 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -157,14 +157,6 @@ class TestConfigParser: self.cfg.get('general', 'bar') # pylint: disable=bad-config-call -def keyconfig_deprecated_test_cases(): - """Generator yielding test cases (command, rgx) for TestKeyConfigParser.""" - for sect in configdata.KEY_DATA.values(): - for command in sect: - for rgx, _repl in configdata.CHANGED_KEY_COMMANDS: - yield (command, rgx) - - class TestKeyConfigParser: """Test config.parsers.keyconf.KeyConfigParser.""" @@ -185,10 +177,13 @@ class TestKeyConfigParser: with pytest.raises(keyconf.KeyConfigError): kcp._read_command(cmdline_test.cmd) - @pytest.mark.parametrize('command, rgx', keyconfig_deprecated_test_cases()) - def test_default_config_no_deprecated(self, command, rgx): + @pytest.mark.parametrize('rgx', [rgx for rgx, _repl + in configdata.CHANGED_KEY_COMMANDS]) + def test_default_config_no_deprecated(self, rgx): """Make sure the default config contains no deprecated commands.""" - assert rgx.match(command) is None + for sect in configdata.KEY_DATA.values(): + for command in sect: + assert rgx.match(command) is None @pytest.mark.parametrize( 'old, new_expected', From 6d879bbca32c51ba501400826179dac6958a29ba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 01:38:17 +0200 Subject: [PATCH 04/30] Exclude resources.py from coverage. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index ff714c43d..16bebb0cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,7 @@ branch = true omit = qutebrowser/__main__.py */__init__.py + qutebrowser/resources.py [report] exclude_lines = From a345b02729034e39f2b245592510b17307e4e1df Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 11:28:50 +0200 Subject: [PATCH 05/30] Fix exception when downloading links without name. We also set a default name to prevent "is a directory" errors. This is a regression introduced in 8f33fcfc52cf598d0aa11a347992c87010d3e37a. Fixes #682. --- CHANGELOG.asciidoc | 1 + qutebrowser/browser/downloads.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index c18927953..0af6c0d0f 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -73,6 +73,7 @@ Fixed - Various fixes for deprecated key bindings and auto-migrations. - Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug) - Fixed handling of keybindings containing Ctrl/Meta on OS X. +- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...". https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index ab055cbf4..790459f6a 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -686,8 +686,11 @@ class DownloadManager(QAbstractListModel): if fileobj is not None or filename is not None: return self.fetch_request(request, page, fileobj, filename, auto_remove, suggested_fn) - encoding = sys.getfilesystemencoding() - suggested_fn = utils.force_encoding(suggested_fn, encoding) + if suggested_fn is None: + suggested_fn = 'qutebrowser-download' + else: + encoding = sys.getfilesystemencoding() + suggested_fn = utils.force_encoding(suggested_fn, encoding) q = self._prepare_question() q.default = _path_suggestion(suggested_fn) message_bridge = objreg.get('message-bridge', scope='window', From 45dea54e3c2cacbd887ce4e0a006d92afd740e53 Mon Sep 17 00:00:00 2001 From: Tobias Patzl Date: Mon, 25 May 2015 15:23:14 +0200 Subject: [PATCH 06/30] Add setting to disable mousewheel tab switching. See #374. --- doc/help/settings.asciidoc | 12 ++++++++++++ qutebrowser/config/configdata.py | 4 ++++ qutebrowser/mainwindow/tabwidget.py | 11 +++++++++++ 3 files changed, 27 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 46539c3b7..d3c571aa3 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -111,6 +111,7 @@ |<>|Spacing between tab edge and indicator. |<>|Whether to open windows instead of tabs. |<>|The format to use for the tab title. The following placeholders are defined: +|<>|Switch between tabs using the mouse wheel. |============== .Quick reference for section ``storage'' @@ -1031,6 +1032,17 @@ The format to use for the tab title. The following placeholders are defined: Default: +pass:[{index}: {title}]+ +[[tabs-mousewheel-tab-switching]] +=== mousewheel-tab-switching +Switch between tabs using the mouse wheel. + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[true]+ + == storage Settings related to cache and storage. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index cb3a47be7..02b8c6008 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -522,6 +522,10 @@ def data(readonly=False): "* `{index}`: The index of this tab.\n" "* `{id}`: The internal tab ID of this tab."), + ('mousewheel-tab-switching', + SettingValue(typ.Bool(), 'true'), + "Switch between tabs using the mouse wheel."), + readonly=readonly )), diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index df44ebbba..1ad97370e 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -480,6 +480,17 @@ class TabBar(QTabBar): new_idx = super().insertTab(idx, icon, '') self.set_page_title(new_idx, text) + def wheelEvent(self, event): + """Override wheelEvent to make the action configurable.""" + if config.get('tabs', 'mousewheel-tab-switching'): + super().wheelEvent(event) + else: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self._win_id) + focused_tab = tabbed_browser.currentWidget() + if focused_tab is not None: + focused_tab.wheelEvent(event) + class TabBarStyle(QCommonStyle): From 61519e63839b81ee1532c8021709534f6f49e9f3 Mon Sep 17 00:00:00 2001 From: Tobias Patzl Date: Mon, 25 May 2015 20:21:37 +0200 Subject: [PATCH 07/30] move part of the logic to `TabbedBrowser` --- qutebrowser/mainwindow/tabbedbrowser.py | 9 +++++++++ qutebrowser/mainwindow/tabwidget.py | 14 ++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index c465c8ca4..7b3477018 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -577,3 +577,12 @@ class TabbedBrowser(tabwidget.TabWidget): """ super().resizeEvent(e) self.resized.emit(self.geometry()) + + def wheelEvent(self, e): + """Override wheelEvent of QWidget to forward it to the focused tab. + + Args: + e: The QWheelEvent + """ + if self._now_focused is not None: + self._now_focused.wheelEvent(e) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 1ad97370e..bbbfdf045 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -480,16 +480,18 @@ class TabBar(QTabBar): new_idx = super().insertTab(idx, icon, '') self.set_page_title(new_idx, text) - def wheelEvent(self, event): - """Override wheelEvent to make the action configurable.""" + def wheelEvent(self, e): + """Override wheelEvent to make the action configurable. + + Args: + e: The QWheelEvent + """ if config.get('tabs', 'mousewheel-tab-switching'): - super().wheelEvent(event) + super().wheelEvent(e) else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) - focused_tab = tabbed_browser.currentWidget() - if focused_tab is not None: - focused_tab.wheelEvent(event) + tabbed_browser.wheelEvent(e) class TabBarStyle(QCommonStyle): From b858b6ac755ffc6633a24a1a85c7902ce78c1edc Mon Sep 17 00:00:00 2001 From: Tobias Patzl Date: Tue, 26 May 2015 10:24:32 +0200 Subject: [PATCH 08/30] call `e.ignore()` when the event is not handled --- qutebrowser/mainwindow/tabbedbrowser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 7b3477018..ba5a5c725 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -586,3 +586,5 @@ class TabbedBrowser(tabwidget.TabWidget): """ if self._now_focused is not None: self._now_focused.wheelEvent(e) + else: + e.ignore() From 6b98c48985fd9ef4c3c6b7095b60bdd69358dd20 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 10:30:21 +0200 Subject: [PATCH 09/30] Regenerate authors. --- README.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.asciidoc b/README.asciidoc index 8742f11cb..ad7a95ff2 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -150,6 +150,7 @@ Contributors, sorted by the number of commits in descending order: * Error 800 * Brian Jackson * sbinix +* Tobias Patzl * Johannes Altmanninger * Samir Benmendil * Regina Hug From e300b2e30d0a275cd88ea4d0d2f462d40c3ef88f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 12:10:36 +0200 Subject: [PATCH 10/30] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 0af6c0d0f..2981578e6 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,6 +32,7 @@ Added - New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations. - New argument `--no-err-windows` to suppress all error windows. - New visual/caret mode (bound to `v`) to select text by keyboard. +- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar. Changed ~~~~~~~ From 27e82ce6c8a4b535babc4370fd958cd1dcd1cdb0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 15:27:29 +0200 Subject: [PATCH 11/30] Improve exception handling in qsavefile_open. Sometimes exceptions were shadowed with new exceptions because of the file flushing. --- qutebrowser/utils/qtutils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index f175dc202..af2a27a74 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -180,7 +180,7 @@ def deserialize_stream(stream, obj): def savefile_open(filename, binary=False, encoding='utf-8'): """Context manager to easily use a QSaveFile.""" f = QSaveFile(filename) - new_f = None + cancelled = False try: ok = f.open(QIODevice.WriteOnly) if not ok: @@ -192,13 +192,15 @@ def savefile_open(filename, binary=False, encoding='utf-8'): yield new_f except: f.cancelWriting() + cancelled = True raise - finally: + else: if new_f is not None: new_f.flush() + finally: commit_ok = f.commit() - if not commit_ok: - raise OSError(f.errorString()) + if not commit_ok and not cancelled: + raise OSError("Commit failed!") @contextlib.contextmanager From 92abf4bdf877f12d334c1eee1ed9e4540f1215ea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 19:25:45 +0200 Subject: [PATCH 12/30] tox: Update pytest-html to 1.3.1. Upstream changelog: 1.3.1: Fix encoding issue in Python 3 1.3: Bump version number to 1.3 Simplify example in README Show extra content in report regardless of test result Support extra content in JSON format --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cf1d596d6..9166f180c 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ deps = pytest-capturelog==0.7 pytest-qt==1.3.0 pytest-mock==0.5 - pytest-html==1.2 + pytest-html==1.3.1 # We don't use {[testenv:mkvenv]commands} here because that seems to be broken # on Ubuntu Trusty. commands = From e10da78a1a065385157ce6dbc5f6296a102fddc0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 08:06:21 +0200 Subject: [PATCH 13/30] urlutils: Remove some more dead code. --- qutebrowser/utils/urlutils.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 3ed82b0db..143e7cfc5 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -94,6 +94,8 @@ def _is_url_naive(urlstr): True if the URL really is a URL, False otherwise. """ url = qurl_from_user_input(urlstr) + assert url.isValid() + if not utils.raises(ValueError, ipaddress.ip_address, urlstr): # Valid IPv4/IPv6 address return True @@ -104,9 +106,7 @@ def _is_url_naive(urlstr): if not QHostAddress(urlstr).isNull(): return False - if not url.isValid(): - return False - elif '.' in url.host(): + if '.' in url.host(): return True else: return False @@ -122,9 +122,7 @@ def _is_url_dns(urlstr): True if the URL really is a URL, False otherwise. """ url = qurl_from_user_input(urlstr) - if not url.isValid(): - log.url.debug("Invalid URL -> False") - return False + assert url.isValid() if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and not QHostAddress(urlstr).isNull()): @@ -246,16 +244,13 @@ def is_url(urlstr): return False if not qurl_userinput.isValid(): + # This will also catch URLs containing spaces. return False if _has_explicit_scheme(qurl): # URLs with explicit schemes are always URLs log.url.debug("Contains explicit scheme") url = True - elif ' ' in urlstr: - # A URL will never contain a space - log.url.debug("Contains space -> no URL") - url = False elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'): log.url.debug("Is localhost.") url = True @@ -274,7 +269,7 @@ def is_url(urlstr): else: raise ValueError("Invalid autosearch value") log.url.debug("url = {}".format(url)) - return url and qurl_userinput.isValid() + return url def qurl_from_user_input(urlstr): From fa69786b0f2f3e338f2ed552c89ecf0075ced20a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:42:45 +0200 Subject: [PATCH 14/30] PyQIODevice: Raise ValueError when closed. --- qutebrowser/utils/qtutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index af2a27a74..4525a5c32 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -235,9 +235,9 @@ class PyQIODevice(io.BufferedIOBase): return self._dev.size() def _check_open(self): - """Check if the device is open, raise OSError if not.""" + """Check if the device is open, raise ValueError if not.""" if not self._dev.isOpen(): - raise OSError("IO operation on closed device!") + raise ValueError("IO operation on closed device!") def _check_random(self): """Check if the device supports random access, raise OSError if not.""" From ba9c782824ddf479dd5ed76594c0defa1ae88838 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:43:28 +0200 Subject: [PATCH 15/30] PyQIODevice: First attempt at fixing read(). This was completely broken because one read overload doesn't exist in PyQt and apparently it was never tested... --- qutebrowser/utils/qtutils.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 4525a5c32..841e94d87 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -311,13 +311,28 @@ class PyQIODevice(io.BufferedIOBase): raise OSError(self._dev.errorString()) return num - def read(self, size): + def read(self, size=-1): self._check_open() - buf = bytes() - num = self._dev.read(buf, size) - if num == -1: - raise OSError(self._dev.errorString()) - return num + self._check_readable() + if size == 0: + # Read no data + return b'' + elif size < 0: + # Read all data + if self._dev.bytesAvailable() > 0: + buf = self._dev.readAll() + if not buf: + raise OSError(self._dev.errorString()) + else: + return b'' + else: + if self._dev.bytesAvailable() > 0: + buf = self._dev.read(size) + if not buf: + raise OSError(self._dev.errorString()) + else: + return b'' + return buf class QtValueError(ValueError): From 35f0b26f4a5b3b17a70f918789f3dd7721d0b99d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:44:16 +0200 Subject: [PATCH 16/30] PyQIODevice: Remove readinto(). Our implementation was broken, and the BufferedIOBase mixin does a better job at doing this. --- qutebrowser/utils/qtutils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 841e94d87..98aee36de 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -300,10 +300,6 @@ class PyQIODevice(io.BufferedIOBase): def writable(self): return self._dev.isWritable() - def readinto(self, b): - self._check_open() - return self._dev.read(b, len(b)) - def write(self, b): self._check_open() num = self._dev.write(b) From b2d763f993d8b164b1ae0e72044a0c8073608492 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:52:51 +0200 Subject: [PATCH 17/30] PyQIODevice: Check if device is readable/writable. --- qutebrowser/utils/qtutils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 98aee36de..f4b737162 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -244,6 +244,16 @@ class PyQIODevice(io.BufferedIOBase): if not self.seekable(): raise OSError("Random access not allowed!") + def _check_readable(self): + """Check if the device is readable, raise OSError if not.""" + if not self._dev.isReadable(): + raise OSError("Trying to read unreadable file!") + + def _check_writable(self): + """Check if the device is writable, raise OSError if not.""" + if not self.writable(): + raise OSError("Trying to write to unwritable file!") + def fileno(self): raise io.UnsupportedOperation @@ -285,6 +295,7 @@ class PyQIODevice(io.BufferedIOBase): def readline(self, size=-1): self._check_open() + self._check_readable() if size == -1: size = 0 return self._dev.readLine(size) @@ -302,6 +313,7 @@ class PyQIODevice(io.BufferedIOBase): def write(self, b): self._check_open() + self._check_writable() num = self._dev.write(b) if num == -1 or num < len(b): raise OSError(self._dev.errorString()) From 0788054dd321403195b1b8b32609a6f22204bce3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 20:53:44 +0200 Subject: [PATCH 18/30] PyQIODevice: Expose underlying device. --- qutebrowser/utils/qtutils.py | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index f4b737162..f66dfa769 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -223,20 +223,20 @@ class PyQIODevice(io.BufferedIOBase): """Wrapper for a QIODevice which provides a python interface. Attributes: - _dev: The underlying QIODevice. + dev: The underlying QIODevice. """ # pylint: disable=missing-docstring def __init__(self, dev): - self._dev = dev + self.dev = dev def __len__(self): - return self._dev.size() + return self.dev.size() def _check_open(self): """Check if the device is open, raise ValueError if not.""" - if not self._dev.isOpen(): + if not self.dev.isOpen(): raise ValueError("IO operation on closed device!") def _check_random(self): @@ -246,7 +246,7 @@ class PyQIODevice(io.BufferedIOBase): def _check_readable(self): """Check if the device is readable, raise OSError if not.""" - if not self._dev.isReadable(): + if not self.dev.isReadable(): raise OSError("Trying to read unreadable file!") def _check_writable(self): @@ -261,62 +261,62 @@ class PyQIODevice(io.BufferedIOBase): self._check_open() self._check_random() if whence == io.SEEK_SET: - ok = self._dev.seek(offset) + ok = self.dev.seek(offset) elif whence == io.SEEK_CUR: - ok = self._dev.seek(self.tell() + offset) + ok = self.dev.seek(self.tell() + offset) elif whence == io.SEEK_END: - ok = self._dev.seek(len(self) + offset) + ok = self.dev.seek(len(self) + offset) else: raise io.UnsupportedOperation("whence = {} is not " "supported!".format(whence)) if not ok: - raise OSError(self._dev.errorString()) + raise OSError(self.dev.errorString()) def truncate(self, size=None): # pylint: disable=unused-argument raise io.UnsupportedOperation def close(self): - self._dev.close() + self.dev.close() @property def closed(self): - return not self._dev.isOpen() + return not self.dev.isOpen() def flush(self): self._check_open() - self._dev.waitForBytesWritten(-1) + self.dev.waitForBytesWritten(-1) def isatty(self): self._check_open() return False def readable(self): - return self._dev.isReadable() + return self.dev.isReadable() def readline(self, size=-1): self._check_open() self._check_readable() if size == -1: size = 0 - return self._dev.readLine(size) + return self.dev.readLine(size) def seekable(self): - return not self._dev.isSequential() + return not self.dev.isSequential() def tell(self): self._check_open() self._check_random() - return self._dev.pos() + return self.dev.pos() def writable(self): - return self._dev.isWritable() + return self.dev.isWritable() def write(self, b): self._check_open() self._check_writable() - num = self._dev.write(b) + num = self.dev.write(b) if num == -1 or num < len(b): - raise OSError(self._dev.errorString()) + raise OSError(self.dev.errorString()) return num def read(self, size=-1): @@ -327,17 +327,17 @@ class PyQIODevice(io.BufferedIOBase): return b'' elif size < 0: # Read all data - if self._dev.bytesAvailable() > 0: - buf = self._dev.readAll() + if self.dev.bytesAvailable() > 0: + buf = self.dev.readAll() if not buf: - raise OSError(self._dev.errorString()) + raise OSError(self.dev.errorString()) else: return b'' else: - if self._dev.bytesAvailable() > 0: - buf = self._dev.read(size) + if self.dev.bytesAvailable() > 0: + buf = self.dev.read(size) if not buf: - raise OSError(self._dev.errorString()) + raise OSError(self.dev.errorString()) else: return b'' return buf From 48de8b145bcb3bc17cf729c9857de09dbb7e94fb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 07:53:19 +0200 Subject: [PATCH 19/30] PyQIODevice: Properly fix read/readLine. --- qutebrowser/utils/qtutils.py | 44 ++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index f66dfa769..7a7050643 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -296,9 +296,25 @@ class PyQIODevice(io.BufferedIOBase): def readline(self, size=-1): self._check_open() self._check_readable() - if size == -1: - size = 0 - return self.dev.readLine(size) + + if size < 0: + qt_size = 0 # no maximum size + elif size == 0: + return QByteArray() + else: + qt_size = size + 1 # Qt also counts the NUL byte + + if self.dev.canReadLine(): + buf = self.dev.readLine(qt_size) + else: + if size < 0: + buf = self.dev.readAll() + else: + buf = self.dev.read(size) + + if buf is None: + raise OSError(self.dev.errorString()) + return buf def seekable(self): return not self.dev.isSequential() @@ -322,24 +338,12 @@ class PyQIODevice(io.BufferedIOBase): def read(self, size=-1): self._check_open() self._check_readable() - if size == 0: - # Read no data - return b'' - elif size < 0: - # Read all data - if self.dev.bytesAvailable() > 0: - buf = self.dev.readAll() - if not buf: - raise OSError(self.dev.errorString()) - else: - return b'' + if size < 0: + buf = self.dev.readAll() else: - if self.dev.bytesAvailable() > 0: - buf = self.dev.read(size) - if not buf: - raise OSError(self.dev.errorString()) - else: - return b'' + buf = self.dev.read(size) + if buf is None: + raise OSError(self.dev.errorString()) return buf From 6a26bc23abbfe81bcb68026a6c10d140cbd98d72 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 07:52:41 +0200 Subject: [PATCH 20/30] PyQIODevice: Remove unneeded check. --- qutebrowser/utils/qtutils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 7a7050643..e38025022 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -195,8 +195,7 @@ def savefile_open(filename, binary=False, encoding='utf-8'): cancelled = True raise else: - if new_f is not None: - new_f.flush() + new_f.flush() finally: commit_ok = f.commit() if not commit_ok and not cancelled: From 460308f388fe1d65b5df0964d7491a5981cfd1da Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 07:52:59 +0200 Subject: [PATCH 21/30] PyQIODevice: Don't use errorString for failed seek. --- qutebrowser/utils/qtutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index e38025022..58772e46a 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -269,7 +269,7 @@ class PyQIODevice(io.BufferedIOBase): raise io.UnsupportedOperation("whence = {} is not " "supported!".format(whence)) if not ok: - raise OSError(self.dev.errorString()) + raise OSError("seek failed!") def truncate(self, size=None): # pylint: disable=unused-argument raise io.UnsupportedOperation From b8dd71a343205f7ee0e1ac82a4e538ef3f2aac00 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 11:22:54 +0200 Subject: [PATCH 22/30] PyQIODevice: Add .open()/.close(). --- qutebrowser/utils/qtutils.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 58772e46a..dc88b8c22 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -253,6 +253,22 @@ class PyQIODevice(io.BufferedIOBase): if not self.writable(): raise OSError("Trying to write to unwritable file!") + def open(self, mode): + """Open the underlying device and ensure opening succeeded. + + Raises OSError if opening failed. + + Args: + mode: QIODevice::OpenMode flags. + """ + ok = self.dev.open(mode) + if not ok: + raise OSError(self.dev.errorString()) + + def close(self): + """Close the underlying device.""" + self.dev.close() + def fileno(self): raise io.UnsupportedOperation @@ -274,9 +290,6 @@ class PyQIODevice(io.BufferedIOBase): def truncate(self, size=None): # pylint: disable=unused-argument raise io.UnsupportedOperation - def close(self): - self.dev.close() - @property def closed(self): return not self.dev.isOpen() From 6452c8f883c2531aea8fbb39e33228a58569ab76 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2015 11:28:03 +0200 Subject: [PATCH 23/30] PyQIODevice: Add context manager support. --- qutebrowser/utils/qtutils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index dc88b8c22..6573306ab 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -260,10 +260,15 @@ class PyQIODevice(io.BufferedIOBase): Args: mode: QIODevice::OpenMode flags. + + Return: + A contextlib.closing() object so this can be used as + contextmanager. """ ok = self.dev.open(mode) if not ok: raise OSError(self.dev.errorString()) + return contextlib.closing(self) def close(self): """Close the underlying device.""" From a969fe021d803a25b4ba788bfcc814af059d0b8d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 07:45:09 +0200 Subject: [PATCH 24/30] tox: Install requirements.txt for tests. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9166f180c..7c53068e7 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ usedevelop = true setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms passenv = DISPLAY XAUTHORITY HOME deps = + -r{toxinidir}/requirements.txt py==1.4.27 pytest==2.7.1 pytest-capturelog==0.7 From 6f3fa9dca65b2fded0640868edcc64962af44cf5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 07:49:27 +0200 Subject: [PATCH 25/30] tox: Show more information when testing. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 7c53068e7..467ba8dfd 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ deps = # on Ubuntu Trusty. commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m py.test --strict {posargs} + {envpython} -m py.test --strict -rfEsw {posargs} [testenv:coverage] passenv = DISPLAY XAUTHORITY HOME @@ -41,7 +41,7 @@ deps = cov-core==1.15.0 commands = {[testenv:mkvenv]commands} - {envpython} -m py.test --strict --cov qutebrowser --cov-report term --cov-report html {posargs} + {envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html {posargs} [testenv:misc] commands = From ddf86600d10a802b90e450e03471acfb4233b4ca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 07:50:56 +0200 Subject: [PATCH 26/30] tests: Rename Testable* classes. This hides some pytest warnings as it tried to collect those classes. --- tests/misc/test_lineparser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/misc/test_lineparser.py b/tests/misc/test_lineparser.py index 613ef3d65..17ae8415b 100644 --- a/tests/misc/test_lineparser.py +++ b/tests/misc/test_lineparser.py @@ -72,7 +72,7 @@ class LineParserWrapper: return True -class TestableAppendLineParser(LineParserWrapper, +class AppendLineParserTestable(LineParserWrapper, lineparsermod.AppendLineParser): """Wrapper over AppendLineParser to make it testable.""" @@ -80,14 +80,14 @@ class TestableAppendLineParser(LineParserWrapper, pass -class TestableLineParser(LineParserWrapper, lineparsermod.LineParser): +class LineParserTestable(LineParserWrapper, lineparsermod.LineParser): """Wrapper over LineParser to make it testable.""" pass -class TestableLimitLineParser(LineParserWrapper, +class LimitLineParserTestable(LineParserWrapper, lineparsermod.LimitLineParser): """Wrapper over LimitLineParser to make it testable.""" @@ -137,7 +137,7 @@ class TestAppendLineParser: @pytest.fixture def lineparser(self): """Fixture to get an AppendLineParser for tests.""" - lp = TestableAppendLineParser('this really', 'does not matter') + lp = AppendLineParserTestable('this really', 'does not matter') lp.new_data = self.BASE_DATA lp.save() return lp @@ -178,7 +178,7 @@ class TestAppendLineParser: def test_get_recent_none(self): """Test get_recent with no data.""" - linep = TestableAppendLineParser('this really', 'does not matter') + linep = AppendLineParserTestable('this really', 'does not matter') assert linep.get_recent() == [] def test_get_recent_little(self, lineparser): From 1b48dc8749eb2135ed269e881fc610acb790da52 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 07:54:25 +0200 Subject: [PATCH 27/30] tox: Also provide sipconfig in link_pyqt.py. --- scripts/link_pyqt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index ee1ebf2de..b6621126d 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -70,7 +70,7 @@ def link_pyqt(sys_path, venv_path): if not globbed_sip: raise Error("Did not find sip in {}!".format(sys_path)) - files = ['PyQt5'] + files = ['PyQt5', 'sipconfig.py'] files += [os.path.basename(e) for e in globbed_sip] for fn in files: source = os.path.join(sys_path, fn) From 2a269e9cd9b2865b3448f7a57a62c078f6cb22d8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 08:10:02 +0200 Subject: [PATCH 28/30] tox: Make sipconfig.py optional in link_pyqt.py. For some reason sipconfig.py doesn't exist at all on Windows... --- scripts/link_pyqt.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index b6621126d..7c8a77b58 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -70,13 +70,16 @@ def link_pyqt(sys_path, venv_path): if not globbed_sip: raise Error("Did not find sip in {}!".format(sys_path)) - files = ['PyQt5', 'sipconfig.py'] - files += [os.path.basename(e) for e in globbed_sip] - for fn in files: + files = [('PyQt5', True), ('sipconfig.py', False)] + files += [(os.path.basename(e), True) for e in globbed_sip] + for fn, required in files: source = os.path.join(sys_path, fn) dest = os.path.join(venv_path, fn) if not os.path.exists(source): - raise FileNotFoundError(source) + if required: + raise FileNotFoundError(source) + else: + continue if os.path.exists(dest): if os.path.isdir(dest) and not os.path.islink(dest): shutil.rmtree(dest) From 091353a7735eea5b118296612c820f5667c57707 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 08:30:26 +0200 Subject: [PATCH 29/30] Mention :adblock-update in quickstart. --- doc/quickstart.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/quickstart.asciidoc b/doc/quickstart.asciidoc index ac3be3a9c..e0b22d378 100644 --- a/doc/quickstart.asciidoc +++ b/doc/quickstart.asciidoc @@ -11,6 +11,7 @@ What to do now * View the 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. * If you just cloned the repository, you'll need to run `scripts/asciidoc2html.py` to generate the documentation. * Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. From 534dbfc4c2634d2f43a379a8f5abb8fb9af112a0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2015 08:51:24 +0200 Subject: [PATCH 30/30] tox: Update check-manifest to 0.25. Upstream changelog: Stop dynamic computation of install_requires in setup.py: this doesn't work well in the presence of the pip 7 wheel cache. Use PEP-426 environment markers instead (this means we now require setuptools version 0.7 or newer). --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 467ba8dfd..f39af4ee3 100644 --- a/tox.ini +++ b/tox.ini @@ -97,7 +97,7 @@ commands = [testenv:check-manifest] skip_install = true deps = - check-manifest==0.24 + check-manifest==0.25 commands = {[testenv:mkvenv]commands} {envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'