diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 29d9c8be4..000000000 --- a/.flake8 +++ /dev/null @@ -1,13 +0,0 @@ -# vim: ft=dosini fileencoding=utf-8: - -[flake8] -# E265: Block comment should start with '#' -# E501: Line too long -# F841: unused variable -# F401: Unused import -# E402: module level import not at top of file -# E266: too many leading '#' for block comment -# W503: line break before binary operator -ignore=E265,E501,F841,F401,E402,E266,W503 -max_complexity = 12 -exclude=resources.py diff --git a/.gitignore b/.gitignore index f3ff3652a..a4c699d38 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ __pycache__ /htmlcov /.tox /testresults.html +/.cache diff --git a/.pylintrc b/.pylintrc index 2cc56909d..a4abb32a0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,7 +4,6 @@ ignore=resources.py extension-pkg-whitelist=PyQt5,sip load-plugins=pylint_checkers.config, - pylint_checkers.crlf, pylint_checkers.modeline, pylint_checkers.openencoding, pylint_checkers.settrace diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2981578e6..c2daf1efe 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -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 `` (rapid), `` (foreground) and `` (background) to switch hint modes while hinting. -- `` is now accepted as an additional alias for ``/`` +- `` and numpad-enter are now bound by default for bindings where `` 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] ----------------------------------------------------------------------- diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 7b0fe6087..1975a9d7c 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -91,9 +91,10 @@ unittests and several linters/checkers. Currently, the following tools will be invoked when you run `tox`: -* Unit tests using the Python -https://docs.python.org/3.4/library/unittest.html[unittest] framework -* https://pypi.python.org/pypi/flake8/[flake8] +* Unit tests using https://www.pytest.org[pytest]. +* https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes] +* https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8] +* https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe] * https://github.com/GreenSteam/pep257/[pep257] * http://pylint.org/[pylint] * https://pypi.python.org/pypi/pyroma/[pyroma] diff --git a/MANIFEST.in b/MANIFEST.in index 7ecd44de2..4092f81c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,7 +28,6 @@ include doc/qutebrowser.1.asciidoc prune tests exclude qutebrowser.rcc exclude .coveragerc -exclude .flake8 exclude .pylintrc exclude .eslintrc exclude doc/help diff --git a/README.asciidoc b/README.asciidoc index c651ffc33..3a701fd76 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -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 diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index c33bccaaf..104478c10 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -642,13 +642,14 @@ Save open pages and quit. [[yank]] === yank -Syntax: +:yank [*--title*] [*--sel*]+ +Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+ Yank the current URL/title to the clipboard or primary selection. ==== optional arguments * +*-t*+, +*--title*+: Yank the title instead of the URL. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. +* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number. [[zoom]] === zoom @@ -692,6 +693,7 @@ How many steps to zoom out. |<>|Drop selection and keep selection mode enabled. |<>|Enter a key mode. |<>|Follow the currently selected hint. +|<>|Follow the selected text. |<>|Leave the mode we're currently in. |<>|Show an error message in the statusbar. |<>|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 diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index d3c571aa3..fc4802232 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -45,6 +45,7 @@ |<>|Whether to hide the statusbar unless a message is shown. |<>|The format to use for the window title. The following placeholders are defined: |<>|Whether to hide the mouse cursor. +|<>|Use standard JavaScript modal dialog for alert() and confirm() |============== .Quick reference for section ``network'' @@ -220,6 +221,7 @@ |<>|Color gradient end for downloads. |<>|Color gradient interpolation system for downloads. |<>|Background color for downloads with errors. +|<>|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. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index d125d1d1a..2e8f7ea6a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -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: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0c01260d7..9bfe81a10 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -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( + '' + widget.selectedHtml() + '').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): diff --git a/qutebrowser/browser/network/networkmanager.py b/qutebrowser/browser/network/networkmanager.py index 3b4c71ed0..a3fc76baf 100644 --- a/qutebrowser/browser/network/networkmanager.py +++ b/qutebrowser/browser/network/networkmanager.py @@ -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') def on_ssl_errors(self, reply, errors): """Decide if SSL errors should be ignored or not. diff --git a/qutebrowser/browser/network/qutescheme.py b/qutebrowser/browser/network/qutescheme.py index 0432fa6fc..48b3dbd5f 100644 --- a/qutebrowser/browser/network/qutescheme.py +++ b/qutebrowser/browser/network/qutescheme.py @@ -34,6 +34,7 @@ import configparser from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtNetwork import QNetworkReply +from PyQt5.QtWebKit import QWebSettings import qutebrowser from qutebrowser.browser.network import schemehandler, networkreply @@ -96,6 +97,12 @@ class JSBridge(QObject): @pyqtSlot(int, str, str, str) def set(self, win_id, sectname, optname, value): """Slot to set a setting from qute:settings.""" + # https://github.com/The-Compiler/qutebrowser/issues/727 + if (sectname, optname == 'content', 'allow-javascript' and + value == 'false'): + message.error(win_id, "Refusing to disable javascript via " + "qute:settings as it needs javascript support.") + return try: objreg.get('config').set('conf', sectname, optname, value) except (configexc.Error, configparser.Error) as e: @@ -172,10 +179,18 @@ def qute_help(win_id, request): def qute_settings(win_id, _request): """Handler for qute:settings. View/change qute configuration.""" - config_getter = functools.partial(objreg.get('config').get, raw=True) - html = jinja.env.get_template('settings.html').render( - win_id=win_id, title='settings', config=configdata, - confget=config_getter) + if not QWebSettings.globalSettings().testAttribute( + QWebSettings.JavascriptEnabled): + # https://github.com/The-Compiler/qutebrowser/issues/727 + template = jinja.env.get_template('pre.html') + html = template.render( + title='Failed to open qute:settings.', + content="qute:settings needs javascript enabled to work.") + else: + config_getter = functools.partial(objreg.get('config').get, raw=True) + html = jinja.env.get_template('settings.html').render( + win_id=win_id, title='settings', config=configdata, + confget=config_getter) return html.encode('UTF-8', errors='xmlcharrefreplace') diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 005a6e300..8e430efcb 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -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), diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 06176064d..ec82d2567 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -24,6 +24,7 @@ import itertools import functools from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl +from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QApplication, QStyleFactory from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebView, QWebPage @@ -108,6 +109,7 @@ class WebView(QWebView): self.search_flags = 0 self.selection_enabled = False self.init_neighborlist() + self._set_bg_color() cfg = objreg.get('config') cfg.changed.connect(self.init_neighborlist) # For some reason, this signal doesn't get disconnected automatically @@ -181,6 +183,15 @@ class WebView(QWebView): self.load_status = val self.load_status_changed.emit(val.name) + def _set_bg_color(self): + """Set the webpage background color as configured.""" + col = config.get('colors', 'webpage.bg') + palette = self.palette() + if col is None: + col = self.style().standardPalette().color(QPalette.Base) + palette.setColor(QPalette.Base, col) + self.setPalette(palette) + @pyqtSlot(str, str) def on_config_changed(self, section, option): """Reinitialize the zoom neighborlist if related config changed.""" @@ -195,6 +206,8 @@ class WebView(QWebView): self.setContextMenuPolicy(Qt.PreventContextMenu) else: self.setContextMenuPolicy(Qt.DefaultContextMenu) + elif section == 'colors' and option == 'webpage.bg': + self._set_bg_color() def init_neighborlist(self): """Initialize the _zoom neighborlist.""" @@ -607,6 +620,7 @@ class WebView(QWebView): """Save a reference to the context menu so we can close it.""" menu = self.page().createStandardContextMenu() self.shutting_down.connect(menu.close) + modeman.instance(self.win_id).entered.connect(menu.close) menu.exec_(e.globalPos()) def wheelEvent(self, e): diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 998c4a892..4bda15f8a 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -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: diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 85c511165..76e9f94a6 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -148,7 +148,7 @@ class _BaseUserscriptRunner(QObject): def run(self, cmd, *args, env=None): """Run the userscript given. - Needs to be overridden by superclasses. + Needs to be overridden by subclasses. Args: cmd: The command to be started. @@ -160,7 +160,7 @@ class _BaseUserscriptRunner(QObject): def on_proc_finished(self): """Called when the process has finished. - Needs to be overridden by superclasses. + Needs to be overridden by subclasses. """ raise NotImplementedError diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 2fd1858ca..197c62ce1 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -272,7 +272,7 @@ class Completer(QObject): pattern = parts[self._cursor_part].strip() except IndexError: pattern = '' - self._model().set_pattern(pattern) + completion.set_pattern(pattern) log.completion.debug( "New completion for {}: {}, with pattern '{}'".format( diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index aa2fd31da..0bd6b04c9 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -201,8 +201,17 @@ class CompletionView(QTreeView): for i in range(model.rowCount()): self.expand(model.index(i, 0)) self._resize_columns() - model.rowsRemoved.connect(self.maybe_resize_completion) - model.rowsInserted.connect(self.maybe_resize_completion) + self.maybe_resize_completion() + + def set_pattern(self, pattern): + """Set the completion pattern for the current model. + + Called from on_update_completion(). + + Args: + pattern: The filter pattern to set (what the user entered). + """ + self.model().set_pattern(pattern) self.maybe_resize_completion() @pyqtSlot() diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 02b8c6008..ad269d212 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -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 = ['', '', '', '', '', + ''] + 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', ['']), ('print', ['']), ('open qute:settings', ['Ss']), + ('follow-selected', RETURN_KEYS), + ('follow-selected -t', ['', '']), ])), ('insert', collections.OrderedDict([ @@ -1246,7 +1265,7 @@ KEY_DATA = collections.OrderedDict([ ])), ('hint', collections.OrderedDict([ - ('follow-hint', ['', '', '']), + ('follow-hint', RETURN_KEYS), ('hint --rapid links tab-bg', ['']), ('hint links', ['']), ('hint all tab-bg', ['']), @@ -1259,13 +1278,11 @@ KEY_DATA = collections.OrderedDict([ ('command-history-next', ['']), ('completion-item-prev', ['', '']), ('completion-item-next', ['', '']), - ('command-accept', ['', '', '', - '']), + ('command-accept', RETURN_KEYS), ])), ('prompt', collections.OrderedDict([ - ('prompt-accept', ['', '', '', - '']), + ('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', '', '']), + ('yank-selected', ['y'] + RETURN_KEYS), ('scroll left', ['H']), ('scroll down', ['J']), ('scroll up', ['K']), diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 94bc278bf..267ae8789 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -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: diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index b52a39824..1c2c9ab86 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -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']) diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index bef364e66..46f179fdb 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser): """ do_log = False + passthrough = True def __init__(self, win_id, mode, parent=None, warn=True): """Constructor. diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index fc70ac76b..357a5ffc9 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -84,38 +84,30 @@ def init(win_id, parent): modeman.destroyed.connect( functools.partial(objreg.delete, 'keyparsers', scope='window', window=win_id)) - modeman.register(KM.normal, keyparsers[KM.normal].handle) - modeman.register(KM.hint, keyparsers[KM.hint].handle) - modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True) - modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle, - passthrough=True) - modeman.register(KM.command, keyparsers[KM.command].handle, - passthrough=True) - modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True) - modeman.register(KM.yesno, keyparsers[KM.yesno].handle) - modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True) + for mode, parser in keyparsers.items(): + modeman.register(mode, parser) return modeman -def _get_modeman(win_id): +def instance(win_id): """Get a modemanager object.""" return objreg.get('mode-manager', scope='window', window=win_id) def enter(win_id, mode, reason=None, only_if_normal=False): """Enter the mode 'mode'.""" - _get_modeman(win_id).enter(mode, reason, only_if_normal) + instance(win_id).enter(mode, reason, only_if_normal) def leave(win_id, mode, reason=None): """Leave the mode 'mode'.""" - _get_modeman(win_id).leave(mode, reason) + instance(win_id).leave(mode, reason) def maybe_leave(win_id, mode, reason=None): """Convenience method to leave 'mode' without exceptions.""" try: - _get_modeman(win_id).leave(mode, reason) + instance(win_id).leave(mode, reason) except NotInModeError as e: # This is rather likely to happen, so we only log to debug log. log.modes.debug("{} (leave reason: {})".format(e, reason)) @@ -126,10 +118,9 @@ class ModeManager(QObject): """Manager for keyboard modes. Attributes: - passthrough: A list of modes in which to pass through events. mode: The mode we're currently in. _win_id: The window ID of this ModeManager - _handlers: A dictionary of modes and their handlers. + _parsers: A dictionary of modes and their keyparsers. _forward_unbound_keys: If we should forward unbound keys. _releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was passed through, so the release event should as @@ -151,8 +142,7 @@ class ModeManager(QObject): def __init__(self, win_id, parent=None): super().__init__(parent) self._win_id = win_id - self._handlers = {} - self.passthrough = [] + self._parsers = {} self.mode = usertypes.KeyMode.normal self._releaseevents_to_pass = set() self._forward_unbound_keys = config.get( @@ -160,8 +150,7 @@ class ModeManager(QObject): objreg.get('config').changed.connect(self.set_forward_unbound_keys) def __repr__(self): - return utils.get_repr(self, mode=self.mode, - passthrough=self.passthrough) + return utils.get_repr(self, mode=self.mode) def _eventFilter_keypress(self, event): """Handle filtering of KeyPress events. @@ -173,11 +162,11 @@ class ModeManager(QObject): True if event should be filtered, False otherwise. """ curmode = self.mode - handler = self._handlers[curmode] + parser = self._parsers[curmode] if curmode != usertypes.KeyMode.insert: - log.modes.debug("got keypress in mode {} - calling handler " - "{}".format(curmode, utils.qualname(handler))) - handled = handler(event) if handler is not None else False + log.modes.debug("got keypress in mode {} - delegating to " + "{}".format(curmode, utils.qualname(parser))) + handled = parser.handle(event) is_non_alnum = bool(event.modifiers()) or not event.text().strip() focus_widget = QApplication.instance().focusWidget() @@ -187,7 +176,7 @@ class ModeManager(QObject): filter_this = True elif is_tab and not isinstance(focus_widget, QWebView): filter_this = True - elif (curmode in self.passthrough or + elif (parser.passthrough or self._forward_unbound_keys == 'all' or (self._forward_unbound_keys == 'auto' and is_non_alnum)): filter_this = False @@ -202,8 +191,8 @@ class ModeManager(QObject): "passthrough: {}, is_non_alnum: {}, is_tab {} --> " "filter: {} (focused: {!r})".format( handled, self._forward_unbound_keys, - curmode in self.passthrough, is_non_alnum, - is_tab, filter_this, focus_widget)) + parser.passthrough, is_non_alnum, is_tab, + filter_this, focus_widget)) return filter_this def _eventFilter_keyrelease(self, event): @@ -226,20 +215,16 @@ class ModeManager(QObject): log.modes.debug("filter: {}".format(filter_this)) return filter_this - def register(self, mode, handler, passthrough=False): + def register(self, mode, parser): """Register a new mode. Args: mode: The name of the mode. - handler: Handler for keyPressEvents. - passthrough: Whether to pass key bindings in this mode through to - the widgets. + parser: The KeyParser which should be used. """ - if not isinstance(mode, usertypes.KeyMode): - raise TypeError("Mode {} is no KeyMode member!".format(mode)) - self._handlers[mode] = handler - if passthrough: - self.passthrough.append(mode) + assert isinstance(mode, usertypes.KeyMode) + assert parser is not None + self._parsers[mode] = parser def enter(self, mode, reason=None, only_if_normal=False): """Enter a new mode. @@ -253,8 +238,8 @@ class ModeManager(QObject): raise TypeError("Mode {} is no KeyMode member!".format(mode)) log.modes.debug("Entering mode {}{}".format( mode, '' if reason is None else ' (reason: {})'.format(reason))) - if mode not in self._handlers: - raise ValueError("No handler for mode {}".format(mode)) + if mode not in self._parsers: + raise ValueError("No keyparser for mode {}".format(mode)) prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno) if self.mode == mode or (self.mode in prompt_modes and mode in prompt_modes): diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 8d47de0c1..d16734ed0 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -224,6 +224,8 @@ class CaretKeyParser(keyparser.CommandKeyParser): """KeyParser for caret mode.""" + passthrough = True + def __init__(self, win_id, parent=None): super().__init__(win_id, parent, supports_count=True, supports_chains=True) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 5f633eaaa..bc828a261 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -469,9 +469,9 @@ class StatusBar(QWidget): @pyqtSlot(usertypes.KeyMode) def on_mode_entered(self, mode): """Mark certain modes in the commandline.""" - mode_manager = objreg.get('mode-manager', scope='window', - window=self._win_id) - if mode in mode_manager.passthrough: + keyparsers = objreg.get('keyparsers', scope='window', + window=self._win_id) + if keyparsers[mode].passthrough: self._set_mode_text(mode.name) if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): self.set_mode_active(mode, True) @@ -479,10 +479,10 @@ class StatusBar(QWidget): @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) def on_mode_left(self, old_mode, new_mode): """Clear marked mode.""" - mode_manager = objreg.get('mode-manager', scope='window', - window=self._win_id) - if old_mode in mode_manager.passthrough: - if new_mode in mode_manager.passthrough: + keyparsers = objreg.get('keyparsers', scope='window', + window=self._win_id) + if keyparsers[old_mode].passthrough: + if keyparsers[new_mode].passthrough: self._set_mode_text(new_mode.name) else: self.txt.set_text(self.txt.Text.normal, '') diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index ba5a5c725..d54b22d0c 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -296,7 +296,7 @@ class TabbedBrowser(tabwidget.TabWidget): newtab: True to open URL in a new tab, False otherwise. """ qtutils.ensure_valid(url) - if newtab: + if newtab or self.currentWidget() is None: self.tabopen(url, background=False) else: self.currentWidget().openurl(url) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 79a425fa9..dbb473b4e 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -183,7 +183,7 @@ class _CrashDialog(QDialog): def _init_text(self): """Initialize the main text to be displayed on an exception. - Should be extended by superclass to set the actual text.""" + Should be extended by subclasses to set the actual text.""" self._lbl = QLabel(wordWrap=True, openExternalLinks=True, textInteractionFlags=Qt.LinksAccessibleByMouse) self._vbox.addWidget(self._lbl) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 7598f3312..2e336bc1e 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -190,7 +190,7 @@ class CrashHandler(QObject): objects = "" return ExceptionInfo(pages, cmd_history, objects) - def exception_hook(self, exctype, excvalue, tb): # noqa + def exception_hook(self, exctype, excvalue, tb): """Handle uncaught python exceptions. It'll try very hard to write all open tabs to a file, and then exit diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 74dd8f92e..04ced01c2 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -77,7 +77,7 @@ class CommandLineEdit(QLineEdit): def __on_cursor_position_changed(self, _old, new): """Prevent the cursor moving to the prompt. - We use __ here to avoid accidentally overriding it in superclasses. + We use __ here to avoid accidentally overriding it in subclasses. """ if new < self._promptlen: self.setCursorPosition(self._promptlen) diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index b763d8246..a7bbeea6e 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -55,7 +55,7 @@ class ShellLexer: self.token = '' self.state = ' ' - def __iter__(self): # noqa + def __iter__(self): # pragma: no mccabe """Read a raw token from the input stream.""" # pylint: disable=too-many-branches,too-many-statements self.reset() diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 5d19ad515..c82a54596 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -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): diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 827762af4..19dae311a 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -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__']), diff --git a/scripts/pylint_checkers/crlf.py b/scripts/pylint_checkers/crlf.py deleted file mode 100644 index a77f8b9e0..000000000 --- a/scripts/pylint_checkers/crlf.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2014-2015 Florian Bruhin (The Compiler) -# 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 . - -"""Checker for CRLF in files.""" - -from pylint import interfaces, checkers - - -class CrlfChecker(checkers.BaseChecker): - - """Check for CRLF in files.""" - - __implements__ = interfaces.IRawChecker - - name = 'crlf' - msgs = {'W9001': ('Uses CRLFs', 'crlf', None)} - options = () - priority = -1 - - def process_module(self, node): - """Process the module.""" - for (lineno, line) in enumerate(node.file_stream): - if b'\r\n' in line: - self.add_message('crlf', line=lineno) - return - - -def register(linter): - """Register the checker.""" - linter.register_checker(CrlfChecker(linter)) diff --git a/tests/mainwindow/statusbar/test_progress.py b/tests/mainwindow/statusbar/test_progress.py index 07e93e0e5..a0d066808 100644 --- a/tests/mainwindow/statusbar/test_progress.py +++ b/tests/mainwindow/statusbar/test_progress.py @@ -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. diff --git a/tests/misc/test_readline.py b/tests/misc/test_readline.py index 523c6f579..da2d05821 100644 --- a/tests/misc/test_readline.py +++ b/tests/misc/test_readline.py @@ -21,144 +21,256 @@ # pylint: disable=protected-access +import re import inspect -from unittest import mock -from PyQt5.QtWidgets import QLineEdit +from PyQt5.QtWidgets import QLineEdit, QApplication import pytest from qutebrowser.misc import readline +# Some functions aren't 100% readline compatible: +# https://github.com/The-Compiler/qutebrowser/issues/678 +# Those are marked with fixme and have another value marked with '# wrong' +# which marks the current behavior. + +fixme = pytest.mark.xfail(reason='readline compatibility - see #678') + + +class LineEdit(QLineEdit): + + """QLineEdit with some methods to make testing easier.""" + + def _get_index(self, haystack, needle): + """Get the index of a char (needle) in a string (haystack). + + Return: + The position where needle was found, or None if it wasn't found. + """ + try: + return haystack.index(needle) + except ValueError: + return None + + def set_aug_text(self, text): + """Set a text with markers for selected text and | as cursor.""" + real_text = re.sub('[<>|]', '', text) + self.setText(real_text) + + cursor_pos = self._get_index(text, '|') + sel_start_pos = self._get_index(text, '<') + sel_end_pos = self._get_index(text, '>') + + if sel_start_pos is not None and sel_end_pos is None: + raise ValueError("< given without >!") + if sel_start_pos is None and sel_end_pos is not None: + raise ValueError("> given without !") + 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', [('fbar', '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', [('fbar', '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 o', 'one |two'), + ('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(('ne two', 'one| two')), + ('ne two', 'one |two'), # wrong + fixme((' two', 'one two|')), + (' two', 'one |two'), # wrong + ('one t', '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('fbar') + 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('fbar') + 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'), + ('fbar', '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'), + ('fbar', '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 test', 'delete this', '| test')), + ('delete test', 'delete ', '|this test'), # wrong + fixme(('fbar', 'foo', '|bar')), + ('fbar', '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(('delete this', 'test delete this', '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 delfoobar', 'delete', 'test |foobar')), + ('test delfoobar', '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 delete', ' delete', 'test foobar|')), + ('test foodelete', 'bardelete', 'test foo|'), # wrong +]) +def test_rl_kill_word(lineedit, bridge, text, deleted, rest): + """Delete to word end and see if it comes back with yank.""" + lineedit.set_aug_text(text) + bridge.rl_kill_word() + assert bridge._deleted[lineedit] == deleted + assert lineedit.aug_text() == rest + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == deleted + '|' + + +def test_rl_yank_no_text(lineedit, bridge): + """Test yank without having deleted anything.""" + lineedit.clear() + bridge.rl_yank() + assert lineedit.aug_text() == '|' diff --git a/tests/utils/usertypes/test_enum.py b/tests/utils/usertypes/test_enum.py index e1443b7be..7298b2861 100644 --- a/tests/utils/usertypes/test_enum.py +++ b/tests/utils/usertypes/test_enum.py @@ -54,3 +54,9 @@ def test_start(): e = usertypes.enum('Enum', ['three', 'four'], start=3) assert e.three.value == 3 assert e.four.value == 4 + + +def test_exit(): + """Make sure the exit status enum is correct.""" + assert usertypes.Exit.ok == 0 + assert usertypes.Exit.reserved == 1 diff --git a/tox.ini b/tox.ini index f39af4ee3..cff331d21 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = unittests,misc,pep257,flake8,pylint,pyroma,check-manifest +envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest [testenv] basepython = python3 @@ -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