diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 29d9c8be4..000000000 --- a/.flake8 +++ /dev/null @@ -1,13 +0,0 @@ -# vim: ft=dosini fileencoding=utf-8: - -[flake8] -# E265: Block comment should start with '#' -# E501: Line too long -# F841: unused variable -# F401: Unused import -# E402: module level import not at top of file -# E266: too many leading '#' for block comment -# W503: line break before binary operator -ignore=E265,E501,F841,F401,E402,E266,W503 -max_complexity = 12 -exclude=resources.py diff --git a/.gitignore b/.gitignore index ee9b26169..696cdc775 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ __pycache__ /htmlcov /.tox /testresults.html +/.cache diff --git a/.pylintrc b/.pylintrc index 2cc56909d..a4abb32a0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,7 +4,6 @@ ignore=resources.py extension-pkg-whitelist=PyQt5,sip load-plugins=pylint_checkers.config, - pylint_checkers.crlf, pylint_checkers.modeline, pylint_checkers.openencoding, pylint_checkers.settrace diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 96e6482ea..92cc4cd24 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -37,6 +37,8 @@ Added - New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is. - New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`). - New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar. +- New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one. +- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `` by default, in addition to clearing search). Changed ~~~~~~~ @@ -45,7 +47,7 @@ Changed - `:spawn` now shows the command being executed in the statusbar, use `-q`/`--quiet` for the old behavior. - The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*. - New bindings `` (rapid), `` (foreground) and `` (background) to switch hint modes while hinting. -- `` is now accepted as an additional alias for ``/`` +- `` and numpad-enter are now bound by default for bindings where `` was bound. - `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`. - `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated. @@ -79,6 +81,8 @@ Fixed - 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...". +- Fixed exception when starting qutebrowser with `:set` as argument. +- Fixed horrible completion performance when the `shrink` option was set. https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 7b0fe6087..1975a9d7c 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -91,9 +91,10 @@ unittests and several linters/checkers. Currently, the following tools will be invoked when you run `tox`: -* Unit tests using the Python -https://docs.python.org/3.4/library/unittest.html[unittest] framework -* https://pypi.python.org/pypi/flake8/[flake8] +* Unit tests using https://www.pytest.org[pytest]. +* https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes] +* https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8] +* https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe] * https://github.com/GreenSteam/pep257/[pep257] * http://pylint.org/[pylint] * https://pypi.python.org/pypi/pyroma/[pyroma] diff --git a/MANIFEST.in b/MANIFEST.in index 7ecd44de2..4092f81c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,7 +28,6 @@ include doc/qutebrowser.1.asciidoc prune tests exclude qutebrowser.rcc exclude .coveragerc -exclude .flake8 exclude .pylintrc exclude .eslintrc exclude doc/help diff --git a/README.asciidoc b/README.asciidoc index 63d803d8f..3a701fd76 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -138,10 +138,10 @@ Contributors, sorted by the number of commits in descending order: * Raphael Pierzina * Joel Torstensson * Claude +* Martin Tournoij * Artur Shaik * Antoni Boucher * ZDarian -* Martin Tournoij * Peter Vilim * John ShaggyTwoDope Jenkins * Jimmy diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 6cc2ee9db..5a6ce06bf 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -642,13 +642,14 @@ Save open pages and quit. [[yank]] === yank -Syntax: +:yank [*--title*] [*--sel*]+ +Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+ Yank the current URL/title to the clipboard or primary selection. ==== optional arguments * +*-t*+, +*--title*+: Yank the title instead of the URL. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. +* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number. [[zoom]] === zoom @@ -684,6 +685,7 @@ How many steps to zoom out. [options="header",width="75%",cols="25%,75%"] |============== |Command|Description +|<>|Clear the currently entered key chain. |<>|Execute the command currently in the commandline. |<>|Go forward in the commandline history. |<>|Go back in the commandline history. @@ -738,6 +740,10 @@ How many steps to zoom out. |<>|Toggle caret selection mode. |<>|Yank the selected text to the clipboard or primary selection. |============== +[[clear-keychain]] +=== clear-keychain +Clear the currently entered key chain. + [[command-accept]] === command-accept Execute the command currently in the commandline. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 073ad2d63..af2453378 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -99,7 +99,7 @@ |<>|Which tab to select when the focused tab is removed. |<>|How new tabs are positioned. |<>|How new tabs opened explicitly are positioned. -|<>|Behaviour when the last tab is closed. +|<>|Behavior when the last tab is closed. |<>|Hide the tab bar if only one tab is open. |<>|Always hide the tab bar. |<>|Whether to wrap when changing tabs. @@ -221,6 +221,7 @@ |<>|Color gradient end for downloads. |<>|Color gradient interpolation system for downloads. |<>|Background color for downloads with errors. +|<>|Background color for webpages if unset (or empty to use the theme's color) |============== .Quick reference for section ``fonts'' @@ -910,7 +911,7 @@ Default: +pass:[last]+ [[tabs-last-close]] === last-close -Behaviour when the last tab is closed. +Behavior when the last tab is closed. Valid values: @@ -1433,7 +1434,7 @@ Default: +pass:[true]+ === next-regexes A comma-separated list of regexes to use for 'next' links. -Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b]+ +Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,\bcontinue\b]+ [[hints-prev-regexes]] === prev-regexes @@ -1752,6 +1753,12 @@ Background color for downloads with errors. Default: +pass:[red]+ +[[colors-webpage.bg]] +=== webpage.bg +Background color for webpages if unset (or empty to use the theme's color) + +Default: +pass:[white]+ + == fonts Fonts used for the UI, with optional style/weight/size. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 40764b098..19fe98b2e 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -610,7 +610,7 @@ class Quitter: # event loop, so we can shut down immediately. self._shutdown(status) - def _shutdown(self, status): # noqa + def _shutdown(self, status): """Second stage of shutdown.""" log.destroy.debug("Stage 2 of shutting down...") if qApp is None: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 170ab5127..c0f19c0b2 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -700,19 +700,28 @@ class CommandDispatcher: frame.scroll(dx, dy) @cmdutils.register(instance='command-dispatcher', scope='window') - def yank(self, title=False, sel=False): + def yank(self, title=False, sel=False, domain=False): """Yank the current URL/title to the clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. title: Yank the title instead of the URL. + domain: Yank only the scheme, domain, and port number. """ clipboard = QApplication.clipboard() if title: s = self._tabbed_browser.page_title(self._current_index()) + what = 'title' + elif domain: + port = self._current_url().port() + s = '{}://{}{}'.format(self._current_url().scheme(), + self._current_url().host(), + ':' + str(port) if port > -1 else '') + what = 'domain' else: s = self._current_url().toString( QUrl.FullyEncoded | QUrl.RemovePassword) + what = 'URL' if sel and clipboard.supportsSelection(): mode = QClipboard.Selection target = "primary selection" @@ -721,8 +730,8 @@ class CommandDispatcher: target = "clipboard" log.misc.debug("Yanking to {}: '{}'".format(target, s)) clipboard.setText(s, mode) - what = 'Title' if title else 'URL' - message.info(self._win_id, "{} yanked to {}".format(what, target)) + message.info(self._win_id, "Yanked {} to {}: {}".format( + what, target, s)) @cmdutils.register(instance='command-dispatcher', scope='window', count='count') diff --git a/qutebrowser/browser/network/networkmanager.py b/qutebrowser/browser/network/networkmanager.py index 3b4c71ed0..aaf5f951b 100644 --- a/qutebrowser/browser/network/networkmanager.py +++ b/qutebrowser/browser/network/networkmanager.py @@ -23,14 +23,8 @@ import collections from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, QUrl) -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslError - -try: - from PyQt5.QtNetwork import QSslSocket -except ImportError: - SSL_AVAILABLE = False -else: - SSL_AVAILABLE = QSslSocket.supportsSsl() +from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError, + QSslSocket) from qutebrowser.config import config from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, @@ -46,13 +40,12 @@ _proxy_auth_cache = {} def init(): """Disable insecure SSL ciphers on old Qt versions.""" - if SSL_AVAILABLE: - if not qtutils.version_check('5.3.0'): - # Disable weak SSL ciphers. - # See https://codereview.qt-project.org/#/c/75943/ - good_ciphers = [c for c in QSslSocket.supportedCiphers() - if c.usedBits() >= 128] - QSslSocket.setDefaultCiphers(good_ciphers) + if not qtutils.version_check('5.3.0'): + # Disable weak SSL ciphers. + # See https://codereview.qt-project.org/#/c/75943/ + good_ciphers = [c for c in QSslSocket.supportedCiphers() + if c.usedBits() >= 128] + QSslSocket.setDefaultCiphers(good_ciphers) class SslError(QSslError): @@ -107,10 +100,9 @@ class NetworkManager(QNetworkAccessManager): } self._set_cookiejar() self._set_cache() - if SSL_AVAILABLE: - self.sslErrors.connect(self.on_ssl_errors) - self._rejected_ssl_errors = collections.defaultdict(list) - self._accepted_ssl_errors = collections.defaultdict(list) + self.sslErrors.connect(self.on_ssl_errors) + self._rejected_ssl_errors = collections.defaultdict(list) + self._accepted_ssl_errors = collections.defaultdict(list) self.authenticationRequired.connect(self.on_authentication_required) self.proxyAuthenticationRequired.connect( self.on_proxy_authentication_required) @@ -181,76 +173,67 @@ class NetworkManager(QNetworkAccessManager): request.deleteLater() self.shutting_down.emit() - if SSL_AVAILABLE: # noqa - @pyqtSlot('QNetworkReply*', 'QList') - def on_ssl_errors(self, reply, errors): - """Decide if SSL errors should be ignored or not. + @pyqtSlot('QNetworkReply*', 'QList') + def on_ssl_errors(self, reply, errors): # pragma: no mccabe + """Decide if SSL errors should be ignored or not. - This slot is called on SSL/TLS errors by the self.sslErrors signal. + This slot is called on SSL/TLS errors by the self.sslErrors signal. - Args: - reply: The QNetworkReply that is encountering the errors. - errors: A list of errors. - """ - errors = [SslError(e) for e in errors] - ssl_strict = config.get('network', 'ssl-strict') - if ssl_strict == 'ask': - try: - host_tpl = urlutils.host_tuple(reply.url()) - except ValueError: - host_tpl = None - is_accepted = False - is_rejected = False - else: - is_accepted = set(errors).issubset( - self._accepted_ssl_errors[host_tpl]) - is_rejected = set(errors).issubset( - self._rejected_ssl_errors[host_tpl]) - if is_accepted: - reply.ignoreSslErrors() - elif is_rejected: - pass - else: - err_string = '\n'.join('- ' + err.errorString() for err in - errors) - answer = self._ask('SSL errors - continue?\n{}'.format( - err_string), mode=usertypes.PromptMode.yesno, - owner=reply) - if answer: - reply.ignoreSslErrors() - d = self._accepted_ssl_errors - else: - d = self._rejected_ssl_errors - if host_tpl is not None: - d[host_tpl] += errors - elif ssl_strict: + Args: + reply: The QNetworkReply that is encountering the errors. + errors: A list of errors. + """ + errors = [SslError(e) for e in errors] + ssl_strict = config.get('network', 'ssl-strict') + if ssl_strict == 'ask': + try: + host_tpl = urlutils.host_tuple(reply.url()) + except ValueError: + host_tpl = None + is_accepted = False + is_rejected = False + else: + is_accepted = set(errors).issubset( + self._accepted_ssl_errors[host_tpl]) + is_rejected = set(errors).issubset( + self._rejected_ssl_errors[host_tpl]) + if is_accepted: + reply.ignoreSslErrors() + elif is_rejected: pass else: - for err in errors: - # FIXME we might want to use warn here (non-fatal error) - # https://github.com/The-Compiler/qutebrowser/issues/114 - message.error(self._win_id, - 'SSL error: {}'.format(err.errorString())) - reply.ignoreSslErrors() + err_string = '\n'.join('- ' + err.errorString() for err in + errors) + answer = self._ask('SSL errors - continue?\n{}'.format( + err_string), mode=usertypes.PromptMode.yesno, + owner=reply) + if answer: + reply.ignoreSslErrors() + d = self._accepted_ssl_errors + else: + d = self._rejected_ssl_errors + if host_tpl is not None: + d[host_tpl] += errors + elif ssl_strict: + pass + else: + for err in errors: + # FIXME we might want to use warn here (non-fatal error) + # https://github.com/The-Compiler/qutebrowser/issues/114 + message.error(self._win_id, + 'SSL error: {}'.format(err.errorString())) + reply.ignoreSslErrors() - @pyqtSlot(QUrl) - def clear_rejected_ssl_errors(self, url): - """Clear the rejected SSL errors on a reload. + @pyqtSlot(QUrl) + def clear_rejected_ssl_errors(self, url): + """Clear the rejected SSL errors on a reload. - Args: - url: The URL to remove. - """ - try: - del self._rejected_ssl_errors[url] - except KeyError: - pass - else: - @pyqtSlot(QUrl) - def clear_rejected_ssl_errors(self, _url): - """Clear the rejected SSL errors on a reload. - - Does nothing because SSL is unavailable. - """ + Args: + url: The URL to remove. + """ + try: + del self._rejected_ssl_errors[url] + except KeyError: pass @pyqtSlot('QNetworkReply', 'QAuthenticator') @@ -334,11 +317,7 @@ class NetworkManager(QNetworkAccessManager): A QNetworkReply. """ scheme = req.url().scheme() - if scheme == 'https' and not SSL_AVAILABLE: - return networkreply.ErrorNetworkReply( - req, "SSL is not supported by the installed Qt library!", - QNetworkReply.ProtocolUnknownError, self) - elif scheme in self._scheme_handlers: + if scheme in self._scheme_handlers: return self._scheme_handlers[scheme].createRequest( op, req, outgoing_data) diff --git a/qutebrowser/browser/network/qutescheme.py b/qutebrowser/browser/network/qutescheme.py index 0432fa6fc..48b3dbd5f 100644 --- a/qutebrowser/browser/network/qutescheme.py +++ b/qutebrowser/browser/network/qutescheme.py @@ -34,6 +34,7 @@ import configparser from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtNetwork import QNetworkReply +from PyQt5.QtWebKit import QWebSettings import qutebrowser from qutebrowser.browser.network import schemehandler, networkreply @@ -96,6 +97,12 @@ class JSBridge(QObject): @pyqtSlot(int, str, str, str) def set(self, win_id, sectname, optname, value): """Slot to set a setting from qute:settings.""" + # https://github.com/The-Compiler/qutebrowser/issues/727 + if (sectname, optname == 'content', 'allow-javascript' and + value == 'false'): + message.error(win_id, "Refusing to disable javascript via " + "qute:settings as it needs javascript support.") + return try: objreg.get('config').set('conf', sectname, optname, value) except (configexc.Error, configparser.Error) as e: @@ -172,10 +179,18 @@ def qute_help(win_id, request): def qute_settings(win_id, _request): """Handler for qute:settings. View/change qute configuration.""" - config_getter = functools.partial(objreg.get('config').get, raw=True) - html = jinja.env.get_template('settings.html').render( - win_id=win_id, title='settings', config=configdata, - confget=config_getter) + if not QWebSettings.globalSettings().testAttribute( + QWebSettings.JavascriptEnabled): + # https://github.com/The-Compiler/qutebrowser/issues/727 + template = jinja.env.get_template('pre.html') + html = template.render( + title='Failed to open qute:settings.', + content="qute:settings needs javascript enabled to work.") + else: + config_getter = functools.partial(objreg.get('config').get, raw=True) + html = jinja.env.get_template('settings.html').render( + win_id=win_id, title='settings', config=configdata, + confget=config_getter) return html.encode('UTF-8', errors='xmlcharrefreplace') diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 59fea9897..263289e6b 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -312,7 +312,7 @@ def javascript_escape(text): def get_child_frames(startframe): """Get all children recursively of a given QWebFrame. - Loosly based on http://blog.nextgenetics.net/?e=64 + Loosely based on http://blog.nextgenetics.net/?e=64 Args: startframe: The QWebFrame to start with. diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 8e430efcb..15659f56f 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -109,7 +109,7 @@ class BrowserPage(QWebPage): def _handle_errorpage(self, info, errpage): """Display an error page if needed. - Loosly based on Helpviewer/HelpBrowserWV.py from eric5 + Loosely based on Helpviewer/HelpBrowserWV.py from eric5 (line 260 @ 5d937eb378dd) Args: @@ -178,7 +178,7 @@ class BrowserPage(QWebPage): def _handle_multiple_files(self, info, files): """Handle uploading of multiple files. - Loosly based on Helpviewer/HelpBrowserWV.py from eric5. + Loosely based on Helpviewer/HelpBrowserWV.py from eric5. Args: info: The ChooseMultipleFilesExtensionOption instance. diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 06176064d..5a4fc3b69 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -24,6 +24,7 @@ import itertools import functools 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 @@ -108,6 +109,7 @@ class WebView(QWebView): self.search_flags = 0 self.selection_enabled = False self.init_neighborlist() + self._set_bg_color() cfg = objreg.get('config') cfg.changed.connect(self.init_neighborlist) # For some reason, this signal doesn't get disconnected automatically @@ -161,7 +163,7 @@ class WebView(QWebView): return utils.get_repr(self, tab_id=self.tab_id, url=url) def __del__(self): - # Explicitely releasing the page here seems to prevent some segfaults + # Explicitly releasing the page here seems to prevent some segfaults # when quitting. # Copied from: # https://code.google.com/p/webscraping/source/browse/webkit.py#325 @@ -181,6 +183,15 @@ class WebView(QWebView): self.load_status = val self.load_status_changed.emit(val.name) + def _set_bg_color(self): + """Set the webpage background color as configured.""" + col = config.get('colors', 'webpage.bg') + palette = self.palette() + if col is None: + col = self.style().standardPalette().color(QPalette.Base) + palette.setColor(QPalette.Base, col) + self.setPalette(palette) + @pyqtSlot(str, str) def on_config_changed(self, section, option): """Reinitialize the zoom neighborlist if related config changed.""" @@ -195,6 +206,8 @@ class WebView(QWebView): self.setContextMenuPolicy(Qt.PreventContextMenu) else: self.setContextMenuPolicy(Qt.DefaultContextMenu) + elif section == 'colors' and option == 'webpage.bg': + self._set_bg_color() def init_neighborlist(self): """Initialize the _zoom neighborlist.""" @@ -607,6 +620,7 @@ class WebView(QWebView): """Save a reference to the context menu so we can close it.""" menu = self.page().createStandardContextMenu() self.shutting_down.connect(menu.close) + modeman.instance(self.win_id).entered.connect(menu.close) menu.exec_(e.globalPos()) def wheelEvent(self, e): diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index d55597d9d..4bda15f8a 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -420,7 +420,7 @@ class Command: value = self._type_conv[param.name](value) return name, value - def _get_call_args(self, win_id): # noqa + def _get_call_args(self, win_id): """Get arguments for a function call. Args: diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 85c511165..76e9f94a6 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -148,7 +148,7 @@ class _BaseUserscriptRunner(QObject): def run(self, cmd, *args, env=None): """Run the userscript given. - Needs to be overridden by superclasses. + Needs to be overridden by subclasses. Args: cmd: The command to be started. @@ -160,7 +160,7 @@ class _BaseUserscriptRunner(QObject): def on_proc_finished(self): """Called when the process has finished. - Needs to be overridden by superclasses. + Needs to be overridden by subclasses. """ raise NotImplementedError diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 2fd1858ca..197c62ce1 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -272,7 +272,7 @@ class Completer(QObject): pattern = parts[self._cursor_part].strip() except IndexError: pattern = '' - self._model().set_pattern(pattern) + completion.set_pattern(pattern) log.completion.debug( "New completion for {}: {}, with pattern '{}'".format( diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index aa2fd31da..0bd6b04c9 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -201,8 +201,17 @@ class CompletionView(QTreeView): for i in range(model.rowCount()): self.expand(model.index(i, 0)) self._resize_columns() - model.rowsRemoved.connect(self.maybe_resize_completion) - model.rowsInserted.connect(self.maybe_resize_completion) + self.maybe_resize_completion() + + def set_pattern(self, pattern): + """Set the completion pattern for the current model. + + Called from on_update_completion(). + + Args: + pattern: The filter pattern to set (what the user entered). + """ + self.model().set_pattern(pattern) self.maybe_resize_completion() @pyqtSlot() diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 6afbdf5e9..6149ba577 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -464,7 +464,7 @@ def data(readonly=False): ('last-close', SettingValue(typ.LastClose(), 'ignore'), - "Behaviour when the last tab is closed."), + "Behavior when the last tab is closed."), ('hide-auto', SettingValue(typ.Bool(), 'false'), @@ -740,7 +740,8 @@ def data(readonly=False): ('next-regexes', SettingValue(typ.RegexList(flags=re.IGNORECASE), - r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b'), + r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,' + r'\bcontinue\b'), "A comma-separated list of regexes to use for 'next' links."), ('prev-regexes', @@ -959,6 +960,11 @@ def data(readonly=False): SettingValue(typ.QtColor(), 'red'), "Background color for downloads with errors."), + ('webpage.bg', + SettingValue(typ.QtColor(none_ok=True), 'white'), + "Background color for webpages if unset (or empty to use the " + "theme's color)"), + readonly=readonly )), @@ -1125,6 +1131,12 @@ KEY_SECTION_DESC = { ""), } +# Keys which are similar to Return and should be bound by default where Return +# is bound. + +RETURN_KEYS = ['', '', '', '', '', + ''] + KEY_DATA = collections.OrderedDict([ ('!normal', collections.OrderedDict([ @@ -1132,7 +1144,7 @@ KEY_DATA = collections.OrderedDict([ ])), ('normal', collections.OrderedDict([ - ('search', ['']), + ('search ;; clear-keychain', ['']), ('set-cmd-text -s :open', ['o']), ('set-cmd-text :open {url}', ['go']), ('set-cmd-text -s :open -t', ['O']), @@ -1193,6 +1205,8 @@ KEY_DATA = collections.OrderedDict([ ('yank -s', ['yY']), ('yank -t', ['yt']), ('yank -ts', ['yT']), + ('yank -d', ['yd']), + ('yank -ds', ['yD']), ('paste', ['pp']), ('paste -s', ['pP']), ('paste -t', ['Pp']), @@ -1244,8 +1258,8 @@ KEY_DATA = collections.OrderedDict([ ('stop', ['']), ('print', ['']), ('open qute:settings', ['Ss']), - ('follow-selected', ['']), - ('follow-selected -t', ['']), + ('follow-selected', RETURN_KEYS), + ('follow-selected -t', ['', '']), ])), ('insert', collections.OrderedDict([ @@ -1253,7 +1267,7 @@ KEY_DATA = collections.OrderedDict([ ])), ('hint', collections.OrderedDict([ - ('follow-hint', ['', '', '']), + ('follow-hint', RETURN_KEYS), ('hint --rapid links tab-bg', ['']), ('hint links', ['']), ('hint all tab-bg', ['']), @@ -1266,13 +1280,11 @@ KEY_DATA = collections.OrderedDict([ ('command-history-next', ['']), ('completion-item-prev', ['', '']), ('completion-item-next', ['', '']), - ('command-accept', ['', '', '', - '']), + ('command-accept', RETURN_KEYS), ])), ('prompt', collections.OrderedDict([ - ('prompt-accept', ['', '', '', - '']), + ('prompt-accept', RETURN_KEYS), ('prompt-yes', ['y']), ('prompt-no', ['n']), ])), @@ -1313,7 +1325,7 @@ KEY_DATA = collections.OrderedDict([ ('move-to-start-of-document', ['gg']), ('move-to-end-of-document', ['G']), ('yank-selected -p', ['Y']), - ('yank-selected', ['y', '', '']), + ('yank-selected', ['y'] + RETURN_KEYS), ('scroll left', ['H']), ('scroll down', ['J']), ('scroll up', ['K']), @@ -1330,8 +1342,8 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r'^download-page$'), r'download'), (re.compile(r'^cancel-download$'), r'download-cancel'), - (re.compile(r'^search ""$'), r'search'), - (re.compile(r"^search ''$"), r'search'), + (re.compile(r"""^search (''|"")$"""), r'search ;; clear-keychain'), + (re.compile(r'^search$'), r'search ;; clear-keychain'), (re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'), (re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'), diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index b9c760116..748748d42 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -693,7 +693,7 @@ class FontFamily(Font): class QtFont(Font): - """A Font which gets converted to q QFont.""" + """A Font which gets converted to a QFont.""" def transform(self, value): if not value: @@ -1312,7 +1312,7 @@ class SelectOnRemove(BaseType): class LastClose(BaseType): - """Behaviour when the last tab is closed.""" + """Behavior when the last tab is closed.""" valid_values = ValidValues(('ignore', "Don't do anything."), ('blank', "Load a blank page."), diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index b52a39824..56b9cfaac 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -23,7 +23,7 @@ import re import functools import unicodedata -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from qutebrowser.config import config from qutebrowser.utils import usertypes, log, utils, objreg @@ -49,6 +49,8 @@ class BaseKeyParser(QObject): special: execute() was called via a special key binding do_log: Whether to log keypresses or not. + passthrough: Whether unbound keys should be passed through with this + handler. Attributes: bindings: Bound key bindings @@ -69,6 +71,7 @@ class BaseKeyParser(QObject): keystring_updated = pyqtSignal(str) do_log = True + passthrough = False Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous', 'other', 'none']) @@ -162,12 +165,6 @@ class BaseKeyParser(QObject): key = e.key() self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) - if key == Qt.Key_Escape: - self._debug_log("Escape pressed, discarding '{}'.".format( - self._keystring)) - self._keystring = '' - return self.Match.none - if len(txt) == 1: category = unicodedata.category(txt) is_control_char = (category == 'Cc') @@ -198,7 +195,7 @@ class BaseKeyParser(QObject): self._keystring = '' self.execute(binding, self.Type.chain, count) elif match == self.Match.ambiguous: - self._debug_log("Ambigious match for '{}'.".format( + self._debug_log("Ambiguous match for '{}'.".format( self._keystring)) self._handle_ambiguous_match(binding, count) elif match == self.Match.partial: @@ -303,6 +300,7 @@ class BaseKeyParser(QObject): True if the event was handled, False otherwise. """ handled = self._handle_special_key(e) + if handled or not self._supports_chains: return handled match = self._handle_single_key(e) @@ -359,3 +357,9 @@ class BaseKeyParser(QObject): "defined!") if mode == self._modename: self.read_config() + + def clear_keystring(self): + """Clear the currently entered key sequence.""" + self._debug_log("discarding keystring '{}'.".format(self._keystring)) + self._keystring = '' + self.keystring_updated.emit(self._keystring) diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index bef364e66..46f179fdb 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser): """ do_log = False + passthrough = True def __init__(self, win_id, mode, parent=None, warn=True): """Constructor. diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index fc70ac76b..6906a8720 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -84,38 +84,30 @@ def init(win_id, parent): modeman.destroyed.connect( functools.partial(objreg.delete, 'keyparsers', scope='window', window=win_id)) - modeman.register(KM.normal, keyparsers[KM.normal].handle) - modeman.register(KM.hint, keyparsers[KM.hint].handle) - modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True) - modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle, - passthrough=True) - modeman.register(KM.command, keyparsers[KM.command].handle, - passthrough=True) - modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True) - modeman.register(KM.yesno, keyparsers[KM.yesno].handle) - modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True) + for mode, parser in keyparsers.items(): + modeman.register(mode, parser) return modeman -def _get_modeman(win_id): +def instance(win_id): """Get a modemanager object.""" return objreg.get('mode-manager', scope='window', window=win_id) def enter(win_id, mode, reason=None, only_if_normal=False): """Enter the mode 'mode'.""" - _get_modeman(win_id).enter(mode, reason, only_if_normal) + instance(win_id).enter(mode, reason, only_if_normal) def leave(win_id, mode, reason=None): """Leave the mode 'mode'.""" - _get_modeman(win_id).leave(mode, reason) + instance(win_id).leave(mode, reason) def maybe_leave(win_id, mode, reason=None): """Convenience method to leave 'mode' without exceptions.""" try: - _get_modeman(win_id).leave(mode, reason) + instance(win_id).leave(mode, reason) except NotInModeError as e: # This is rather likely to happen, so we only log to debug log. log.modes.debug("{} (leave reason: {})".format(e, reason)) @@ -126,10 +118,9 @@ class ModeManager(QObject): """Manager for keyboard modes. Attributes: - passthrough: A list of modes in which to pass through events. mode: The mode we're currently in. _win_id: The window ID of this ModeManager - _handlers: A dictionary of modes and their handlers. + _parsers: A dictionary of modes and their keyparsers. _forward_unbound_keys: If we should forward unbound keys. _releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was passed through, so the release event should as @@ -151,8 +142,7 @@ class ModeManager(QObject): def __init__(self, win_id, parent=None): super().__init__(parent) self._win_id = win_id - self._handlers = {} - self.passthrough = [] + self._parsers = {} self.mode = usertypes.KeyMode.normal self._releaseevents_to_pass = set() self._forward_unbound_keys = config.get( @@ -160,8 +150,7 @@ class ModeManager(QObject): objreg.get('config').changed.connect(self.set_forward_unbound_keys) def __repr__(self): - return utils.get_repr(self, mode=self.mode, - passthrough=self.passthrough) + return utils.get_repr(self, mode=self.mode) def _eventFilter_keypress(self, event): """Handle filtering of KeyPress events. @@ -173,11 +162,11 @@ class ModeManager(QObject): True if event should be filtered, False otherwise. """ curmode = self.mode - handler = self._handlers[curmode] + parser = self._parsers[curmode] if curmode != usertypes.KeyMode.insert: - log.modes.debug("got keypress in mode {} - calling handler " - "{}".format(curmode, utils.qualname(handler))) - handled = handler(event) if handler is not None else False + log.modes.debug("got keypress in mode {} - delegating to " + "{}".format(curmode, utils.qualname(parser))) + handled = parser.handle(event) is_non_alnum = bool(event.modifiers()) or not event.text().strip() focus_widget = QApplication.instance().focusWidget() @@ -187,7 +176,7 @@ class ModeManager(QObject): filter_this = True elif is_tab and not isinstance(focus_widget, QWebView): filter_this = True - elif (curmode in self.passthrough or + elif (parser.passthrough or self._forward_unbound_keys == 'all' or (self._forward_unbound_keys == 'auto' and is_non_alnum)): filter_this = False @@ -202,8 +191,8 @@ class ModeManager(QObject): "passthrough: {}, is_non_alnum: {}, is_tab {} --> " "filter: {} (focused: {!r})".format( handled, self._forward_unbound_keys, - curmode in self.passthrough, is_non_alnum, - is_tab, filter_this, focus_widget)) + parser.passthrough, is_non_alnum, is_tab, + filter_this, focus_widget)) return filter_this def _eventFilter_keyrelease(self, event): @@ -226,20 +215,16 @@ class ModeManager(QObject): log.modes.debug("filter: {}".format(filter_this)) return filter_this - def register(self, mode, handler, passthrough=False): + def register(self, mode, parser): """Register a new mode. Args: mode: The name of the mode. - handler: Handler for keyPressEvents. - passthrough: Whether to pass key bindings in this mode through to - the widgets. + parser: The KeyParser which should be used. """ - if not isinstance(mode, usertypes.KeyMode): - raise TypeError("Mode {} is no KeyMode member!".format(mode)) - self._handlers[mode] = handler - if passthrough: - self.passthrough.append(mode) + assert isinstance(mode, usertypes.KeyMode) + assert parser is not None + self._parsers[mode] = parser def enter(self, mode, reason=None, only_if_normal=False): """Enter a new mode. @@ -253,8 +238,8 @@ class ModeManager(QObject): raise TypeError("Mode {} is no KeyMode member!".format(mode)) log.modes.debug("Entering mode {}{}".format( mode, '' if reason is None else ' (reason: {})'.format(reason))) - if mode not in self._handlers: - raise ValueError("No handler for mode {}".format(mode)) + if mode not in self._parsers: + raise ValueError("No keyparser for mode {}".format(mode)) prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno) if self.mode == mode or (self.mode in prompt_modes and mode in prompt_modes): @@ -332,3 +317,8 @@ class ModeManager(QObject): return self._eventFilter_keypress(event) else: return self._eventFilter_keyrelease(event) + + @cmdutils.register(instance='mode-manager', scope='window', hide=True) + def clear_keychain(self): + """Clear the currently entered key chain.""" + self._parsers[self.mode].clear_keystring() diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 8d47de0c1..d16734ed0 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -224,6 +224,8 @@ class CaretKeyParser(keyparser.CommandKeyParser): """KeyParser for caret mode.""" + passthrough = True + def __init__(self, win_id, parent=None): super().__init__(win_id, parent, supports_count=True, supports_chains=True) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 5f633eaaa..bc828a261 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -469,9 +469,9 @@ class StatusBar(QWidget): @pyqtSlot(usertypes.KeyMode) def on_mode_entered(self, mode): """Mark certain modes in the commandline.""" - mode_manager = objreg.get('mode-manager', scope='window', - window=self._win_id) - if mode in mode_manager.passthrough: + keyparsers = objreg.get('keyparsers', scope='window', + window=self._win_id) + if keyparsers[mode].passthrough: self._set_mode_text(mode.name) if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self.set_mode_active(mode, True) @@ -479,10 +479,10 @@ class StatusBar(QWidget): @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) def on_mode_left(self, old_mode, new_mode): """Clear marked mode.""" - mode_manager = objreg.get('mode-manager', scope='window', - window=self._win_id) - if old_mode in mode_manager.passthrough: - if new_mode in mode_manager.passthrough: + keyparsers = objreg.get('keyparsers', scope='window', + window=self._win_id) + if keyparsers[old_mode].passthrough: + if keyparsers[new_mode].passthrough: self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '') diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index ba5a5c725..7b4d84b7b 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -296,7 +296,7 @@ class TabbedBrowser(tabwidget.TabWidget): newtab: True to open URL in a new tab, False otherwise. """ qtutils.ensure_valid(url) - if newtab: + if newtab or self.currentWidget() is None: self.tabopen(url, background=False) else: self.currentWidget().openurl(url) @@ -332,7 +332,7 @@ class TabbedBrowser(tabwidget.TabWidget): the default settings we handle it like Chromium does: - Tabs from clicked links etc. are to the right of the current. - - Explicitely opened tabs are at the very right. + - Explicitly opened tabs are at the very right. Return: The opened WebView instance. diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 79a425fa9..dbb473b4e 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -183,7 +183,7 @@ class _CrashDialog(QDialog): def _init_text(self): """Initialize the main text to be displayed on an exception. - Should be extended by superclass to set the actual text.""" + Should be extended by subclasses to set the actual text.""" self._lbl = QLabel(wordWrap=True, openExternalLinks=True, textInteractionFlags=Qt.LinksAccessibleByMouse) self._vbox.addWidget(self._lbl) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 7598f3312..2e336bc1e 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -190,7 +190,7 @@ class CrashHandler(QObject): objects = "" return ExceptionInfo(pages, cmd_history, objects) - def exception_hook(self, exctype, excvalue, tb): # noqa + def exception_hook(self, exctype, excvalue, tb): """Handle uncaught python exceptions. It'll try very hard to write all open tabs to a file, and then exit diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 3bc214389..43806df58 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -213,6 +213,19 @@ def check_qt_version(): _die(text) +def check_ssl_support(): + """Check if SSL support is available.""" + try: + from PyQt5.QtNetwork import QSslSocket + except ImportError: + ok = False + else: + ok = QSslSocket.supportsSsl() + if not ok: + text = "Fatal error: Your Qt is built without SSL support." + _die(text) + + def check_libraries(): """Check if all needed Python libraries are installed.""" modules = { @@ -288,6 +301,7 @@ def earlyinit(args): # Now we can be sure QtCore is available, so we can print dialogs on # errors, so people only using the GUI notice them as well. check_qt_version() + check_ssl_support() remove_inputhook() check_libraries() init_log(args) diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 74dd8f92e..04ced01c2 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -77,7 +77,7 @@ class CommandLineEdit(QLineEdit): def __on_cursor_position_changed(self, _old, new): """Prevent the cursor moving to the prompt. - We use __ here to avoid accidentally overriding it in superclasses. + We use __ here to avoid accidentally overriding it in subclasses. """ if new < self._promptlen: self.setCursorPosition(self._promptlen) diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index b763d8246..a7bbeea6e 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -55,7 +55,7 @@ class ShellLexer: self.token = '' self.state = ' ' - def __iter__(self): # noqa + def __iter__(self): # pragma: no mccabe """Read a raw token from the input stream.""" # pylint: disable=too-many-branches,too-many-statements self.reset() diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index d156c6be1..1f1071673 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -377,7 +377,7 @@ class RAMHandler(logging.Handler): """Logging handler which keeps the messages in a deque in RAM. - Loosly based on logging.BufferingHandler which is unsuitable because it + Loosely based on logging.BufferingHandler which is unsuitable because it uses a simple list rather than a deque. Attributes: diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index cabfdc979..8aebfc6da 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -73,7 +73,7 @@ class NeighborList(collections.abc.Sequence): Args: items: The list of items to iterate in. _default: The initially selected value. - _mode: Behaviour when the first/last item is reached. + _mode: Behavior when the first/last item is reached. Modes.block: Stay on the selected item Modes.wrap: Wrap around to the other end Modes.exception: Raise an IndexError. @@ -243,7 +243,7 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value', # Exit statuses for errors. Needs to be an int for sys.exit. Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', - 'err_config', 'err_key_config'], is_int=True) + 'err_config', 'err_key_config'], is_int=True, start=0) class Question(QObject): diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 827762af4..add7e4c84 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -29,10 +29,7 @@ import collections from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion from PyQt5.QtWebKit import qWebKitVersion -try: - from PyQt5.QtNetwork import QSslSocket -except ImportError: - QSslSocket = None +from PyQt5.QtNetwork import QSslSocket import qutebrowser from qutebrowser.utils import log, utils @@ -127,18 +124,8 @@ def _module_versions(): A list of lines with version info. """ lines = [] - try: - import sipconfig # pylint: disable=import-error,unused-variable - except ImportError: - lines.append('SIP: ?') - else: - try: - lines.append('SIP: {}'.format( - sipconfig.Configuration().sip_version_str)) - except (AttributeError, TypeError): - log.misc.exception("Error while getting SIP version") - lines.append('SIP: ?') modules = collections.OrderedDict([ + ('sip', ['SIP_VERSION_STR']), ('colorlog', []), ('colorama', ['VERSION', '__version__']), ('pypeg2', ['__version__']), @@ -209,16 +196,13 @@ def version(): 'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()), 'PyQt: {}'.format(PYQT_VERSION_STR), ] + lines += _module_versions() - if QSslSocket is not None and QSslSocket.supportsSsl(): - ssl_version = QSslSocket.sslLibraryVersionString() - else: - ssl_version = 'unavailable' lines += [ 'Webkit: {}'.format(qWebKitVersion()), 'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')), - 'SSL: {}'.format(ssl_version), + 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()), '', 'Frozen: {}'.format(hasattr(sys, 'frozen')), 'Platform: {}, {}'.format(platform.platform(), diff --git a/scripts/misc_checks.py b/scripts/misc_checks.py index 4e2c185f5..dac0fe017 100644 --- a/scripts/misc_checks.py +++ b/scripts/misc_checks.py @@ -35,9 +35,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) from scripts import utils -def _py_files(target): +def _py_files(): """Iterate over all python files and yield filenames.""" - for (dirpath, _dirnames, filenames) in os.walk(target): + for (dirpath, _dirnames, filenames) in os.walk('.'): + parts = dirpath.split(os.sep) + if len(parts) >= 2 and parts[1].startswith('.'): + # ignore hidden dirs + continue for name in (e for e in filenames if e.endswith('.py')): yield os.path.join(dirpath, name) @@ -64,31 +68,32 @@ def check_git(): return status -def check_spelling(target): +def check_spelling(): """Check commonly misspelled words.""" # Words which I often misspell - words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully', - 'occur[^r .]', 'seperator', 'explicitely', 'resetted', - 'auxillary', 'accidentaly', 'ambigious', 'loosly', - 'initialis', 'convienence', 'similiar', 'uncommited', - 'reproducable'} + words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully', + '[Oo]ccur[^r .]', '[Ss]eperator', '[Ee]xplicitely', '[Rr]esetted', + '[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly', + '[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited', + '[Rr]eproducable'} # Words which look better when splitted, but might need some fine tuning. - words |= {'keystrings', 'webelements', 'mouseevent', 'keysequence', - 'normalmode', 'eventloops', 'sizehint', 'statemachine', - 'metaobject', 'logrecord', 'filetype'} + words |= {'[Kk]eystrings', '[Ww]ebelements', '[Mm]ouseevent', + '[Kk]eysequence', '[Nn]ormalmode', '[Ee]ventloops', + '[Ss]izehint', '[Ss]tatemachine', '[Mm]etaobject', + '[Ll]ogrecord', '[Ff]iletype'} seen = collections.defaultdict(list) try: ok = True - for fn in _py_files(target): + for fn in _py_files(): with tokenize.open(fn) as f: - if fn == os.path.join('scripts', 'misc_checks.py'): + if fn == os.path.join('.', 'scripts', 'misc_checks.py'): continue for line in f: for w in words: if re.search(w, line) and fn not in seen[w]: - print("Found '{}' in {}!".format(w, fn)) + print('Found "{}" in {}!'.format(w, fn)) seen[w].append(fn) ok = False print() @@ -98,11 +103,11 @@ def check_spelling(target): return None -def check_vcs_conflict(target): +def check_vcs_conflict(): """Check VCS conflict markers.""" try: ok = True - for fn in _py_files(target): + for fn in _py_files(): with tokenize.open(fn) as f: for line in f: if any(line.startswith(c * 7) for c in '<>=|'): @@ -120,25 +125,14 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('checker', choices=('git', 'vcs', 'spelling'), help="Which checker to run.") - parser.add_argument('target', help="What to check", nargs='*') args = parser.parse_args() if args.checker == 'git': ok = check_git() - return 0 if ok else 1 elif args.checker == 'vcs': - is_ok = True - for target in args.target: - ok = check_vcs_conflict(target) - if not ok: - is_ok = False - return 0 if is_ok else 1 + ok = check_vcs_conflict() elif args.checker == 'spelling': - is_ok = True - for target in args.target: - ok = check_spelling(target) - if not ok: - is_ok = False - return 0 if is_ok else 1 + ok = check_spelling() + return 0 if ok else 1 if __name__ == '__main__': diff --git a/scripts/pylint_checkers/crlf.py b/scripts/pylint_checkers/crlf.py deleted file mode 100644 index a77f8b9e0..000000000 --- a/scripts/pylint_checkers/crlf.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2014-2015 Florian Bruhin (The Compiler) -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Checker for CRLF in files.""" - -from pylint import interfaces, checkers - - -class CrlfChecker(checkers.BaseChecker): - - """Check for CRLF in files.""" - - __implements__ = interfaces.IRawChecker - - name = 'crlf' - msgs = {'W9001': ('Uses CRLFs', 'crlf', None)} - options = () - priority = -1 - - def process_module(self, node): - """Process the module.""" - for (lineno, line) in enumerate(node.file_stream): - if b'\r\n' in line: - self.add_message('crlf', line=lineno) - return - - -def register(linter): - """Register the checker.""" - linter.register_checker(CrlfChecker(linter)) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index d5fab2ed1..3fff0ea66 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -197,8 +197,10 @@ class TestKeyConfigParser: ('download-page', 'download'), ('cancel-download', 'download-cancel'), - ('search ""', 'search'), - ("search ''", 'search'), + ('search ""', 'search ;; clear-keychain'), + ("search ''", 'search ;; clear-keychain'), + ("search", 'search ;; clear-keychain'), + ("search ;; foobar", None), ('search "foo"', None), ('set-cmd-text "foo bar"', 'set-cmd-text foo bar'), diff --git a/tests/javascript/conftest.py b/tests/javascript/conftest.py index d97b38625..85b56a577 100644 --- a/tests/javascript/conftest.py +++ b/tests/javascript/conftest.py @@ -80,8 +80,10 @@ class JSTester: def scroll_anchor(self, name): """Scroll the main frame to the given anchor.""" page = self.webview.page() - with self._qtbot.waitSignal(page.scrollRequested): - page.mainFrame().scrollToAnchor(name) + old_pos = page.mainFrame().scrollPosition() + page.mainFrame().scrollToAnchor(name) + new_pos = page.mainFrame().scrollPosition() + assert old_pos != new_pos def load(self, path, **kwargs): """Load and display the given test data. @@ -92,7 +94,7 @@ class JSTester: **kwargs: Passed to jinja's template.render(). """ template = self._jinja_env.get_template(path) - with self._qtbot.waitSignal(self.webview.loadFinished): + with self._qtbot.waitSignal(self.webview.loadFinished, raising=True): self.webview.setHtml(template.render(**kwargs)) def run_file(self, filename): diff --git a/tests/mainwindow/statusbar/test_progress.py b/tests/mainwindow/statusbar/test_progress.py index 07e93e0e5..b8b03cd29 100644 --- a/tests/mainwindow/statusbar/test_progress.py +++ b/tests/mainwindow/statusbar/test_progress.py @@ -39,6 +39,7 @@ def progress_widget(qtbot, monkeypatch, config_stub): 'qutebrowser.mainwindow.statusbar.progress.style.config', config_stub) widget = Progress() qtbot.add_widget(widget) + widget.setGeometry(200, 200, 200, 200) assert not widget.isVisible() assert not widget.isTextVisible() return widget diff --git a/tests/misc/test_readline.py b/tests/misc/test_readline.py index 523c6f579..da2d05821 100644 --- a/tests/misc/test_readline.py +++ b/tests/misc/test_readline.py @@ -21,144 +21,256 @@ # pylint: disable=protected-access +import re import inspect -from unittest import mock -from PyQt5.QtWidgets import QLineEdit +from PyQt5.QtWidgets import QLineEdit, QApplication import pytest from qutebrowser.misc import readline +# Some functions aren't 100% readline compatible: +# https://github.com/The-Compiler/qutebrowser/issues/678 +# Those are marked with fixme and have another value marked with '# wrong' +# which marks the current behavior. + +fixme = pytest.mark.xfail(reason='readline compatibility - see #678') + + +class LineEdit(QLineEdit): + + """QLineEdit with some methods to make testing easier.""" + + def _get_index(self, haystack, needle): + """Get the index of a char (needle) in a string (haystack). + + Return: + The position where needle was found, or None if it wasn't found. + """ + try: + return haystack.index(needle) + except ValueError: + return None + + def set_aug_text(self, text): + """Set a text with markers for selected text and | as cursor.""" + real_text = re.sub('[<>|]', '', text) + self.setText(real_text) + + cursor_pos = self._get_index(text, '|') + sel_start_pos = self._get_index(text, '<') + sel_end_pos = self._get_index(text, '>') + + if sel_start_pos is not None and sel_end_pos is None: + raise ValueError("< given without >!") + if sel_start_pos is None and sel_end_pos is not None: + raise ValueError("> given without !") + self.setCursorPosition(cursor_pos) + elif sel_start_pos is not None: + if sel_start_pos > sel_end_pos: + raise ValueError("< given after >!") + sel_len = sel_end_pos - sel_start_pos - 1 + self.setSelection(sel_start_pos, sel_len) + + def aug_text(self): + """Get a text with markers for selected text and | as cursor.""" + text = self.text() + chars = list(text) + cur_pos = self.cursorPosition() + assert cur_pos >= 0 + chars.insert(cur_pos, '|') + if self.hasSelectedText(): + selected_text = self.selectedText() + sel_start = self.selectionStart() + sel_end = sel_start + len(selected_text) + assert sel_start > 0 + assert sel_end > 0 + assert sel_end > sel_start + assert cur_pos == sel_end + assert text[sel_start:sel_end] == selected_text + chars.insert(sel_start, '<') + chars.insert(sel_end + 1, '>') + return ''.join(chars) + + @pytest.fixture -def mocked_qapp(monkeypatch, stubs): - """Fixture that mocks readline.QApplication and returns it.""" - stub = stubs.FakeQApplication() - monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub) - return stub +def lineedit(qtbot, monkeypatch): + """Fixture providing a LineEdit.""" + le = LineEdit() + qtbot.add_widget(le) + monkeypatch.setattr(QApplication.instance(), 'focusWidget', lambda: le) + return le -class TestNoneWidget: - - """Test if there are no exceptions when the widget is None.""" - - def test_none(self, mocked_qapp): - """Call each rl_* method with a None focusWidget.""" - self.bridge = readline.ReadlineBridge() - mocked_qapp.focusWidget = mock.Mock(return_value=None) - for name, method in inspect.getmembers(self.bridge, inspect.ismethod): - if name.startswith('rl_'): - method() +@pytest.fixture +def bridge(): + """Fixture providing a ReadlineBridge.""" + return readline.ReadlineBridge() -class TestReadlineBridgeTest: +def test_none(bridge, qtbot): + """Call each rl_* method with a None focusWidget.""" + assert QApplication.instance().focusWidget() is None + for name, method in inspect.getmembers(bridge, inspect.ismethod): + if name.startswith('rl_'): + method() - """Tests for readline bridge.""" - @pytest.fixture(autouse=True) - def setup(self): - self.qle = mock.Mock() - self.qle.__class__ = QLineEdit - self.bridge = readline.ReadlineBridge() +@pytest.mark.parametrize('text, expected', [('fbar', 'fo|obar'), + ('|foobar', '|foobar')]) +def test_rl_backward_char(text, expected, lineedit, bridge): + """Test rl_backward_char.""" + lineedit.set_aug_text(text) + bridge.rl_backward_char() + assert lineedit.aug_text() == expected - def _set_selected_text(self, text): - """Set the value the fake QLineEdit should return for selectedText.""" - self.qle.configure_mock(**{'selectedText.return_value': text}) - def test_rl_backward_char(self, mocked_qapp): - """Test rl_backward_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_char() - self.qle.cursorBackward.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [('fbar', 'foob|ar'), + ('foobar|', 'foobar|')]) +def test_rl_forward_char(text, expected, lineedit, bridge): + """Test rl_forward_char.""" + lineedit.set_aug_text(text) + bridge.rl_forward_char() + assert lineedit.aug_text() == expected - def test_rl_forward_char(self, mocked_qapp): - """Test rl_forward_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_forward_char() - self.qle.cursorForward.assert_called_with(False) - def test_rl_backward_word(self, mocked_qapp): - """Test rl_backward_word.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_word() - self.qle.cursorWordBackward.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [('one o', 'one |two'), + ('two', '|one two'), + ('|one two', '|one two')]) +def test_rl_backward_word(text, expected, lineedit, bridge): + """Test rl_backward_word.""" + lineedit.set_aug_text(text) + bridge.rl_backward_word() + assert lineedit.aug_text() == expected - def test_rl_forward_word(self, mocked_qapp): - """Test rl_forward_word.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_forward_word() - self.qle.cursorWordForward.assert_called_with(False) - def test_rl_beginning_of_line(self, mocked_qapp): - """Test rl_beginning_of_line.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_beginning_of_line() - self.qle.home.assert_called_with(False) +@pytest.mark.parametrize('text, expected', [ + fixme(('ne two', 'one| two')), + ('ne two', 'one |two'), # wrong + fixme((' two', 'one two|')), + (' two', 'one |two'), # wrong + ('one t', 'one two|') +]) +def test_rl_forward_word(text, expected, lineedit, bridge): + """Test rl_forward_word.""" + lineedit.set_aug_text(text) + bridge.rl_forward_word() + assert lineedit.aug_text() == expected - def test_rl_end_of_line(self, mocked_qapp): - """Test rl_end_of_line.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_end_of_line() - self.qle.end.assert_called_with(False) - def test_rl_delete_char(self, mocked_qapp): - """Test rl_delete_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_delete_char() - self.qle.del_.assert_called_with() +def test_rl_beginning_of_line(lineedit, bridge): + """Test rl_beginning_of_line.""" + lineedit.set_aug_text('fbar') + bridge.rl_beginning_of_line() + assert lineedit.aug_text() == '|foobar' - def test_rl_backward_delete_char(self, mocked_qapp): - """Test rl_backward_delete_char.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_backward_delete_char() - self.qle.backspace.assert_called_with() - def test_rl_unix_line_discard(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_unix_line_discard() - self.qle.home.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") +def test_rl_end_of_line(lineedit, bridge): + """Test rl_end_of_line.""" + lineedit.set_aug_text('fbar') + bridge.rl_end_of_line() + assert lineedit.aug_text() == 'foobar|' - def test_rl_kill_line(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_kill_line() - self.qle.end.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") - def test_rl_unix_word_rubout(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_unix_word_rubout() - self.qle.cursorWordBackward.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") +@pytest.mark.parametrize('text, expected', [('foo|bar', 'foo|ar'), + ('foobar|', 'foobar|'), + ('|foobar', '|oobar'), + ('fbar', 'f|bar')]) +def test_rl_delete_char(text, expected, lineedit, bridge): + """Test rl_delete_char.""" + lineedit.set_aug_text(text) + bridge.rl_delete_char() + assert lineedit.aug_text() == expected - def test_rl_kill_word(self, mocked_qapp): - """Set a selected text, delete it, see if it comes back with yank.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self._set_selected_text("delete test") - self.bridge.rl_kill_word() - self.qle.cursorWordForward.assert_called_with(True) - assert self.bridge._deleted[self.qle] == "delete test" - self.qle.del_.assert_called_with() - self.bridge.rl_yank() - self.qle.insert.assert_called_with("delete test") - def test_rl_yank_no_text(self, mocked_qapp): - """Test yank without having deleted anything.""" - mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) - self.bridge.rl_yank() - assert not self.qle.insert.called +@pytest.mark.parametrize('text, expected', [('foo|bar', 'fo|bar'), + ('foobar|', 'fooba|'), + ('|foobar', '|foobar'), + ('fbar', 'f|bar')]) +def test_rl_backward_delete_char(text, expected, lineedit, bridge): + """Test rl_backward_delete_char.""" + lineedit.set_aug_text(text) + bridge.rl_backward_delete_char() + assert lineedit.aug_text() == expected + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('delete this| test', 'delete this', '| test'), + fixme(('delete test', 'delete this', '| test')), + ('delete test', 'delete ', '|this test'), # wrong + fixme(('fbar', 'foo', '|bar')), + ('fbar', 'f', '|oobar'), # wrong +]) +def test_rl_unix_line_discard(lineedit, bridge, text, deleted, rest): + """Delete from the cursor to the beginning of the line and yank back.""" + lineedit.set_aug_text(text) + bridge.rl_unix_line_discard() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('test |delete this', 'delete this', 'test |'), + fixme(('delete this', 'test delete this', 'test |')), + ('delete this', 'test delete this', '|'), # wrong +]) +def test_rl_kill_line(lineedit, bridge, text, deleted, rest): + """Delete from the cursor to the end of line and yank back.""" + lineedit.set_aug_text(text) + bridge.rl_kill_line() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + ('test delete|foobar', 'delete', 'test |foobar'), + ('test delete |foobar', 'delete ', 'test |foobar'), + fixme(('test delfoobar', 'delete', 'test |foobar')), + ('test delfoobar', 'del', 'test |ete foobar'), # wrong +]) +def test_rl_unix_word_rubout(lineedit, bridge, text, deleted, rest): + """Delete to word beginning and see if it comes back with yank.""" + lineedit.set_aug_text(text) + bridge.rl_unix_word_rubout() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +@pytest.mark.parametrize('text, deleted, rest', [ + fixme(('test foobar| delete', ' delete', 'test foobar|')), + ('test foobar| delete', ' ', 'test foobar|delete'), # wrong + fixme(('test foo|delete bar', 'delete', 'test foo| bar')), + ('test foo|delete bar', 'delete ', 'test foo|bar'), # wrong + fixme(('test foo delete', ' delete', 'test foobar|')), + ('test foodelete', 'bardelete', 'test foo|'), # wrong +]) +def test_rl_kill_word(lineedit, bridge, text, deleted, rest): + """Delete to word end and see if it comes back with yank.""" + lineedit.set_aug_text(text) + bridge.rl_kill_word() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +def test_rl_yank_no_text(lineedit, bridge): + """Test yank without having deleted anything.""" + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == '|' diff --git a/tests/utils/test_log.py b/tests/utils/test_log.py index a09a354ee..03575ea5b 100644 --- a/tests/utils/test_log.py +++ b/tests/utils/test_log.py @@ -27,7 +27,6 @@ import itertools import sys import pytest -from PyQt5.QtCore import qWarning from qutebrowser.utils import log @@ -214,7 +213,7 @@ class TestInitLog: @pytest.fixture def args(self): - """Fixture providing an argparse namespace.""" + """Fixture providing an argparse namespace for init_log.""" return argparse.Namespace(debug=True, loglevel=logging.DEBUG, color=True, loglines=10, logfilter="") @@ -230,33 +229,37 @@ class TestHideQtWarning: """Tests for hide_qt_warning/QtWarningFilter.""" - def test_unfiltered(self, caplog): + @pytest.fixture() + def logger(self): + return logging.getLogger('qt-tests') + + def test_unfiltered(self, logger, caplog): """Test a message which is not filtered.""" with log.hide_qt_warning("World", logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'): - qWarning("Hello World") + logger.warning("Hello World") assert len(caplog.records()) == 1 record = caplog.records()[0] assert record.levelname == 'WARNING' assert record.message == "Hello World" - def test_filtered_exact(self, caplog): + def test_filtered_exact(self, logger, caplog): """Test a message which is filtered (exact match).""" with log.hide_qt_warning("Hello", logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'): - qWarning("Hello") + logger.warning("Hello") assert not caplog.records() - def test_filtered_start(self, caplog): + def test_filtered_start(self, logger, caplog): """Test a message which is filtered (match at line start).""" with log.hide_qt_warning("Hello", logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'): - qWarning("Hello World") + logger.warning("Hello World") assert not caplog.records() - def test_filtered_whitespace(self, caplog): + def test_filtered_whitespace(self, logger, caplog): """Test a message which is filtered (match with whitespace).""" with log.hide_qt_warning("Hello", logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'): - qWarning(" Hello World ") + logger.warning(" Hello World ") assert not caplog.records() diff --git a/tests/utils/usertypes/test_enum.py b/tests/utils/usertypes/test_enum.py index e1443b7be..7298b2861 100644 --- a/tests/utils/usertypes/test_enum.py +++ b/tests/utils/usertypes/test_enum.py @@ -54,3 +54,9 @@ def test_start(): e = usertypes.enum('Enum', ['three', 'four'], start=3) assert e.three.value == 3 assert e.four.value == 4 + + +def test_exit(): + """Make sure the exit status enum is correct.""" + assert usertypes.Exit.ok == 0 + assert usertypes.Exit.reserved == 1 diff --git a/tox.ini b/tox.ini index f39af4ee3..870ee72fd 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = unittests,misc,pep257,flake8,pylint,pyroma,check-manifest +envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest [testenv] basepython = python3 @@ -20,11 +20,11 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/pl passenv = DISPLAY XAUTHORITY HOME deps = -r{toxinidir}/requirements.txt - py==1.4.27 + py==1.4.28 pytest==2.7.1 pytest-capturelog==0.7 - pytest-qt==1.3.0 - pytest-mock==0.5 + pytest-qt==1.4.0 + pytest-mock==0.6.0 pytest-html==1.3.1 # We don't use {[testenv:mkvenv]commands} here because that seems to be broken # on Ubuntu Trusty. @@ -46,8 +46,8 @@ commands = [testenv:misc] commands = {envpython} scripts/misc_checks.py git - {envpython} scripts/misc_checks.py vcs qutebrowser scripts tests - {envpython} scripts/misc_checks.py spelling qutebrowser scripts tests + {envpython} scripts/misc_checks.py vcs + {envpython} scripts/misc_checks.py spelling [testenv:pylint] skip_install = true @@ -61,8 +61,8 @@ deps = six==1.9.0 commands = {[testenv:mkvenv]commands} - {envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no - {envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no + {envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF + {envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF [testenv:pep257] skip_install = true @@ -74,16 +74,40 @@ passenv = LANG # D402: First line should not be function's signature (false-positives) commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D209,D402 '--match=(?!resources|test_content_disposition).*\.py' -[testenv:flake8] -skip_install = true +[testenv:pyflakes] +# https://github.com/fschulze/pytest-flakes/issues/6 +setenv = LANG=en_US.UTF-8 deps = -r{toxinidir}/requirements.txt - pyflakes==0.8.1 - pep8==1.5.7 # rq.filter: <1.6.0 - flake8==2.4.0 + py==1.4.28 + pytest==2.7.1 + pyflakes==0.9.0 + pytest-flakes==1.0.0 commands = {[testenv:mkvenv]commands} - {envdir}/bin/flake8 scripts tests qutebrowser --config=.flake8 + {envpython} -m py.test -q --flakes -m flakes + +[testenv:pep8] +deps = + -r{toxinidir}/requirements.txt + py==1.4.28 + pytest==2.7.1 + pep8==1.6.2 + pytest-pep8==1.0.6 +commands = + {[testenv:mkvenv]commands} + {envpython} -m py.test -q --pep8 -m pep8 + +[testenv:mccabe] +deps = + -r{toxinidir}/requirements.txt + py==1.4.28 + pytest==2.7.1 + mccabe==0.3 + pytest-mccabe==0.1 +commands = + {[testenv:mkvenv]commands} + {envpython} -m py.test -q --mccabe -m mccabe [testenv:pyroma] skip_install = true @@ -129,3 +153,17 @@ commands = norecursedirs = .tox .venv markers = gui: Tests using the GUI (e.g. spawning widgets) +flakes-ignore = + UnusedImport + UnusedVariable + resources.py ALL +pep8ignore = + E265 # Block comment should start with '#' + E501 # Line too long + E402 # module level import not at top of file + E266 # too many leading '#' for block comment + W503 # line break before binary operator + resources.py ALL +mccabe-complexity = 12 +qt_log_level_fail = WARNING +qt_log_ignore = ^SpellCheck: .*