Merge branch 'master' of github.com:The-Compiler/qutebrowser

This commit is contained in:
Lamar Pavel 2015-06-05 16:10:55 +02:00
commit 402aa66756
38 changed files with 535 additions and 282 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

@ -22,3 +22,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

@ -33,6 +33,11 @@ Added
- New argument `--no-err-windows` to suppress all error windows.
- New visual/caret mode (bound to `v`) to select text by keyboard.
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
- New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom).
- 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.
Changed
~~~~~~~
@ -41,7 +46,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.
@ -75,6 +80,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,13 +138,14 @@ Contributors, sorted by the number of commits in descending order:
* Raphael Pierzina
* Joel Torstensson
* Claude
* Martin Tournoij
* Artur Shaik
* Antoni Boucher
* ZDarian
* Peter Vilim
* John ShaggyTwoDope Jenkins
* Jimmy
* Zach-Button
* Martin Tournoij
* rikn00
* Patric Schmitz
* Martin Zimmermann

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
@ -692,6 +693,7 @@ How many steps to zoom out.
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|<<enter-mode,enter-mode>>|Enter a key mode.
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|<<follow-selected,follow-selected>>|Follow the selected text.
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|<<message-error,message-error>>|Show an error message in the statusbar.
|<<message-info,message-info>>|Show an info message in the statusbar.
@ -774,6 +776,15 @@ Enter a key mode.
=== follow-hint
Follow the currently selected hint.
[[follow-selected]]
=== follow-selected
Syntax: +:follow-selected [*--tab*]+
Follow the selected text.
==== optional arguments
* +*-t*+, +*--tab*+: Load the selected link in a new tab.
[[leave-mode]]
=== leave-mode
Leave the mode we're currently in.
@ -1009,7 +1020,7 @@ multiplier
[[scroll-page]]
=== scroll-page
Syntax: +:scroll-page 'x' 'y'+
Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+
Scroll the frame page-wise.
@ -1017,6 +1028,12 @@ Scroll the frame page-wise.
* +'x'+: How many pages to scroll to the right.
* +'y'+: How many pages to scroll down.
==== optional arguments
* +*-t*+, +*--top-navigate*+: :navigate action (prev, decrement) to run when scrolling up at the top of the page.
* +*-b*+, +*--bottom-navigate*+: :navigate action (next, increment) to run when scrolling down at the bottom of the page.
==== count
multiplier

View File

@ -45,6 +45,7 @@
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|==============
.Quick reference for section ``network''
@ -220,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''
@ -594,6 +596,17 @@ Valid values:
Default: +pass:[false]+
[[ui-modal-js-dialog]]
=== modal-js-dialog
Use standard JavaScript modal dialog for alert() and confirm()
Valid values:
* +true+
* +false+
Default: +pass:[false]+
== network
Settings related to the network.
@ -1740,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

@ -606,7 +606,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

@ -25,7 +25,9 @@ import shlex
import subprocess
import posixpath
import functools
import xml.etree.ElementTree
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import Qt, QUrl, QEvent
from PyQt5.QtGui import QClipboard, QKeyEvent
@ -643,14 +645,37 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', count='count')
def scroll_page(self, x: {'type': float}, y: {'type': float}, count=1):
def scroll_page(self, x: {'type': float}, y: {'type': float}, *,
top_navigate: {'type': ('prev', 'decrement'),
'metavar': 'ACTION'}=None,
bottom_navigate: {'type': ('next', 'increment'),
'metavar': 'ACTION'}=None,
count=1):
"""Scroll the frame page-wise.
Args:
x: How many pages to scroll to the right.
y: How many pages to scroll down.
bottom_navigate: :navigate action (next, increment) to run when
scrolling down at the bottom of the page.
top_navigate: :navigate action (prev, decrement) to run when
scrolling up at the top of the page.
count: multiplier
"""
frame = self._current_widget().page().currentFrame()
if not frame.url().isValid():
# See https://github.com/The-Compiler/qutebrowser/issues/701
return
if (bottom_navigate is not None and
frame.scrollPosition().y() >=
frame.scrollBarMaximum(Qt.Vertical)):
self.navigate(bottom_navigate)
return
elif top_navigate is not None and frame.scrollPosition().y() == 0:
self.navigate(top_navigate)
return
mult_x = count * x
mult_y = count * y
if mult_y.is_integer():
@ -663,7 +688,6 @@ class CommandDispatcher:
mult_y = 0
if mult_x == 0 and mult_y == 0:
return
frame = self._current_widget().page().currentFrame()
size = frame.geometry()
dx = mult_x * size.width()
dy = mult_y * size.height()
@ -672,19 +696,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"
@ -693,8 +726,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')
@ -985,6 +1018,39 @@ class CommandDispatcher:
url = objreg.get('quickmark-manager').get(name)
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def follow_selected(self, tab=False):
"""Follow the selected text.
Args:
tab: Load the selected link in a new tab.
"""
widget = self._current_widget()
page = widget.page()
if not page.hasSelection():
return
if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
if tab:
page.open_target = usertypes.ClickTarget.tab
page.currentFrame().evaluateJavaScript(
'window.getSelection().anchorNode.parentNode.click()')
else:
try:
selected_element = xml.etree.ElementTree.fromstring(
'<html>' + widget.selectedHtml() + '</html>').find('a')
except xml.etree.ElementTree.ParseError:
raise cmdexc.CommandError('Could not parse selected element!')
if selected_element is not None:
try:
url = selected_element.attrib['href']
except KeyError:
raise cmdexc.CommandError('Anchor element without href!')
url = self._current_url().resolved(QUrl(url))
self._open(url, tab)
@cmdutils.register(instance='command-dispatcher', name='inspector',
scope='window')
def toggle_inspector(self):

View File

@ -181,7 +181,7 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater()
self.shutting_down.emit()
if SSL_AVAILABLE: # noqa
if SSL_AVAILABLE: # pragma: no mccabe
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors):
"""Decide if SSL errors should be ignored or not.

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

@ -478,17 +478,23 @@ class BrowserPage(QWebPage):
return super().extension(ext, opt, out)
return handler(opt, out)
def javaScriptAlert(self, _frame, msg):
def javaScriptAlert(self, frame, msg):
"""Override javaScriptAlert to use the statusbar."""
log.js.debug("alert: {}".format(msg))
if config.get('ui', 'modal-js-dialog'):
return super().javaScriptAlert(frame, msg)
if (self._is_shutting_down or
config.get('content', 'ignore-javascript-alert')):
return
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
def javaScriptConfirm(self, _frame, msg):
def javaScriptConfirm(self, frame, msg):
"""Override javaScriptConfirm to use the statusbar."""
log.js.debug("confirm: {}".format(msg))
if config.get('ui', 'modal-js-dialog'):
return super().javaScriptConfirm(frame, msg)
if self._is_shutting_down:
return False
ans = self._ask("[js confirm] {}".format(msg),

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

@ -61,7 +61,8 @@ class Command:
"""
AnnotationInfo = collections.namedtuple('AnnotationInfo',
['kwargs', 'type', 'flag', 'hide'])
['kwargs', 'type', 'flag', 'hide',
'metavar'])
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, completion=None, modes=None, not_modes=None,
@ -257,10 +258,10 @@ class Command:
pass
if isinstance(typ, tuple):
pass
kwargs['metavar'] = annotation_info.metavar or param.name
elif utils.is_enum(typ):
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
kwargs['metavar'] = param.name
kwargs['metavar'] = annotation_info.metavar or param.name
elif typ is bool:
kwargs['action'] = 'store_true'
elif typ is not None:
@ -322,11 +323,12 @@ class Command:
flag: The short name/flag if overridden.
name: The long name if overridden.
"""
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False}
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
'metavar': None}
if param.annotation is not inspect.Parameter.empty:
log.commands.vdebug("Parsing annotation {}".format(
param.annotation))
for field in ('type', 'flag', 'name', 'hide'):
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
if field in param.annotation:
info[field] = param.annotation[field]
if 'nargs' in param.annotation:
@ -418,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

@ -305,6 +305,10 @@ def data(readonly=False):
SettingValue(typ.Bool(), 'false'),
"Whether to hide the mouse cursor."),
('modal-js-dialog',
SettingValue(typ.Bool(), 'false'),
"Use standard JavaScript modal dialog for alert() and confirm()"),
readonly=readonly
)),
@ -955,6 +959,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
)),
@ -1121,6 +1130,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([
@ -1189,6 +1204,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']),
@ -1239,6 +1256,8 @@ KEY_DATA = collections.OrderedDict([
('stop', ['<Ctrl-s>']),
('print', ['<Ctrl-Alt-p>']),
('open qute:settings', ['Ss']),
('follow-selected', RETURN_KEYS),
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
])),
('insert', collections.OrderedDict([
@ -1246,7 +1265,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>']),
@ -1259,13 +1278,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']),
])),
@ -1306,7 +1323,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']),

View File

@ -694,7 +694,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:

View File

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

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

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)

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

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

@ -242,7 +242,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

@ -127,18 +127,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__']),

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

@ -44,6 +44,9 @@ def progress_widget(qtbot, monkeypatch, config_stub):
return widget
@pytest.mark.xfail(
reason='Blacklisted because it could cause random segfaults - see '
'https://github.com/hackebrot/qutebrowser/issues/22', run=False)
def test_load_started(progress_widget):
"""Ensure the Progress widget reacts properly when the page starts loading.

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

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

54
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
@ -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.27
pytest==2.7.1
pyflakes==0.9.0
pytest-flakes==0.2
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.27
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.27
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,15 @@ 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