Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Antoni Boucher 2015-06-07 14:38:17 -04:00
commit cf4b89efe3
45 changed files with 594 additions and 444 deletions

13
.flake8
View File

@ -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

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ __pycache__
/htmlcov /htmlcov
/.tox /.tox
/testresults.html /testresults.html
/.cache

View File

@ -4,7 +4,6 @@
ignore=resources.py ignore=resources.py
extension-pkg-whitelist=PyQt5,sip extension-pkg-whitelist=PyQt5,sip
load-plugins=pylint_checkers.config, load-plugins=pylint_checkers.config,
pylint_checkers.crlf,
pylint_checkers.modeline, pylint_checkers.modeline,
pylint_checkers.openencoding, pylint_checkers.openencoding,
pylint_checkers.settrace pylint_checkers.settrace

View File

@ -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 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 (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 `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 Changed
~~~~~~~ ~~~~~~~
@ -45,7 +47,7 @@ Changed
- `:spawn` now shows the command being executed in the statusbar, use `-q`/`--quiet` for the old behavior. - `: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*. - 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. - 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`. - `: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. - `: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) - 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 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 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] https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
----------------------------------------------------------------------- -----------------------------------------------------------------------

View File

@ -91,9 +91,10 @@ unittests and several linters/checkers.
Currently, the following tools will be invoked when you run `tox`: Currently, the following tools will be invoked when you run `tox`:
* Unit tests using the Python * Unit tests using https://www.pytest.org[pytest].
https://docs.python.org/3.4/library/unittest.html[unittest] framework * https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes]
* https://pypi.python.org/pypi/flake8/[flake8] * 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] * https://github.com/GreenSteam/pep257/[pep257]
* http://pylint.org/[pylint] * http://pylint.org/[pylint]
* https://pypi.python.org/pypi/pyroma/[pyroma] * https://pypi.python.org/pypi/pyroma/[pyroma]

View File

@ -28,7 +28,6 @@ include doc/qutebrowser.1.asciidoc
prune tests prune tests
exclude qutebrowser.rcc exclude qutebrowser.rcc
exclude .coveragerc exclude .coveragerc
exclude .flake8
exclude .pylintrc exclude .pylintrc
exclude .eslintrc exclude .eslintrc
exclude doc/help exclude doc/help

View File

@ -138,10 +138,10 @@ Contributors, sorted by the number of commits in descending order:
* Raphael Pierzina * Raphael Pierzina
* Joel Torstensson * Joel Torstensson
* Claude * Claude
* Martin Tournoij
* Artur Shaik * Artur Shaik
* Antoni Boucher * Antoni Boucher
* ZDarian * ZDarian
* Martin Tournoij
* Peter Vilim * Peter Vilim
* John ShaggyTwoDope Jenkins * John ShaggyTwoDope Jenkins
* Jimmy * Jimmy

View File

@ -642,13 +642,14 @@ Save open pages and quit.
[[yank]] [[yank]]
=== yank === yank
Syntax: +:yank [*--title*] [*--sel*]+ Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+
Yank the current URL/title to the clipboard or primary selection. Yank the current URL/title to the clipboard or primary selection.
==== optional arguments ==== optional arguments
* +*-t*+, +*--title*+: Yank the title instead of the URL. * +*-t*+, +*--title*+: Yank the title instead of the URL.
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
[[zoom]] [[zoom]]
=== zoom === zoom
@ -684,6 +685,7 @@ How many steps to zoom out.
[options="header",width="75%",cols="25%,75%"] [options="header",width="75%",cols="25%,75%"]
|============== |==============
|Command|Description |Command|Description
|<<clear-keychain,clear-keychain>>|Clear the currently entered key chain.
|<<command-accept,command-accept>>|Execute the command currently in the commandline. |<<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-next,command-history-next>>|Go forward in the commandline history.
|<<command-history-prev,command-history-prev>>|Go back 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. |<<toggle-selection,toggle-selection>>|Toggle caret selection mode.
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection. |<<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]]
=== command-accept === command-accept
Execute the command currently in the commandline. Execute the command currently in the commandline.

View File

@ -99,7 +99,7 @@
|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed. |<<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,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-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-auto,hide-auto>>|Hide the tab bar if only one tab is open.
|<<tabs-hide-always,hide-always>>|Always hide the tab bar. |<<tabs-hide-always,hide-always>>|Always hide the tab bar.
|<<tabs-wrap,wrap>>|Whether to wrap when changing tabs. |<<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.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.system,downloads.bg.system>>|Color gradient interpolation system for downloads.
|<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors. |<<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'' .Quick reference for section ``fonts''
@ -910,7 +911,7 @@ Default: +pass:[last]+
[[tabs-last-close]] [[tabs-last-close]]
=== last-close === last-close
Behaviour when the last tab is closed. Behavior when the last tab is closed.
Valid values: Valid values:
@ -1433,7 +1434,7 @@ Default: +pass:[true]+
=== next-regexes === next-regexes
A comma-separated list of regexes to use for 'next' links. 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]] [[hints-prev-regexes]]
=== prev-regexes === prev-regexes
@ -1752,6 +1753,12 @@ Background color for downloads with errors.
Default: +pass:[red]+ 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
Fonts used for the UI, with optional style/weight/size. Fonts used for the UI, with optional style/weight/size.

View File

@ -610,7 +610,7 @@ class Quitter:
# event loop, so we can shut down immediately. # event loop, so we can shut down immediately.
self._shutdown(status) self._shutdown(status)
def _shutdown(self, status): # noqa def _shutdown(self, status):
"""Second stage of shutdown.""" """Second stage of shutdown."""
log.destroy.debug("Stage 2 of shutting down...") log.destroy.debug("Stage 2 of shutting down...")
if qApp is None: if qApp is None:

View File

@ -700,19 +700,28 @@ class CommandDispatcher:
frame.scroll(dx, dy) frame.scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher', scope='window') @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. """Yank the current URL/title to the clipboard or primary selection.
Args: Args:
sel: Use the primary selection instead of the clipboard. sel: Use the primary selection instead of the clipboard.
title: Yank the title instead of the URL. title: Yank the title instead of the URL.
domain: Yank only the scheme, domain, and port number.
""" """
clipboard = QApplication.clipboard() clipboard = QApplication.clipboard()
if title: if title:
s = self._tabbed_browser.page_title(self._current_index()) 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: else:
s = self._current_url().toString( s = self._current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword) QUrl.FullyEncoded | QUrl.RemovePassword)
what = 'URL'
if sel and clipboard.supportsSelection(): if sel and clipboard.supportsSelection():
mode = QClipboard.Selection mode = QClipboard.Selection
target = "primary selection" target = "primary selection"
@ -721,8 +730,8 @@ class CommandDispatcher:
target = "clipboard" target = "clipboard"
log.misc.debug("Yanking to {}: '{}'".format(target, s)) log.misc.debug("Yanking to {}: '{}'".format(target, s))
clipboard.setText(s, mode) clipboard.setText(s, mode)
what = 'Title' if title else 'URL' message.info(self._win_id, "Yanked {} to {}: {}".format(
message.info(self._win_id, "{} yanked to {}".format(what, target)) what, target, s))
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
count='count') count='count')

View File

@ -23,14 +23,8 @@ import collections
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
QUrl) QUrl)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslError from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
QSslSocket)
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
SSL_AVAILABLE = False
else:
SSL_AVAILABLE = QSslSocket.supportsSsl()
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
@ -46,13 +40,12 @@ _proxy_auth_cache = {}
def init(): def init():
"""Disable insecure SSL ciphers on old Qt versions.""" """Disable insecure SSL ciphers on old Qt versions."""
if SSL_AVAILABLE: if not qtutils.version_check('5.3.0'):
if not qtutils.version_check('5.3.0'): # Disable weak SSL ciphers.
# Disable weak SSL ciphers. # See https://codereview.qt-project.org/#/c/75943/
# See https://codereview.qt-project.org/#/c/75943/ good_ciphers = [c for c in QSslSocket.supportedCiphers()
good_ciphers = [c for c in QSslSocket.supportedCiphers() if c.usedBits() >= 128]
if c.usedBits() >= 128] QSslSocket.setDefaultCiphers(good_ciphers)
QSslSocket.setDefaultCiphers(good_ciphers)
class SslError(QSslError): class SslError(QSslError):
@ -107,10 +100,9 @@ class NetworkManager(QNetworkAccessManager):
} }
self._set_cookiejar() self._set_cookiejar()
self._set_cache() self._set_cache()
if SSL_AVAILABLE: self.sslErrors.connect(self.on_ssl_errors)
self.sslErrors.connect(self.on_ssl_errors) self._rejected_ssl_errors = collections.defaultdict(list)
self._rejected_ssl_errors = collections.defaultdict(list) self._accepted_ssl_errors = collections.defaultdict(list)
self._accepted_ssl_errors = collections.defaultdict(list)
self.authenticationRequired.connect(self.on_authentication_required) self.authenticationRequired.connect(self.on_authentication_required)
self.proxyAuthenticationRequired.connect( self.proxyAuthenticationRequired.connect(
self.on_proxy_authentication_required) self.on_proxy_authentication_required)
@ -181,76 +173,67 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater() request.deleteLater()
self.shutting_down.emit() self.shutting_down.emit()
if SSL_AVAILABLE: # noqa @pyqtSlot('QNetworkReply*', 'QList<QSslError>')
@pyqtSlot('QNetworkReply*', 'QList<QSslError>') def on_ssl_errors(self, reply, errors): # pragma: no mccabe
def on_ssl_errors(self, reply, errors): """Decide if SSL errors should be ignored or not.
"""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: Args:
reply: The QNetworkReply that is encountering the errors. reply: The QNetworkReply that is encountering the errors.
errors: A list of errors. errors: A list of errors.
""" """
errors = [SslError(e) for e in errors] errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict') ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask': if ssl_strict == 'ask':
try: try:
host_tpl = urlutils.host_tuple(reply.url()) host_tpl = urlutils.host_tuple(reply.url())
except ValueError: except ValueError:
host_tpl = None host_tpl = None
is_accepted = False is_accepted = False
is_rejected = False is_rejected = False
else: else:
is_accepted = set(errors).issubset( is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl]) self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset( is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl]) self._rejected_ssl_errors[host_tpl])
if is_accepted: if is_accepted:
reply.ignoreSslErrors() reply.ignoreSslErrors()
elif is_rejected: 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 pass
else: else:
for err in errors: err_string = '\n'.join('- ' + err.errorString() for err in
# FIXME we might want to use warn here (non-fatal error) errors)
# https://github.com/The-Compiler/qutebrowser/issues/114 answer = self._ask('SSL errors - continue?\n{}'.format(
message.error(self._win_id, err_string), mode=usertypes.PromptMode.yesno,
'SSL error: {}'.format(err.errorString())) owner=reply)
reply.ignoreSslErrors() 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) @pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url): def clear_rejected_ssl_errors(self, url):
"""Clear the rejected SSL errors on a reload. """Clear the rejected SSL errors on a reload.
Args: Args:
url: The URL to remove. url: The URL to remove.
""" """
try: try:
del self._rejected_ssl_errors[url] del self._rejected_ssl_errors[url]
except KeyError: 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.
"""
pass pass
@pyqtSlot('QNetworkReply', 'QAuthenticator') @pyqtSlot('QNetworkReply', 'QAuthenticator')
@ -334,11 +317,7 @@ class NetworkManager(QNetworkAccessManager):
A QNetworkReply. A QNetworkReply.
""" """
scheme = req.url().scheme() scheme = req.url().scheme()
if scheme == 'https' and not SSL_AVAILABLE: if scheme in self._scheme_handlers:
return networkreply.ErrorNetworkReply(
req, "SSL is not supported by the installed Qt library!",
QNetworkReply.ProtocolUnknownError, self)
elif scheme in self._scheme_handlers:
return self._scheme_handlers[scheme].createRequest( return self._scheme_handlers[scheme].createRequest(
op, req, outgoing_data) op, req, outgoing_data)

View File

@ -34,6 +34,7 @@ import configparser
from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
from PyQt5.QtWebKit import QWebSettings
import qutebrowser import qutebrowser
from qutebrowser.browser.network import schemehandler, networkreply from qutebrowser.browser.network import schemehandler, networkreply
@ -96,6 +97,12 @@ class JSBridge(QObject):
@pyqtSlot(int, str, str, str) @pyqtSlot(int, str, str, str)
def set(self, win_id, sectname, optname, value): def set(self, win_id, sectname, optname, value):
"""Slot to set a setting from qute:settings.""" """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: try:
objreg.get('config').set('conf', sectname, optname, value) objreg.get('config').set('conf', sectname, optname, value)
except (configexc.Error, configparser.Error) as e: except (configexc.Error, configparser.Error) as e:
@ -172,10 +179,18 @@ def qute_help(win_id, request):
def qute_settings(win_id, _request): def qute_settings(win_id, _request):
"""Handler for qute:settings. View/change qute configuration.""" """Handler for qute:settings. View/change qute configuration."""
config_getter = functools.partial(objreg.get('config').get, raw=True) if not QWebSettings.globalSettings().testAttribute(
html = jinja.env.get_template('settings.html').render( QWebSettings.JavascriptEnabled):
win_id=win_id, title='settings', config=configdata, # https://github.com/The-Compiler/qutebrowser/issues/727
confget=config_getter) 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') return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@ -312,7 +312,7 @@ def javascript_escape(text):
def get_child_frames(startframe): def get_child_frames(startframe):
"""Get all children recursively of a given QWebFrame. """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: Args:
startframe: The QWebFrame to start with. startframe: The QWebFrame to start with.

View File

@ -109,7 +109,7 @@ class BrowserPage(QWebPage):
def _handle_errorpage(self, info, errpage): def _handle_errorpage(self, info, errpage):
"""Display an error page if needed. """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) (line 260 @ 5d937eb378dd)
Args: Args:
@ -178,7 +178,7 @@ class BrowserPage(QWebPage):
def _handle_multiple_files(self, info, files): def _handle_multiple_files(self, info, files):
"""Handle uploading of multiple files. """Handle uploading of multiple files.
Loosly based on Helpviewer/HelpBrowserWV.py from eric5. Loosely based on Helpviewer/HelpBrowserWV.py from eric5.
Args: Args:
info: The ChooseMultipleFilesExtensionOption instance. info: The ChooseMultipleFilesExtensionOption instance.

View File

@ -24,6 +24,7 @@ import itertools
import functools import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QApplication, QStyleFactory from PyQt5.QtWidgets import QApplication, QStyleFactory
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage from PyQt5.QtWebKitWidgets import QWebView, QWebPage
@ -108,6 +109,7 @@ class WebView(QWebView):
self.search_flags = 0 self.search_flags = 0
self.selection_enabled = False self.selection_enabled = False
self.init_neighborlist() self.init_neighborlist()
self._set_bg_color()
cfg = objreg.get('config') cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist) cfg.changed.connect(self.init_neighborlist)
# For some reason, this signal doesn't get disconnected automatically # 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) return utils.get_repr(self, tab_id=self.tab_id, url=url)
def __del__(self): 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. # when quitting.
# Copied from: # Copied from:
# https://code.google.com/p/webscraping/source/browse/webkit.py#325 # 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 = val
self.load_status_changed.emit(val.name) 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) @pyqtSlot(str, str)
def on_config_changed(self, section, option): def on_config_changed(self, section, option):
"""Reinitialize the zoom neighborlist if related config changed.""" """Reinitialize the zoom neighborlist if related config changed."""
@ -195,6 +206,8 @@ class WebView(QWebView):
self.setContextMenuPolicy(Qt.PreventContextMenu) self.setContextMenuPolicy(Qt.PreventContextMenu)
else: else:
self.setContextMenuPolicy(Qt.DefaultContextMenu) self.setContextMenuPolicy(Qt.DefaultContextMenu)
elif section == 'colors' and option == 'webpage.bg':
self._set_bg_color()
def init_neighborlist(self): def init_neighborlist(self):
"""Initialize the _zoom neighborlist.""" """Initialize the _zoom neighborlist."""
@ -607,6 +620,7 @@ class WebView(QWebView):
"""Save a reference to the context menu so we can close it.""" """Save a reference to the context menu so we can close it."""
menu = self.page().createStandardContextMenu() menu = self.page().createStandardContextMenu()
self.shutting_down.connect(menu.close) self.shutting_down.connect(menu.close)
modeman.instance(self.win_id).entered.connect(menu.close)
menu.exec_(e.globalPos()) menu.exec_(e.globalPos())
def wheelEvent(self, e): def wheelEvent(self, e):

View File

@ -420,7 +420,7 @@ class Command:
value = self._type_conv[param.name](value) value = self._type_conv[param.name](value)
return 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. """Get arguments for a function call.
Args: Args:

View File

@ -148,7 +148,7 @@ class _BaseUserscriptRunner(QObject):
def run(self, cmd, *args, env=None): def run(self, cmd, *args, env=None):
"""Run the userscript given. """Run the userscript given.
Needs to be overridden by superclasses. Needs to be overridden by subclasses.
Args: Args:
cmd: The command to be started. cmd: The command to be started.
@ -160,7 +160,7 @@ class _BaseUserscriptRunner(QObject):
def on_proc_finished(self): def on_proc_finished(self):
"""Called when the process has finished. """Called when the process has finished.
Needs to be overridden by superclasses. Needs to be overridden by subclasses.
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -272,7 +272,7 @@ class Completer(QObject):
pattern = parts[self._cursor_part].strip() pattern = parts[self._cursor_part].strip()
except IndexError: except IndexError:
pattern = '' pattern = ''
self._model().set_pattern(pattern) completion.set_pattern(pattern)
log.completion.debug( log.completion.debug(
"New completion for {}: {}, with pattern '{}'".format( "New completion for {}: {}, with pattern '{}'".format(

View File

@ -201,8 +201,17 @@ class CompletionView(QTreeView):
for i in range(model.rowCount()): for i in range(model.rowCount()):
self.expand(model.index(i, 0)) self.expand(model.index(i, 0))
self._resize_columns() self._resize_columns()
model.rowsRemoved.connect(self.maybe_resize_completion) self.maybe_resize_completion()
model.rowsInserted.connect(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() self.maybe_resize_completion()
@pyqtSlot() @pyqtSlot()

View File

@ -464,7 +464,7 @@ def data(readonly=False):
('last-close', ('last-close',
SettingValue(typ.LastClose(), 'ignore'), SettingValue(typ.LastClose(), 'ignore'),
"Behaviour when the last tab is closed."), "Behavior when the last tab is closed."),
('hide-auto', ('hide-auto',
SettingValue(typ.Bool(), 'false'), SettingValue(typ.Bool(), 'false'),
@ -740,7 +740,8 @@ def data(readonly=False):
('next-regexes', ('next-regexes',
SettingValue(typ.RegexList(flags=re.IGNORECASE), 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."), "A comma-separated list of regexes to use for 'next' links."),
('prev-regexes', ('prev-regexes',
@ -959,6 +960,11 @@ def data(readonly=False):
SettingValue(typ.QtColor(), 'red'), SettingValue(typ.QtColor(), 'red'),
"Background color for downloads with errors."), "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 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([ KEY_DATA = collections.OrderedDict([
('!normal', collections.OrderedDict([ ('!normal', collections.OrderedDict([
@ -1132,7 +1144,7 @@ KEY_DATA = collections.OrderedDict([
])), ])),
('normal', collections.OrderedDict([ ('normal', collections.OrderedDict([
('search', ['<Escape>']), ('search ;; clear-keychain', ['<Escape>']),
('set-cmd-text -s :open', ['o']), ('set-cmd-text -s :open', ['o']),
('set-cmd-text :open {url}', ['go']), ('set-cmd-text :open {url}', ['go']),
('set-cmd-text -s :open -t', ['O']), ('set-cmd-text -s :open -t', ['O']),
@ -1193,6 +1205,8 @@ KEY_DATA = collections.OrderedDict([
('yank -s', ['yY']), ('yank -s', ['yY']),
('yank -t', ['yt']), ('yank -t', ['yt']),
('yank -ts', ['yT']), ('yank -ts', ['yT']),
('yank -d', ['yd']),
('yank -ds', ['yD']),
('paste', ['pp']), ('paste', ['pp']),
('paste -s', ['pP']), ('paste -s', ['pP']),
('paste -t', ['Pp']), ('paste -t', ['Pp']),
@ -1244,8 +1258,8 @@ KEY_DATA = collections.OrderedDict([
('stop', ['<Ctrl-s>']), ('stop', ['<Ctrl-s>']),
('print', ['<Ctrl-Alt-p>']), ('print', ['<Ctrl-Alt-p>']),
('open qute:settings', ['Ss']), ('open qute:settings', ['Ss']),
('follow-selected', ['<Return>']), ('follow-selected', RETURN_KEYS),
('follow-selected -t', ['<Ctrl-Return>']), ('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
])), ])),
('insert', collections.OrderedDict([ ('insert', collections.OrderedDict([
@ -1253,7 +1267,7 @@ KEY_DATA = collections.OrderedDict([
])), ])),
('hint', collections.OrderedDict([ ('hint', collections.OrderedDict([
('follow-hint', ['<Return>', '<Ctrl-M>', '<Ctrl-J>']), ('follow-hint', RETURN_KEYS),
('hint --rapid links tab-bg', ['<Ctrl-R>']), ('hint --rapid links tab-bg', ['<Ctrl-R>']),
('hint links', ['<Ctrl-F>']), ('hint links', ['<Ctrl-F>']),
('hint all tab-bg', ['<Ctrl-B>']), ('hint all tab-bg', ['<Ctrl-B>']),
@ -1266,13 +1280,11 @@ KEY_DATA = collections.OrderedDict([
('command-history-next', ['<Ctrl-N>']), ('command-history-next', ['<Ctrl-N>']),
('completion-item-prev', ['<Shift-Tab>', '<Up>']), ('completion-item-prev', ['<Shift-Tab>', '<Up>']),
('completion-item-next', ['<Tab>', '<Down>']), ('completion-item-next', ['<Tab>', '<Down>']),
('command-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>', ('command-accept', RETURN_KEYS),
'<Ctrl-M>']),
])), ])),
('prompt', collections.OrderedDict([ ('prompt', collections.OrderedDict([
('prompt-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>', ('prompt-accept', RETURN_KEYS),
'<Ctrl-M>']),
('prompt-yes', ['y']), ('prompt-yes', ['y']),
('prompt-no', ['n']), ('prompt-no', ['n']),
])), ])),
@ -1313,7 +1325,7 @@ KEY_DATA = collections.OrderedDict([
('move-to-start-of-document', ['gg']), ('move-to-start-of-document', ['gg']),
('move-to-end-of-document', ['G']), ('move-to-end-of-document', ['G']),
('yank-selected -p', ['Y']), ('yank-selected -p', ['Y']),
('yank-selected', ['y', '<Return>', '<Ctrl-J>']), ('yank-selected', ['y'] + RETURN_KEYS),
('scroll left', ['H']), ('scroll left', ['H']),
('scroll down', ['J']), ('scroll down', ['J']),
('scroll up', ['K']), ('scroll up', ['K']),
@ -1330,8 +1342,8 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^download-page$'), r'download'), (re.compile(r'^download-page$'), r'download'),
(re.compile(r'^cancel-download$'), r'download-cancel'), (re.compile(r'^cancel-download$'), r'download-cancel'),
(re.compile(r'^search ""$'), r'search'), (re.compile(r"""^search (''|"")$"""), r'search ;; clear-keychain'),
(re.compile(r"^search ''$"), r'search'), (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 -s \1'),
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'), (re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),

View File

@ -693,7 +693,7 @@ class FontFamily(Font):
class QtFont(Font): class QtFont(Font):
"""A Font which gets converted to q QFont.""" """A Font which gets converted to a QFont."""
def transform(self, value): def transform(self, value):
if not value: if not value:
@ -1312,7 +1312,7 @@ class SelectOnRemove(BaseType):
class LastClose(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."), valid_values = ValidValues(('ignore', "Don't do anything."),
('blank', "Load a blank page."), ('blank', "Load a blank page."),

View File

@ -23,7 +23,7 @@ import re
import functools import functools
import unicodedata import unicodedata
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils, objreg from qutebrowser.utils import usertypes, log, utils, objreg
@ -49,6 +49,8 @@ class BaseKeyParser(QObject):
special: execute() was called via a special key binding special: execute() was called via a special key binding
do_log: Whether to log keypresses or not. do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
Attributes: Attributes:
bindings: Bound key bindings bindings: Bound key bindings
@ -69,6 +71,7 @@ class BaseKeyParser(QObject):
keystring_updated = pyqtSignal(str) keystring_updated = pyqtSignal(str)
do_log = True do_log = True
passthrough = False
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous', Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
'other', 'none']) 'other', 'none'])
@ -162,12 +165,6 @@ class BaseKeyParser(QObject):
key = e.key() key = e.key()
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) 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: if len(txt) == 1:
category = unicodedata.category(txt) category = unicodedata.category(txt)
is_control_char = (category == 'Cc') is_control_char = (category == 'Cc')
@ -198,7 +195,7 @@ class BaseKeyParser(QObject):
self._keystring = '' self._keystring = ''
self.execute(binding, self.Type.chain, count) self.execute(binding, self.Type.chain, count)
elif match == self.Match.ambiguous: elif match == self.Match.ambiguous:
self._debug_log("Ambigious match for '{}'.".format( self._debug_log("Ambiguous match for '{}'.".format(
self._keystring)) self._keystring))
self._handle_ambiguous_match(binding, count) self._handle_ambiguous_match(binding, count)
elif match == self.Match.partial: elif match == self.Match.partial:
@ -303,6 +300,7 @@ class BaseKeyParser(QObject):
True if the event was handled, False otherwise. True if the event was handled, False otherwise.
""" """
handled = self._handle_special_key(e) handled = self._handle_special_key(e)
if handled or not self._supports_chains: if handled or not self._supports_chains:
return handled return handled
match = self._handle_single_key(e) match = self._handle_single_key(e)
@ -359,3 +357,9 @@ class BaseKeyParser(QObject):
"defined!") "defined!")
if mode == self._modename: if mode == self._modename:
self.read_config() 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)

View File

@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser):
""" """
do_log = False do_log = False
passthrough = True
def __init__(self, win_id, mode, parent=None, warn=True): def __init__(self, win_id, mode, parent=None, warn=True):
"""Constructor. """Constructor.

View File

@ -84,38 +84,30 @@ def init(win_id, parent):
modeman.destroyed.connect( modeman.destroyed.connect(
functools.partial(objreg.delete, 'keyparsers', scope='window', functools.partial(objreg.delete, 'keyparsers', scope='window',
window=win_id)) window=win_id))
modeman.register(KM.normal, keyparsers[KM.normal].handle) for mode, parser in keyparsers.items():
modeman.register(KM.hint, keyparsers[KM.hint].handle) modeman.register(mode, parser)
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)
return modeman return modeman
def _get_modeman(win_id): def instance(win_id):
"""Get a modemanager object.""" """Get a modemanager object."""
return objreg.get('mode-manager', scope='window', window=win_id) return objreg.get('mode-manager', scope='window', window=win_id)
def enter(win_id, mode, reason=None, only_if_normal=False): def enter(win_id, mode, reason=None, only_if_normal=False):
"""Enter the mode 'mode'.""" """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): def leave(win_id, mode, reason=None):
"""Leave the mode 'mode'.""" """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): def maybe_leave(win_id, mode, reason=None):
"""Convenience method to leave 'mode' without exceptions.""" """Convenience method to leave 'mode' without exceptions."""
try: try:
_get_modeman(win_id).leave(mode, reason) instance(win_id).leave(mode, reason)
except NotInModeError as e: except NotInModeError as e:
# This is rather likely to happen, so we only log to debug log. # This is rather likely to happen, so we only log to debug log.
log.modes.debug("{} (leave reason: {})".format(e, reason)) log.modes.debug("{} (leave reason: {})".format(e, reason))
@ -126,10 +118,9 @@ class ModeManager(QObject):
"""Manager for keyboard modes. """Manager for keyboard modes.
Attributes: Attributes:
passthrough: A list of modes in which to pass through events.
mode: The mode we're currently in. mode: The mode we're currently in.
_win_id: The window ID of this ModeManager _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. _forward_unbound_keys: If we should forward unbound keys.
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was _releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
passed through, so the release event should as passed through, so the release event should as
@ -151,8 +142,7 @@ class ModeManager(QObject):
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id self._win_id = win_id
self._handlers = {} self._parsers = {}
self.passthrough = []
self.mode = usertypes.KeyMode.normal self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set() self._releaseevents_to_pass = set()
self._forward_unbound_keys = config.get( self._forward_unbound_keys = config.get(
@ -160,8 +150,7 @@ class ModeManager(QObject):
objreg.get('config').changed.connect(self.set_forward_unbound_keys) objreg.get('config').changed.connect(self.set_forward_unbound_keys)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, mode=self.mode, return utils.get_repr(self, mode=self.mode)
passthrough=self.passthrough)
def _eventFilter_keypress(self, event): def _eventFilter_keypress(self, event):
"""Handle filtering of KeyPress events. """Handle filtering of KeyPress events.
@ -173,11 +162,11 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise. True if event should be filtered, False otherwise.
""" """
curmode = self.mode curmode = self.mode
handler = self._handlers[curmode] parser = self._parsers[curmode]
if curmode != usertypes.KeyMode.insert: if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - calling handler " log.modes.debug("got keypress in mode {} - delegating to "
"{}".format(curmode, utils.qualname(handler))) "{}".format(curmode, utils.qualname(parser)))
handled = handler(event) if handler is not None else False handled = parser.handle(event)
is_non_alnum = bool(event.modifiers()) or not event.text().strip() is_non_alnum = bool(event.modifiers()) or not event.text().strip()
focus_widget = QApplication.instance().focusWidget() focus_widget = QApplication.instance().focusWidget()
@ -187,7 +176,7 @@ class ModeManager(QObject):
filter_this = True filter_this = True
elif is_tab and not isinstance(focus_widget, QWebView): elif is_tab and not isinstance(focus_widget, QWebView):
filter_this = True filter_this = True
elif (curmode in self.passthrough or elif (parser.passthrough or
self._forward_unbound_keys == 'all' or self._forward_unbound_keys == 'all' or
(self._forward_unbound_keys == 'auto' and is_non_alnum)): (self._forward_unbound_keys == 'auto' and is_non_alnum)):
filter_this = False filter_this = False
@ -202,8 +191,8 @@ class ModeManager(QObject):
"passthrough: {}, is_non_alnum: {}, is_tab {} --> " "passthrough: {}, is_non_alnum: {}, is_tab {} --> "
"filter: {} (focused: {!r})".format( "filter: {} (focused: {!r})".format(
handled, self._forward_unbound_keys, handled, self._forward_unbound_keys,
curmode in self.passthrough, is_non_alnum, parser.passthrough, is_non_alnum, is_tab,
is_tab, filter_this, focus_widget)) filter_this, focus_widget))
return filter_this return filter_this
def _eventFilter_keyrelease(self, event): def _eventFilter_keyrelease(self, event):
@ -226,20 +215,16 @@ class ModeManager(QObject):
log.modes.debug("filter: {}".format(filter_this)) log.modes.debug("filter: {}".format(filter_this))
return filter_this return filter_this
def register(self, mode, handler, passthrough=False): def register(self, mode, parser):
"""Register a new mode. """Register a new mode.
Args: Args:
mode: The name of the mode. mode: The name of the mode.
handler: Handler for keyPressEvents. parser: The KeyParser which should be used.
passthrough: Whether to pass key bindings in this mode through to
the widgets.
""" """
if not isinstance(mode, usertypes.KeyMode): assert isinstance(mode, usertypes.KeyMode)
raise TypeError("Mode {} is no KeyMode member!".format(mode)) assert parser is not None
self._handlers[mode] = handler self._parsers[mode] = parser
if passthrough:
self.passthrough.append(mode)
def enter(self, mode, reason=None, only_if_normal=False): def enter(self, mode, reason=None, only_if_normal=False):
"""Enter a new mode. """Enter a new mode.
@ -253,8 +238,8 @@ class ModeManager(QObject):
raise TypeError("Mode {} is no KeyMode member!".format(mode)) raise TypeError("Mode {} is no KeyMode member!".format(mode))
log.modes.debug("Entering mode {}{}".format( log.modes.debug("Entering mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason))) mode, '' if reason is None else ' (reason: {})'.format(reason)))
if mode not in self._handlers: if mode not in self._parsers:
raise ValueError("No handler for mode {}".format(mode)) raise ValueError("No keyparser for mode {}".format(mode))
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno) prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
if self.mode == mode or (self.mode in prompt_modes and if self.mode == mode or (self.mode in prompt_modes and
mode in prompt_modes): mode in prompt_modes):
@ -332,3 +317,8 @@ class ModeManager(QObject):
return self._eventFilter_keypress(event) return self._eventFilter_keypress(event)
else: else:
return self._eventFilter_keyrelease(event) 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()

View File

@ -224,6 +224,8 @@ class CaretKeyParser(keyparser.CommandKeyParser):
"""KeyParser for caret mode.""" """KeyParser for caret mode."""
passthrough = True
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True, super().__init__(win_id, parent, supports_count=True,
supports_chains=True) supports_chains=True)

View File

@ -469,9 +469,9 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode): def on_mode_entered(self, mode):
"""Mark certain modes in the commandline.""" """Mark certain modes in the commandline."""
mode_manager = objreg.get('mode-manager', scope='window', keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id) window=self._win_id)
if mode in mode_manager.passthrough: if keyparsers[mode].passthrough:
self._set_mode_text(mode.name) self._set_mode_text(mode.name)
if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret):
self.set_mode_active(mode, True) self.set_mode_active(mode, True)
@ -479,10 +479,10 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
def on_mode_left(self, old_mode, new_mode): def on_mode_left(self, old_mode, new_mode):
"""Clear marked mode.""" """Clear marked mode."""
mode_manager = objreg.get('mode-manager', scope='window', keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id) window=self._win_id)
if old_mode in mode_manager.passthrough: if keyparsers[old_mode].passthrough:
if new_mode in mode_manager.passthrough: if keyparsers[new_mode].passthrough:
self._set_mode_text(new_mode.name) self._set_mode_text(new_mode.name)
else: else:
self.txt.set_text(self.txt.Text.normal, '') self.txt.set_text(self.txt.Text.normal, '')

View File

@ -296,7 +296,7 @@ class TabbedBrowser(tabwidget.TabWidget):
newtab: True to open URL in a new tab, False otherwise. newtab: True to open URL in a new tab, False otherwise.
""" """
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
if newtab: if newtab or self.currentWidget() is None:
self.tabopen(url, background=False) self.tabopen(url, background=False)
else: else:
self.currentWidget().openurl(url) self.currentWidget().openurl(url)
@ -332,7 +332,7 @@ class TabbedBrowser(tabwidget.TabWidget):
the default settings we handle it like Chromium does: the default settings we handle it like Chromium does:
- Tabs from clicked links etc. are to the right of - Tabs from clicked links etc. are to the right of
the current. the current.
- Explicitely opened tabs are at the very right. - Explicitly opened tabs are at the very right.
Return: Return:
The opened WebView instance. The opened WebView instance.

View File

@ -183,7 +183,7 @@ class _CrashDialog(QDialog):
def _init_text(self): def _init_text(self):
"""Initialize the main text to be displayed on an exception. """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, self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
textInteractionFlags=Qt.LinksAccessibleByMouse) textInteractionFlags=Qt.LinksAccessibleByMouse)
self._vbox.addWidget(self._lbl) self._vbox.addWidget(self._lbl)

View File

@ -190,7 +190,7 @@ class CrashHandler(QObject):
objects = "" objects = ""
return ExceptionInfo(pages, cmd_history, 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. """Handle uncaught python exceptions.
It'll try very hard to write all open tabs to a file, and then exit It'll try very hard to write all open tabs to a file, and then exit

View File

@ -213,6 +213,19 @@ def check_qt_version():
_die(text) _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(): def check_libraries():
"""Check if all needed Python libraries are installed.""" """Check if all needed Python libraries are installed."""
modules = { modules = {
@ -288,6 +301,7 @@ def earlyinit(args):
# Now we can be sure QtCore is available, so we can print dialogs on # 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. # errors, so people only using the GUI notice them as well.
check_qt_version() check_qt_version()
check_ssl_support()
remove_inputhook() remove_inputhook()
check_libraries() check_libraries()
init_log(args) init_log(args)

View File

@ -77,7 +77,7 @@ class CommandLineEdit(QLineEdit):
def __on_cursor_position_changed(self, _old, new): def __on_cursor_position_changed(self, _old, new):
"""Prevent the cursor moving to the prompt. """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: if new < self._promptlen:
self.setCursorPosition(self._promptlen) self.setCursorPosition(self._promptlen)

View File

@ -55,7 +55,7 @@ class ShellLexer:
self.token = '' self.token = ''
self.state = ' ' self.state = ' '
def __iter__(self): # noqa def __iter__(self): # pragma: no mccabe
"""Read a raw token from the input stream.""" """Read a raw token from the input stream."""
# pylint: disable=too-many-branches,too-many-statements # pylint: disable=too-many-branches,too-many-statements
self.reset() self.reset()

View File

@ -377,7 +377,7 @@ class RAMHandler(logging.Handler):
"""Logging handler which keeps the messages in a deque in RAM. """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. uses a simple list rather than a deque.
Attributes: Attributes:

View File

@ -73,7 +73,7 @@ class NeighborList(collections.abc.Sequence):
Args: Args:
items: The list of items to iterate in. items: The list of items to iterate in.
_default: The initially selected value. _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.block: Stay on the selected item
Modes.wrap: Wrap around to the other end Modes.wrap: Wrap around to the other end
Modes.exception: Raise an IndexError. 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 statuses for errors. Needs to be an int for sys.exit.
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', 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): class Question(QObject):

View File

@ -29,10 +29,7 @@ import collections
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
from PyQt5.QtWebKit import qWebKitVersion from PyQt5.QtWebKit import qWebKitVersion
try: from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtNetwork import QSslSocket
except ImportError:
QSslSocket = None
import qutebrowser import qutebrowser
from qutebrowser.utils import log, utils from qutebrowser.utils import log, utils
@ -127,18 +124,8 @@ def _module_versions():
A list of lines with version info. A list of lines with version info.
""" """
lines = [] 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([ modules = collections.OrderedDict([
('sip', ['SIP_VERSION_STR']),
('colorlog', []), ('colorlog', []),
('colorama', ['VERSION', '__version__']), ('colorama', ['VERSION', '__version__']),
('pypeg2', ['__version__']), ('pypeg2', ['__version__']),
@ -209,16 +196,13 @@ def version():
'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()), 'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()),
'PyQt: {}'.format(PYQT_VERSION_STR), 'PyQt: {}'.format(PYQT_VERSION_STR),
] ]
lines += _module_versions() lines += _module_versions()
if QSslSocket is not None and QSslSocket.supportsSsl():
ssl_version = QSslSocket.sslLibraryVersionString()
else:
ssl_version = 'unavailable'
lines += [ lines += [
'Webkit: {}'.format(qWebKitVersion()), 'Webkit: {}'.format(qWebKitVersion()),
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')), 'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
'SSL: {}'.format(ssl_version), 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
'', '',
'Frozen: {}'.format(hasattr(sys, 'frozen')), 'Frozen: {}'.format(hasattr(sys, 'frozen')),
'Platform: {}, {}'.format(platform.platform(), 'Platform: {}, {}'.format(platform.platform(),

View File

@ -35,9 +35,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils from scripts import utils
def _py_files(target): def _py_files():
"""Iterate over all python files and yield filenames.""" """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')): for name in (e for e in filenames if e.endswith('.py')):
yield os.path.join(dirpath, name) yield os.path.join(dirpath, name)
@ -64,31 +68,32 @@ def check_git():
return status return status
def check_spelling(target): def check_spelling():
"""Check commonly misspelled words.""" """Check commonly misspelled words."""
# Words which I often misspell # Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully', words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully',
'occur[^r .]', 'seperator', 'explicitely', 'resetted', '[Oo]ccur[^r .]', '[Ss]eperator', '[Ee]xplicitely', '[Rr]esetted',
'auxillary', 'accidentaly', 'ambigious', 'loosly', '[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly',
'initialis', 'convienence', 'similiar', 'uncommited', '[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
'reproducable'} '[Rr]eproducable'}
# Words which look better when splitted, but might need some fine tuning. # Words which look better when splitted, but might need some fine tuning.
words |= {'keystrings', 'webelements', 'mouseevent', 'keysequence', words |= {'[Kk]eystrings', '[Ww]ebelements', '[Mm]ouseevent',
'normalmode', 'eventloops', 'sizehint', 'statemachine', '[Kk]eysequence', '[Nn]ormalmode', '[Ee]ventloops',
'metaobject', 'logrecord', 'filetype'} '[Ss]izehint', '[Ss]tatemachine', '[Mm]etaobject',
'[Ll]ogrecord', '[Ff]iletype'}
seen = collections.defaultdict(list) seen = collections.defaultdict(list)
try: try:
ok = True ok = True
for fn in _py_files(target): for fn in _py_files():
with tokenize.open(fn) as f: 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 continue
for line in f: for line in f:
for w in words: for w in words:
if re.search(w, line) and fn not in seen[w]: 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) seen[w].append(fn)
ok = False ok = False
print() print()
@ -98,11 +103,11 @@ def check_spelling(target):
return None return None
def check_vcs_conflict(target): def check_vcs_conflict():
"""Check VCS conflict markers.""" """Check VCS conflict markers."""
try: try:
ok = True ok = True
for fn in _py_files(target): for fn in _py_files():
with tokenize.open(fn) as f: with tokenize.open(fn) as f:
for line in f: for line in f:
if any(line.startswith(c * 7) for c in '<>=|'): if any(line.startswith(c * 7) for c in '<>=|'):
@ -120,25 +125,14 @@ def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('checker', choices=('git', 'vcs', 'spelling'), parser.add_argument('checker', choices=('git', 'vcs', 'spelling'),
help="Which checker to run.") help="Which checker to run.")
parser.add_argument('target', help="What to check", nargs='*')
args = parser.parse_args() args = parser.parse_args()
if args.checker == 'git': if args.checker == 'git':
ok = check_git() ok = check_git()
return 0 if ok else 1
elif args.checker == 'vcs': elif args.checker == 'vcs':
is_ok = True ok = check_vcs_conflict()
for target in args.target:
ok = check_vcs_conflict(target)
if not ok:
is_ok = False
return 0 if is_ok else 1
elif args.checker == 'spelling': elif args.checker == 'spelling':
is_ok = True ok = check_spelling()
for target in args.target: return 0 if ok else 1
ok = check_spelling(target)
if not ok:
is_ok = False
return 0 if is_ok else 1
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -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))

View File

@ -197,8 +197,10 @@ class TestKeyConfigParser:
('download-page', 'download'), ('download-page', 'download'),
('cancel-download', 'download-cancel'), ('cancel-download', 'download-cancel'),
('search ""', 'search'), ('search ""', 'search ;; clear-keychain'),
("search ''", 'search'), ("search ''", 'search ;; clear-keychain'),
("search", 'search ;; clear-keychain'),
("search ;; foobar", None),
('search "foo"', None), ('search "foo"', None),
('set-cmd-text "foo bar"', 'set-cmd-text foo bar'), ('set-cmd-text "foo bar"', 'set-cmd-text foo bar'),

View File

@ -80,8 +80,10 @@ class JSTester:
def scroll_anchor(self, name): def scroll_anchor(self, name):
"""Scroll the main frame to the given anchor.""" """Scroll the main frame to the given anchor."""
page = self.webview.page() page = self.webview.page()
with self._qtbot.waitSignal(page.scrollRequested): old_pos = page.mainFrame().scrollPosition()
page.mainFrame().scrollToAnchor(name) page.mainFrame().scrollToAnchor(name)
new_pos = page.mainFrame().scrollPosition()
assert old_pos != new_pos
def load(self, path, **kwargs): def load(self, path, **kwargs):
"""Load and display the given test data. """Load and display the given test data.
@ -92,7 +94,7 @@ class JSTester:
**kwargs: Passed to jinja's template.render(). **kwargs: Passed to jinja's template.render().
""" """
template = self._jinja_env.get_template(path) 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)) self.webview.setHtml(template.render(**kwargs))
def run_file(self, filename): def run_file(self, filename):

View File

@ -39,6 +39,7 @@ def progress_widget(qtbot, monkeypatch, config_stub):
'qutebrowser.mainwindow.statusbar.progress.style.config', config_stub) 'qutebrowser.mainwindow.statusbar.progress.style.config', config_stub)
widget = Progress() widget = Progress()
qtbot.add_widget(widget) qtbot.add_widget(widget)
widget.setGeometry(200, 200, 200, 200)
assert not widget.isVisible() assert not widget.isVisible()
assert not widget.isTextVisible() assert not widget.isTextVisible()
return widget return widget

View File

@ -21,144 +21,256 @@
# pylint: disable=protected-access # pylint: disable=protected-access
import re
import inspect import inspect
from unittest import mock
from PyQt5.QtWidgets import QLineEdit from PyQt5.QtWidgets import QLineEdit, QApplication
import pytest import pytest
from qutebrowser.misc import readline 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 @pytest.fixture
def mocked_qapp(monkeypatch, stubs): def lineedit(qtbot, monkeypatch):
"""Fixture that mocks readline.QApplication and returns it.""" """Fixture providing a LineEdit."""
stub = stubs.FakeQApplication() le = LineEdit()
monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub) qtbot.add_widget(le)
return stub monkeypatch.setattr(QApplication.instance(), 'focusWidget', lambda: le)
return le
class TestNoneWidget: @pytest.fixture
def bridge():
"""Test if there are no exceptions when the widget is None.""" """Fixture providing a ReadlineBridge."""
return readline.ReadlineBridge()
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: 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) @pytest.mark.parametrize('text, expected', [('f<oo>bar', 'fo|obar'),
def setup(self): ('|foobar', '|foobar')])
self.qle = mock.Mock() def test_rl_backward_char(text, expected, lineedit, bridge):
self.qle.__class__ = QLineEdit """Test rl_backward_char."""
self.bridge = readline.ReadlineBridge() 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): @pytest.mark.parametrize('text, expected', [('f<oo>bar', 'foob|ar'),
"""Test rl_backward_char.""" ('foobar|', 'foobar|')])
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) def test_rl_forward_char(text, expected, lineedit, bridge):
self.bridge.rl_backward_char() """Test rl_forward_char."""
self.qle.cursorBackward.assert_called_with(False) 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): @pytest.mark.parametrize('text, expected', [('one <tw>o', 'one |two'),
"""Test rl_backward_word.""" ('<one >two', '|one two'),
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) ('|one two', '|one two')])
self.bridge.rl_backward_word() def test_rl_backward_word(text, expected, lineedit, bridge):
self.qle.cursorWordBackward.assert_called_with(False) """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): @pytest.mark.parametrize('text, expected', [
"""Test rl_beginning_of_line.""" fixme(('<o>ne two', 'one| two')),
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) ('<o>ne two', 'one |two'), # wrong
self.bridge.rl_beginning_of_line() fixme(('<one> two', 'one two|')),
self.qle.home.assert_called_with(False) ('<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_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): def test_rl_beginning_of_line(lineedit, bridge):
"""Test rl_delete_char.""" """Test rl_beginning_of_line."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) lineedit.set_aug_text('f<oo>bar')
self.bridge.rl_delete_char() bridge.rl_beginning_of_line()
self.qle.del_.assert_called_with() 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): def test_rl_end_of_line(lineedit, bridge):
"""Set a selected text, delete it, see if it comes back with yank.""" """Test rl_end_of_line."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) lineedit.set_aug_text('f<oo>bar')
self._set_selected_text("delete test") bridge.rl_end_of_line()
self.bridge.rl_unix_line_discard() assert lineedit.aug_text() == 'foobar|'
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): @pytest.mark.parametrize('text, expected', [('foo|bar', 'foo|ar'),
"""Set a selected text, delete it, see if it comes back with yank.""" ('foobar|', 'foobar|'),
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) ('|foobar', '|oobar'),
self._set_selected_text("delete test") ('f<oo>bar', 'f|bar')])
self.bridge.rl_unix_word_rubout() def test_rl_delete_char(text, expected, lineedit, bridge):
self.qle.cursorWordBackward.assert_called_with(True) """Test rl_delete_char."""
assert self.bridge._deleted[self.qle] == "delete test" lineedit.set_aug_text(text)
self.qle.del_.assert_called_with() bridge.rl_delete_char()
self.bridge.rl_yank() assert lineedit.aug_text() == expected
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): @pytest.mark.parametrize('text, expected', [('foo|bar', 'fo|bar'),
"""Test yank without having deleted anything.""" ('foobar|', 'fooba|'),
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle) ('|foobar', '|foobar'),
self.bridge.rl_yank() ('f<oo>bar', 'f|bar')])
assert not self.qle.insert.called 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() == '|'

View File

@ -27,7 +27,6 @@ import itertools
import sys import sys
import pytest import pytest
from PyQt5.QtCore import qWarning
from qutebrowser.utils import log from qutebrowser.utils import log
@ -214,7 +213,7 @@ class TestInitLog:
@pytest.fixture @pytest.fixture
def args(self): def args(self):
"""Fixture providing an argparse namespace.""" """Fixture providing an argparse namespace for init_log."""
return argparse.Namespace(debug=True, loglevel=logging.DEBUG, return argparse.Namespace(debug=True, loglevel=logging.DEBUG,
color=True, loglines=10, logfilter="") color=True, loglines=10, logfilter="")
@ -230,33 +229,37 @@ class TestHideQtWarning:
"""Tests for hide_qt_warning/QtWarningFilter.""" """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.""" """Test a message which is not filtered."""
with log.hide_qt_warning("World", logger='qt-tests'): with log.hide_qt_warning("World", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello World") logger.warning("Hello World")
assert len(caplog.records()) == 1 assert len(caplog.records()) == 1
record = caplog.records()[0] record = caplog.records()[0]
assert record.levelname == 'WARNING' assert record.levelname == 'WARNING'
assert record.message == "Hello World" 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).""" """Test a message which is filtered (exact match)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello") logger.warning("Hello")
assert not caplog.records() 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).""" """Test a message which is filtered (match at line start)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello World") logger.warning("Hello World")
assert not caplog.records() 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).""" """Test a message which is filtered (match with whitespace)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning(" Hello World ") logger.warning(" Hello World ")
assert not caplog.records() assert not caplog.records()

View File

@ -54,3 +54,9 @@ def test_start():
e = usertypes.enum('Enum', ['three', 'four'], start=3) e = usertypes.enum('Enum', ['three', 'four'], start=3)
assert e.three.value == 3 assert e.three.value == 3
assert e.four.value == 4 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

66
tox.ini
View File

@ -4,7 +4,7 @@
# and then run "tox" from this directory. # and then run "tox" from this directory.
[tox] [tox]
envlist = unittests,misc,pep257,flake8,pylint,pyroma,check-manifest envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest
[testenv] [testenv]
basepython = python3 basepython = python3
@ -20,11 +20,11 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/pl
passenv = DISPLAY XAUTHORITY HOME passenv = DISPLAY XAUTHORITY HOME
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
py==1.4.27 py==1.4.28
pytest==2.7.1 pytest==2.7.1
pytest-capturelog==0.7 pytest-capturelog==0.7
pytest-qt==1.3.0 pytest-qt==1.4.0
pytest-mock==0.5 pytest-mock==0.6.0
pytest-html==1.3.1 pytest-html==1.3.1
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken # We don't use {[testenv:mkvenv]commands} here because that seems to be broken
# on Ubuntu Trusty. # on Ubuntu Trusty.
@ -46,8 +46,8 @@ commands =
[testenv:misc] [testenv:misc]
commands = commands =
{envpython} scripts/misc_checks.py git {envpython} scripts/misc_checks.py git
{envpython} scripts/misc_checks.py vcs qutebrowser scripts tests {envpython} scripts/misc_checks.py vcs
{envpython} scripts/misc_checks.py spelling qutebrowser scripts tests {envpython} scripts/misc_checks.py spelling
[testenv:pylint] [testenv:pylint]
skip_install = true skip_install = true
@ -61,8 +61,8 @@ deps =
six==1.9.0 six==1.9.0
commands = commands =
{[testenv:mkvenv]commands} {[testenv:mkvenv]commands}
{envdir}/bin/pylint scripts qutebrowser --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 {envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
[testenv:pep257] [testenv:pep257]
skip_install = true skip_install = true
@ -74,16 +74,40 @@ passenv = LANG
# D402: First line should not be function's signature (false-positives) # 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' commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D209,D402 '--match=(?!resources|test_content_disposition).*\.py'
[testenv:flake8] [testenv:pyflakes]
skip_install = true # https://github.com/fschulze/pytest-flakes/issues/6
setenv = LANG=en_US.UTF-8
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
pyflakes==0.8.1 py==1.4.28
pep8==1.5.7 # rq.filter: <1.6.0 pytest==2.7.1
flake8==2.4.0 pyflakes==0.9.0
pytest-flakes==1.0.0
commands = commands =
{[testenv:mkvenv]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] [testenv:pyroma]
skip_install = true skip_install = true
@ -129,3 +153,17 @@ commands =
norecursedirs = .tox .venv norecursedirs = .tox .venv
markers = markers =
gui: Tests using the GUI (e.g. spawning widgets) 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: .*