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
/.tox
/testresults.html
/.cache

View File

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

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 (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]
-----------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors): # pragma: no mccabe
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
try:
host_tpl = urlutils.host_tuple(reply.url())
except ValueError:
host_tpl = None
is_accepted = False
is_rejected = False
else:
is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl])
if is_accepted:
reply.ignoreSslErrors()
elif is_rejected:
pass
else:
err_string = '\n'.join('- ' + err.errorString() for err in
errors)
answer = self._ask('SSL errors - continue?\n{}'.format(
err_string), mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
d = self._accepted_ssl_errors
else:
d = self._rejected_ssl_errors
if host_tpl is not None:
d[host_tpl] += errors
elif ssl_strict:
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
try:
host_tpl = urlutils.host_tuple(reply.url())
except ValueError:
host_tpl = None
is_accepted = False
is_rejected = False
else:
is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl])
if is_accepted:
reply.ignoreSslErrors()
elif is_rejected:
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
err_string = '\n'.join('- ' + err.errorString() for err in
errors)
answer = self._ask('SSL errors - continue?\n{}'.format(
err_string), mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
d = self._accepted_ssl_errors
else:
d = self._rejected_ssl_errors
if host_tpl is not None:
d[host_tpl] += errors
elif ssl_strict:
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url):
"""Clear the rejected SSL errors on a reload.
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url):
"""Clear the rejected SSL errors on a reload.
Args:
url: The URL to remove.
"""
try:
del self._rejected_ssl_errors[url]
except KeyError:
pass
else:
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, _url):
"""Clear the rejected SSL errors on a reload.
Does nothing because SSL is unavailable.
"""
Args:
url: The URL to remove.
"""
try:
del self._rejected_ssl_errors[url]
except KeyError:
pass
@pyqtSlot('QNetworkReply', 'QAuthenticator')
@ -334,11 +317,7 @@ class NetworkManager(QNetworkAccessManager):
A QNetworkReply.
"""
scheme = req.url().scheme()
if scheme == 'https' and not SSL_AVAILABLE:
return networkreply.ErrorNetworkReply(
req, "SSL is not supported by the installed Qt library!",
QNetworkReply.ProtocolUnknownError, self)
elif scheme in self._scheme_handlers:
if scheme in self._scheme_handlers:
return self._scheme_handlers[scheme].createRequest(
op, req, outgoing_data)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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."),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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__':

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'),
('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'),

View File

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

View File

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

View File

@ -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 mocked_qapp(monkeypatch, stubs):
"""Fixture that mocks readline.QApplication and returns it."""
stub = stubs.FakeQApplication()
monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub)
return stub
def lineedit(qtbot, monkeypatch):
"""Fixture providing a LineEdit."""
le = LineEdit()
qtbot.add_widget(le)
monkeypatch.setattr(QApplication.instance(), 'focusWidget', lambda: le)
return le
class TestNoneWidget:
"""Test if there are no exceptions when the widget is None."""
def test_none(self, mocked_qapp):
"""Call each rl_* method with a None focusWidget."""
self.bridge = readline.ReadlineBridge()
mocked_qapp.focusWidget = mock.Mock(return_value=None)
for name, method in inspect.getmembers(self.bridge, inspect.ismethod):
if name.startswith('rl_'):
method()
@pytest.fixture
def bridge():
"""Fixture providing a ReadlineBridge."""
return readline.ReadlineBridge()
class TestReadlineBridgeTest:
def test_none(bridge, qtbot):
"""Call each rl_* method with a None focusWidget."""
assert QApplication.instance().focusWidget() is None
for name, method in inspect.getmembers(bridge, inspect.ismethod):
if name.startswith('rl_'):
method()
"""Tests for readline bridge."""
@pytest.fixture(autouse=True)
def setup(self):
self.qle = mock.Mock()
self.qle.__class__ = QLineEdit
self.bridge = readline.ReadlineBridge()
@pytest.mark.parametrize('text, expected', [('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
def _set_selected_text(self, text):
"""Set the value the fake QLineEdit should return for selectedText."""
self.qle.configure_mock(**{'selectedText.return_value': text})
def test_rl_backward_char(self, mocked_qapp):
"""Test rl_backward_char."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_backward_char()
self.qle.cursorBackward.assert_called_with(False)
@pytest.mark.parametrize('text, expected', [('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
def test_rl_forward_char(self, mocked_qapp):
"""Test rl_forward_char."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_forward_char()
self.qle.cursorForward.assert_called_with(False)
def test_rl_backward_word(self, mocked_qapp):
"""Test rl_backward_word."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_backward_word()
self.qle.cursorWordBackward.assert_called_with(False)
@pytest.mark.parametrize('text, expected', [('one <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
def test_rl_forward_word(self, mocked_qapp):
"""Test rl_forward_word."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_forward_word()
self.qle.cursorWordForward.assert_called_with(False)
def test_rl_beginning_of_line(self, mocked_qapp):
"""Test rl_beginning_of_line."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_beginning_of_line()
self.qle.home.assert_called_with(False)
@pytest.mark.parametrize('text, expected', [
fixme(('<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_end_of_line(self, mocked_qapp):
"""Test rl_end_of_line."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_end_of_line()
self.qle.end.assert_called_with(False)
def test_rl_delete_char(self, mocked_qapp):
"""Test rl_delete_char."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_delete_char()
self.qle.del_.assert_called_with()
def test_rl_beginning_of_line(lineedit, bridge):
"""Test rl_beginning_of_line."""
lineedit.set_aug_text('f<oo>bar')
bridge.rl_beginning_of_line()
assert lineedit.aug_text() == '|foobar'
def test_rl_backward_delete_char(self, mocked_qapp):
"""Test rl_backward_delete_char."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_backward_delete_char()
self.qle.backspace.assert_called_with()
def test_rl_unix_line_discard(self, mocked_qapp):
"""Set a selected text, delete it, see if it comes back with yank."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self._set_selected_text("delete test")
self.bridge.rl_unix_line_discard()
self.qle.home.assert_called_with(True)
assert self.bridge._deleted[self.qle] == "delete test"
self.qle.del_.assert_called_with()
self.bridge.rl_yank()
self.qle.insert.assert_called_with("delete test")
def test_rl_end_of_line(lineedit, bridge):
"""Test rl_end_of_line."""
lineedit.set_aug_text('f<oo>bar')
bridge.rl_end_of_line()
assert lineedit.aug_text() == 'foobar|'
def test_rl_kill_line(self, mocked_qapp):
"""Set a selected text, delete it, see if it comes back with yank."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self._set_selected_text("delete test")
self.bridge.rl_kill_line()
self.qle.end.assert_called_with(True)
assert self.bridge._deleted[self.qle] == "delete test"
self.qle.del_.assert_called_with()
self.bridge.rl_yank()
self.qle.insert.assert_called_with("delete test")
def test_rl_unix_word_rubout(self, mocked_qapp):
"""Set a selected text, delete it, see if it comes back with yank."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self._set_selected_text("delete test")
self.bridge.rl_unix_word_rubout()
self.qle.cursorWordBackward.assert_called_with(True)
assert self.bridge._deleted[self.qle] == "delete test"
self.qle.del_.assert_called_with()
self.bridge.rl_yank()
self.qle.insert.assert_called_with("delete test")
@pytest.mark.parametrize('text, expected', [('foo|bar', 'foo|ar'),
('foobar|', 'foobar|'),
('|foobar', '|oobar'),
('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
def test_rl_kill_word(self, mocked_qapp):
"""Set a selected text, delete it, see if it comes back with yank."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self._set_selected_text("delete test")
self.bridge.rl_kill_word()
self.qle.cursorWordForward.assert_called_with(True)
assert self.bridge._deleted[self.qle] == "delete test"
self.qle.del_.assert_called_with()
self.bridge.rl_yank()
self.qle.insert.assert_called_with("delete test")
def test_rl_yank_no_text(self, mocked_qapp):
"""Test yank without having deleted anything."""
mocked_qapp.focusWidget = mock.Mock(return_value=self.qle)
self.bridge.rl_yank()
assert not self.qle.insert.called
@pytest.mark.parametrize('text, expected', [('foo|bar', 'fo|bar'),
('foobar|', 'fooba|'),
('|foobar', '|foobar'),
('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() == '|'

View File

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

View File

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

66
tox.ini
View File

@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
envlist = unittests,misc,pep257,flake8,pylint,pyroma,check-manifest
envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest
[testenv]
basepython = python3
@ -20,11 +20,11 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/pl
passenv = DISPLAY XAUTHORITY HOME
deps =
-r{toxinidir}/requirements.txt
py==1.4.27
py==1.4.28
pytest==2.7.1
pytest-capturelog==0.7
pytest-qt==1.3.0
pytest-mock==0.5
pytest-qt==1.4.0
pytest-mock==0.6.0
pytest-html==1.3.1
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
# on Ubuntu Trusty.
@ -46,8 +46,8 @@ commands =
[testenv:misc]
commands =
{envpython} scripts/misc_checks.py git
{envpython} scripts/misc_checks.py vcs qutebrowser scripts tests
{envpython} scripts/misc_checks.py spelling qutebrowser scripts tests
{envpython} scripts/misc_checks.py vcs
{envpython} scripts/misc_checks.py spelling
[testenv:pylint]
skip_install = true
@ -61,8 +61,8 @@ deps =
six==1.9.0
commands =
{[testenv:mkvenv]commands}
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no
{envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
{envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
[testenv:pep257]
skip_install = true
@ -74,16 +74,40 @@ passenv = LANG
# D402: First line should not be function's signature (false-positives)
commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D209,D402 '--match=(?!resources|test_content_disposition).*\.py'
[testenv:flake8]
skip_install = true
[testenv:pyflakes]
# https://github.com/fschulze/pytest-flakes/issues/6
setenv = LANG=en_US.UTF-8
deps =
-r{toxinidir}/requirements.txt
pyflakes==0.8.1
pep8==1.5.7 # rq.filter: <1.6.0
flake8==2.4.0
py==1.4.28
pytest==2.7.1
pyflakes==0.9.0
pytest-flakes==1.0.0
commands =
{[testenv:mkvenv]commands}
{envdir}/bin/flake8 scripts tests qutebrowser --config=.flake8
{envpython} -m py.test -q --flakes -m flakes
[testenv:pep8]
deps =
-r{toxinidir}/requirements.txt
py==1.4.28
pytest==2.7.1
pep8==1.6.2
pytest-pep8==1.0.6
commands =
{[testenv:mkvenv]commands}
{envpython} -m py.test -q --pep8 -m pep8
[testenv:mccabe]
deps =
-r{toxinidir}/requirements.txt
py==1.4.28
pytest==2.7.1
mccabe==0.3
pytest-mccabe==0.1
commands =
{[testenv:mkvenv]commands}
{envpython} -m py.test -q --mccabe -m mccabe
[testenv:pyroma]
skip_install = true
@ -129,3 +153,17 @@ commands =
norecursedirs = .tox .venv
markers =
gui: Tests using the GUI (e.g. spawning widgets)
flakes-ignore =
UnusedImport
UnusedVariable
resources.py ALL
pep8ignore =
E265 # Block comment should start with '#'
E501 # Line too long
E402 # module level import not at top of file
E266 # too many leading '#' for block comment
W503 # line break before binary operator
resources.py ALL
mccabe-complexity = 12
qt_log_level_fail = WARNING
qt_log_ignore = ^SpellCheck: .*