summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flake813
-rw-r--r--.gitignore1
-rw-r--r--.pylintrc1
-rw-r--r--CHANGELOG.asciidoc6
-rw-r--r--CONTRIBUTING.asciidoc7
-rw-r--r--MANIFEST.in1
-rw-r--r--README.asciidoc2
-rw-r--r--doc/help/commands.asciidoc8
-rw-r--r--doc/help/settings.asciidoc13
-rw-r--r--qutebrowser/app.py2
-rw-r--r--qutebrowser/browser/commands.py15
-rw-r--r--qutebrowser/browser/network/networkmanager.py157
-rw-r--r--qutebrowser/browser/network/qutescheme.py23
-rw-r--r--qutebrowser/browser/webelem.py2
-rw-r--r--qutebrowser/browser/webpage.py4
-rw-r--r--qutebrowser/browser/webview.py16
-rw-r--r--qutebrowser/commands/command.py2
-rw-r--r--qutebrowser/commands/userscripts.py4
-rw-r--r--qutebrowser/completion/completer.py2
-rw-r--r--qutebrowser/completion/completionwidget.py13
-rw-r--r--qutebrowser/config/configdata.py38
-rw-r--r--qutebrowser/config/configtypes.py4
-rw-r--r--qutebrowser/keyinput/basekeyparser.py20
-rw-r--r--qutebrowser/keyinput/keyparser.py1
-rw-r--r--qutebrowser/keyinput/modeman.py66
-rw-r--r--qutebrowser/keyinput/modeparsers.py2
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py14
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py4
-rw-r--r--qutebrowser/misc/crashdialog.py2
-rw-r--r--qutebrowser/misc/crashsignal.py2
-rw-r--r--qutebrowser/misc/earlyinit.py14
-rw-r--r--qutebrowser/misc/miscwidgets.py2
-rw-r--r--qutebrowser/misc/split.py2
-rw-r--r--qutebrowser/utils/log.py2
-rw-r--r--qutebrowser/utils/usertypes.py4
-rw-r--r--qutebrowser/utils/version.py24
-rw-r--r--scripts/misc_checks.py54
-rw-r--r--scripts/pylint_checkers/crlf.py45
-rw-r--r--tests/config/test_config.py6
-rw-r--r--tests/javascript/conftest.py8
-rw-r--r--tests/mainwindow/statusbar/test_progress.py1
-rw-r--r--tests/misc/test_readline.py378
-rw-r--r--tests/utils/test_log.py23
-rw-r--r--tests/utils/usertypes/test_enum.py6
-rw-r--r--tox.ini66
45 files changed, 615 insertions, 465 deletions
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 `<Escape>` 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 `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
-- `<Ctrl-M>` is now accepted as an additional alias for `<Return>`/`<Ctrl-J>`
+- `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` 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-keychain,clear-keychain>>|Clear the currently entered key chain.
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
@@ -738,6 +740,10 @@ How many steps to zoom out.
|<<toggle-selection,toggle-selection>>|Toggle caret selection mode.
|<<yank-selected,yank-selected>>|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 @@
|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed.
|<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned.
|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitly are positioned.
-|<<tabs-last-close,last-close>>|Behaviour when the last tab is closed.
+|<<tabs-last-close,last-close>>|Behavior when the last tab is closed.
|<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open.
|<<tabs-hide-always,hide-always>>|Always hide the tab bar.
|<<tabs-wrap,wrap>>|Whether to wrap when changing tabs.
@@ -221,6 +221,7 @@
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient end for downloads.
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for downloads.
|<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors.
+|<<colors-webpage.bg,webpage.bg>>|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[&gt;→≫]\b,\b(&gt;&gt;|»)\b]+
+Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[&gt;→≫]\b,\b(&gt;&gt;|»)\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<QSslError>')
- def on_ssl_errors(self, reply, errors):
- """Decide if SSL errors should be ignored or not.
-
- 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:
- 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('QNetworkReply*', 'QList<QSslError>')
+ def on_ssl_errors(self, reply, errors): # pragma: no mccabe
+ """Decide if SSL errors should be ignored or not.
- @pyqtSlot(QUrl)
- def clear_rejected_ssl_errors(self, url):
- """Clear the rejected SSL errors on a reload.
+ This slot is called on SSL/TLS errors by the self.sslErrors signal.
- Args:
- url: The URL to remove.
- """
+ 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:
- del self._rejected_ssl_errors[url]
- except KeyError:
+ 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:
- @pyqtSlot(QUrl)
- def clear_rejected_ssl_errors(self, _url):
- """Clear the rejected SSL errors on a reload.
+ 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:
+ 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()
- Does nothing because SSL is unavailable.
- """
+ @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
@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 = ['<Return>', '<Ctrl-M>', '<Ctrl-J>', '<Shift-Return>', '<Enter>',
+ '<Shift-Enter>']
+
KEY_DATA = collections.OrderedDict([
('!normal', collections.OrderedDict([
@@ -1132,7 +1144,7 @@ KEY_DATA = collections.OrderedDict([
])),
('normal', collections.OrderedDict([
- ('search', ['<Escape>']),
+ ('search ;; clear-keychain', ['<Escape>']),
('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', ['<Ctrl-s>']),
('print', ['<Ctrl-Alt-p>']),
('open qute:settings', ['Ss']),
- ('follow-selected', ['<Return>']),
- ('follow-selected -t', ['<Ctrl-Return>']),
+ ('follow-selected', RETURN_KEYS),
+ ('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
])),
('insert', collections.OrderedDict([
@@ -1253,7 +1267,7 @@ KEY_DATA = collections.OrderedDict([
])),
('hint', collections.OrderedDict([
- ('follow-hint', ['<Return>', '<Ctrl-M>', '<Ctrl-J>']),
+ ('follow-hint', RETURN_KEYS),
('hint --rapid links tab-bg', ['<Ctrl-R>']),
('hint links', ['<Ctrl-F>']),
('hint all tab-bg', ['<Ctrl-B>']),
@@ -1266,13 +1280,11 @@ KEY_DATA = collections.OrderedDict([
('command-history-next', ['<Ctrl-N>']),
('completion-item-prev', ['<Shift-Tab>', '<Up>']),
('completion-item-next', ['<Tab>', '<Down>']),
- ('command-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>',
- '<Ctrl-M>']),
+ ('command-accept', RETURN_KEYS),
])),
('prompt', collections.OrderedDict([
- ('prompt-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>',
- '<Ctrl-M>']),
+ ('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', '<Return>', '<Ctrl-J>']),
+ ('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) <mail@qutebrowser.org>
-# 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 <http://www.gnu.org/licenses/>.
-
-"""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 <!")
+
+ if cursor_pos is not None:
+ if sel_start_pos is not None or sel_end_pos is not None:
+ raise ValueError("Can't mix | and </>!")
+ 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 lineedit(qtbot, monkeypatch):
+ """Fixture providing a LineEdit."""
+ le = LineEdit()
+ qtbot.add_widget(le)
+ monkeypatch.setattr(QApplication.instance(), 'focusWidget', lambda: le)
+ return le
+
+
@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
-
-
-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()
-
-
-class TestReadlineBridgeTest:
-
- """Tests for readline bridge."""
-
- @pytest.fixture(autouse=True)
- def setup(self):
- self.qle = mock.Mock()
- self.qle.__class__ = QLineEdit
- self.bridge = readline.ReadlineBridge()
-
- 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)
-
- 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)
-
- 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)
-
- 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_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_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")
-
- 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
+def bridge():
+ """Fixture providing a ReadlineBridge."""
+ return readline.ReadlineBridge()
+
+
+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()
+
+
+@pytest.mark.parametrize('text, expected', [('f<oo>bar', '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
+
+
+@pytest.mark.parametrize('text, expected', [('f<oo>bar', '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
+
+
+@pytest.mark.parametrize('text, expected', [('one <tw>o', 'one |two'),
+ ('<one >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
+
+
+@pytest.mark.parametrize('text, expected', [
+ fixme(('<o>ne two', 'one| two')),
+ ('<o>ne two', 'one |two'), # wrong
+ fixme(('<one> two', 'one two|')),
+ ('<one> two', 'one |two'), # wrong
+ ('one t<wo>', '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_beginning_of_line(lineedit, bridge):
+ """Test rl_beginning_of_line."""
+ lineedit.set_aug_text('f<oo>bar')
+ bridge.rl_beginning_of_line()
+ assert lineedit.aug_text() == '|foobar'
+
+
+def test_rl_end_of_line(lineedit, bridge):
+ """Test rl_end_of_line."""
+ lineedit.set_aug_text('f<oo>bar')
+ bridge.rl_end_of_line()
+ assert lineedit.aug_text() == 'foobar|'
+
+
+@pytest.mark.parametrize('text, expected', [('foo|bar', 'foo|ar'),
+ ('foobar|', 'foobar|'),
+ ('|foobar', '|oobar'),
+ ('f<oo>bar', '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
+
+
+@pytest.mark.parametrize('text, expected', [('foo|bar', 'fo|bar'),
+ ('foobar|', 'fooba|'),
+ ('|foobar', '|foobar'),
+ ('f<oo>bar', '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 <this> test', 'delete this', '| test')),
+ ('delete <this> test', 'delete ', '|this test'), # wrong
+ fixme(('f<oo>bar', 'foo', '|bar')),
+ ('f<oo>bar', '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(('<test >delete this', 'test delete this', 'test |')),
+ ('<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 del<ete>foobar', 'delete', 'test |foobar')),
+ ('test del<ete >foobar', '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<bar> delete', ' delete', 'test foobar|')),
+ ('test foo<bar>delete', '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
+ py==1.4.28
+ pytest==2.7.1
+ pyflakes==0.9.0
+ pytest-flakes==1.0.0
+commands =
+ {[testenv:mkvenv]commands}
+ {envpython} -m py.test -q --flakes -m flakes
+
+[testenv:pep8]
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
+ 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}
- {envdir}/bin/flake8 scripts tests qutebrowser --config=.flake8
+ {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: .*