Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
cf4b89efe3
13
.flake8
13
.flake8
@ -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
1
.gitignore
vendored
@ -23,3 +23,4 @@ __pycache__
|
||||
/htmlcov
|
||||
/.tox
|
||||
/testresults.html
|
||||
/.cache
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
-----------------------------------------------------------------------
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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[>→≫]\b,\b(>>|»)\b]+
|
||||
Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,\bcontinue\b]+
|
||||
|
||||
[[hints-prev-regexes]]
|
||||
=== prev-regexes
|
||||
@ -1752,6 +1753,12 @@ Background color for downloads with errors.
|
||||
|
||||
Default: +pass:[red]+
|
||||
|
||||
[[colors-webpage.bg]]
|
||||
=== webpage.bg
|
||||
Background color for webpages if unset (or empty to use the theme's color)
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
== fonts
|
||||
Fonts used for the UI, with optional style/weight/size.
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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'),
|
||||
|
@ -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."),
|
||||
|
@ -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)
|
||||
|
@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser):
|
||||
"""
|
||||
|
||||
do_log = False
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, mode, parent=None, warn=True):
|
||||
"""Constructor.
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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, '')
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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(),
|
||||
|
@ -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__':
|
||||
|
@ -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))
|
@ -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'),
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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() == '|'
|
||||
|
@ -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()
|
||||
|
@ -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
66
tox.ini
@ -4,7 +4,7 @@
|
||||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist = unittests,misc,pep257,flake8,pylint,pyroma,check-manifest
|
||||
envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
@ -20,11 +20,11 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/pl
|
||||
passenv = DISPLAY XAUTHORITY HOME
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
py==1.4.27
|
||||
py==1.4.28
|
||||
pytest==2.7.1
|
||||
pytest-capturelog==0.7
|
||||
pytest-qt==1.3.0
|
||||
pytest-mock==0.5
|
||||
pytest-qt==1.4.0
|
||||
pytest-mock==0.6.0
|
||||
pytest-html==1.3.1
|
||||
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
|
||||
# on Ubuntu Trusty.
|
||||
@ -46,8 +46,8 @@ commands =
|
||||
[testenv:misc]
|
||||
commands =
|
||||
{envpython} scripts/misc_checks.py git
|
||||
{envpython} scripts/misc_checks.py vcs qutebrowser scripts tests
|
||||
{envpython} scripts/misc_checks.py spelling qutebrowser scripts tests
|
||||
{envpython} scripts/misc_checks.py vcs
|
||||
{envpython} scripts/misc_checks.py spelling
|
||||
|
||||
[testenv:pylint]
|
||||
skip_install = true
|
||||
@ -61,8 +61,8 @@ deps =
|
||||
six==1.9.0
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no
|
||||
{envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no
|
||||
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
|
||||
{envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
|
||||
|
||||
[testenv:pep257]
|
||||
skip_install = true
|
||||
@ -74,16 +74,40 @@ passenv = LANG
|
||||
# D402: First line should not be function's signature (false-positives)
|
||||
commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D209,D402 '--match=(?!resources|test_content_disposition).*\.py'
|
||||
|
||||
[testenv:flake8]
|
||||
skip_install = true
|
||||
[testenv:pyflakes]
|
||||
# https://github.com/fschulze/pytest-flakes/issues/6
|
||||
setenv = LANG=en_US.UTF-8
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
pyflakes==0.8.1
|
||||
pep8==1.5.7 # rq.filter: <1.6.0
|
||||
flake8==2.4.0
|
||||
py==1.4.28
|
||||
pytest==2.7.1
|
||||
pyflakes==0.9.0
|
||||
pytest-flakes==1.0.0
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envdir}/bin/flake8 scripts tests qutebrowser --config=.flake8
|
||||
{envpython} -m py.test -q --flakes -m flakes
|
||||
|
||||
[testenv:pep8]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
py==1.4.28
|
||||
pytest==2.7.1
|
||||
pep8==1.6.2
|
||||
pytest-pep8==1.0.6
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envpython} -m py.test -q --pep8 -m pep8
|
||||
|
||||
[testenv:mccabe]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
py==1.4.28
|
||||
pytest==2.7.1
|
||||
mccabe==0.3
|
||||
pytest-mccabe==0.1
|
||||
commands =
|
||||
{[testenv:mkvenv]commands}
|
||||
{envpython} -m py.test -q --mccabe -m mccabe
|
||||
|
||||
[testenv:pyroma]
|
||||
skip_install = true
|
||||
@ -129,3 +153,17 @@ commands =
|
||||
norecursedirs = .tox .venv
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
flakes-ignore =
|
||||
UnusedImport
|
||||
UnusedVariable
|
||||
resources.py ALL
|
||||
pep8ignore =
|
||||
E265 # Block comment should start with '#'
|
||||
E501 # Line too long
|
||||
E402 # module level import not at top of file
|
||||
E266 # too many leading '#' for block comment
|
||||
W503 # line break before binary operator
|
||||
resources.py ALL
|
||||
mccabe-complexity = 12
|
||||
qt_log_level_fail = WARNING
|
||||
qt_log_ignore = ^SpellCheck: .*
|
||||
|
Loading…
Reference in New Issue
Block a user