Merge branch 'master' into stylesheet

This commit is contained in:
Ulrik de Muelenaere 2017-11-03 12:13:52 +02:00
commit 72c57d16f4
73 changed files with 741 additions and 437 deletions

View File

@ -40,7 +40,11 @@ disable=no-self-use,
# https://github.com/PyCQA/pylint/issues/1698 # https://github.com/PyCQA/pylint/issues/1698
unsupported-membership-test, unsupported-membership-test,
unsupported-assignment-operation, unsupported-assignment-operation,
unsubscriptable-object unsubscriptable-object,
too-many-boolean-expressions,
too-many-locals,
too-many-branches,
too-many-statements
[BASIC] [BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$ function-rgx=[a-z_][a-z0-9_]{2,50}$

View File

@ -51,7 +51,7 @@ matrix:
env: TESTENV=eslint env: TESTENV=eslint
language: node_js language: node_js
python: null python: null
node_js: node node_js: "lts/*"
fast_finish: true fast_finish: true
cache: cache:

View File

@ -32,6 +32,10 @@ Added
- New `config.source(...)` method for `config.py` to source another file. - New `config.source(...)` method for `config.py` to source another file.
- New `keyhint.radius` option to configure the edge rounding for the key hint - New `keyhint.radius` option to configure the edge rounding for the key hint
widget. widget.
- `:edit-url` now handles the `--private` and `--related` flags, which have the
same effect they have with `:open`.
- New `{line}` and `{column}` replacements for `editor.command` to position the
cursor correctly.
Changed Changed
~~~~~~~ ~~~~~~~
@ -65,10 +69,17 @@ Removed
v1.0.3 (unreleased) v1.0.3 (unreleased)
------------------- -------------------
Changed
~~~~~~~
- Performance improvements for tab rendering
Fixed Fixed
~~~~~ ~~~~~
- Handle accessing a locked sqlite database gracefully - Handle accessing a locked sqlite database gracefully
- Abort pinned tab dialogs properly when a tab is closed e.g. by closing a
window.
v1.0.2 v1.0.2
------ ------

View File

@ -152,7 +152,7 @@ For QtWebEngine:
`:set spellcheck.languages "['en-US', 'pl-PL']"` `:set spellcheck.languages "['en-US', 'pl-PL']"`
How do I use Tor with qutebrowser?:: How do I use Tor with qutebrowser?::
Start tor on your machine, and do `:set network proxy socks://localhost:9050/` Start tor on your machine, and do `:set content.proxy socks://localhost:9050/`
in qutebrowser. Note this won't give you the same amount of fingerprinting in qutebrowser. Note this won't give you the same amount of fingerprinting
protection that the Tor Browser does, but it's useful to be able to access protection that the Tor Browser does, but it's useful to be able to access
`.onion` sites. `.onion` sites.
@ -162,7 +162,7 @@ Why does J move to the next (right) tab, and K to the previous (left) one?::
and qutebrowser's keybindings are designed to be compatible with dwb's. and qutebrowser's keybindings are designed to be compatible with dwb's.
The rationale behind it is that J is "down" in vim, and K is "up", which The rationale behind it is that J is "down" in vim, and K is "up", which
corresponds nicely to "next"/"previous". It also makes much more sense with corresponds nicely to "next"/"previous". It also makes much more sense with
vertical tabs (e.g. `:set tabs position left`). vertical tabs (e.g. `:set tabs.position left`).
What's the difference between insert and passthrough mode?:: What's the difference between insert and passthrough mode?::
They are quite similar, but insert mode has some bindings (like `Ctrl-e` to They are quite similar, but insert mode has some bindings (like `Ctrl-e` to

View File

@ -60,6 +60,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<messages,messages>>|Show a log of past messages. |<<messages,messages>>|Show a log of past messages.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path. |<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab. |<<open,open>>|Open a URL in the current/[count]th tab.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<print,print>>|Print the current/[count]th tab. |<<print,print>>|Print the current/[count]th tab.
|<<quickmark-add,quickmark-add>>|Add a new quickmark. |<<quickmark-add,quickmark-add>>|Add a new quickmark.
|<<quickmark-del,quickmark-del>>|Delete a quickmark. |<<quickmark-del,quickmark-del>>|Delete a quickmark.
@ -361,7 +362,7 @@ The index of the download to retry.
[[edit-url]] [[edit-url]]
=== edit-url === edit-url
Syntax: +:edit-url [*--bg*] [*--tab*] [*--window*] ['url']+ Syntax: +:edit-url [*--bg*] [*--tab*] [*--window*] [*--private*] [*--related*] ['url']+
Navigate to a url formed in an external editor. Navigate to a url formed in an external editor.
@ -374,6 +375,9 @@ The editor which should be launched can be configured via the `editor.command` c
* +*-b*+, +*--bg*+: Open in a new background tab. * +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab. * +*-t*+, +*--tab*+: Open in a new tab.
* +*-w*+, +*--window*+: Open in a new window. * +*-w*+, +*--window*+: Open in a new window.
* +*-p*+, +*--private*+: Open a new window in private browsing mode.
* +*-r*+, +*--related*+: If opening a new tab, position the tab as related to the current one (like clicking on a link).
[[fake-key]] [[fake-key]]
=== fake-key === fake-key
@ -654,6 +658,12 @@ The tab index to open the URL in.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * This command does not split arguments after the last argument and handles quotes literally.
[[open-editor]]
=== open-editor
Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the `editor.command` config option.
[[print]] [[print]]
=== print === print
Syntax: +:print [*--preview*] [*--pdf* 'file']+ Syntax: +:print [*--preview*] [*--pdf* 'file']+
@ -1155,7 +1165,6 @@ How many steps to zoom out.
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block. |<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block. |<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|<<nop,nop>>|Do nothing. |<<nop,nop>>|Do nothing.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt. |<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item. |<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item.
|<<prompt-open-download,prompt-open-download>>|Immediately open a download. |<<prompt-open-download,prompt-open-download>>|Immediately open a download.
@ -1406,12 +1415,6 @@ How many blocks to move.
=== nop === nop
Do nothing. Do nothing.
[[open-editor]]
=== open-editor
Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the `editor.command` config option.
[[prompt-accept]] [[prompt-accept]]
=== prompt-accept === prompt-accept
Syntax: +:prompt-accept ['value']+ Syntax: +:prompt-accept ['value']+

View File

@ -1961,7 +1961,12 @@ Default: +pass:[-1]+
[[editor.command]] [[editor.command]]
=== editor.command === editor.command
The editor (and arguments) to use for the `open-editor` command. The editor (and arguments) to use for the `open-editor` command.
`{}` gets replaced by the filename of the file to be edited. Several placeholders are supported. Placeholders are substituted by the respective value when executing the command.
`{file}` gets replaced by the filename of the file to be edited.
`{line}` gets replaced by the line in which the caret is found in the text.
`{column}` gets replaced by the column in which the caret is found in the text.
`{line0}` same as `{line}`, but starting from index 0.
`{column0}` same as `{column}`, but starting from index 0.
Type: <<types,ShellCommand>> Type: <<types,ShellCommand>>
@ -1969,7 +1974,9 @@ Default:
- +pass:[gvim]+ - +pass:[gvim]+
- +pass:[-f]+ - +pass:[-f]+
- +pass:[{}]+ - +pass:[{file}]+
- +pass:[-c]+
- +pass:[normal {line}G{column0}l]+
[[editor.encoding]] [[editor.encoding]]
=== editor.encoding === editor.encoding

View File

@ -417,7 +417,6 @@ def _init_modules(args, crash_handler):
args: The argparse namespace. args: The argparse namespace.
crash_handler: The CrashHandler instance. crash_handler: The CrashHandler instance.
""" """
# pylint: disable=too-many-statements
log.init.debug("Initializing save manager...") log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp) save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager) objreg.register('save-manager', save_manager)
@ -661,9 +660,9 @@ class Quitter:
try: try:
args, cwd = self._get_restart_args(pages, session, override_args) args, cwd = self._get_restart_args(pages, session, override_args)
if cwd is None: if cwd is None:
subprocess.Popen(args) subprocess.run(args)
else: else:
subprocess.Popen(args, cwd=cwd) subprocess.run(args, cwd=cwd)
except OSError: except OSError:
log.destroy.exception("Failed to restart") log.destroy.exception("Failed to restart")
return False return False

View File

@ -19,6 +19,7 @@
"""Base class for a wrapper over QWebView/QWebEngineView.""" """Base class for a wrapper over QWebView/QWebEngineView."""
import enum
import itertools import itertools
import attr import attr
@ -74,7 +75,7 @@ class UnsupportedOperationError(WebTabError):
"""Raised when an operation is not supported with the given backend.""" """Raised when an operation is not supported with the given backend."""
TerminationStatus = usertypes.enum('TerminationStatus', [ TerminationStatus = enum.Enum('TerminationStatus', [
'normal', 'normal',
'abnormal', # non-zero exit status 'abnormal', # non-zero exit status
'crashed', # e.g. segfault 'crashed', # e.g. segfault

View File

@ -1618,13 +1618,14 @@ class CommandDispatcher:
return return
assert isinstance(text, str), text assert isinstance(text, str), text
caret_position = elem.caret_position()
ed = editor.ExternalEditor(self._tabbed_browser) ed = editor.ExternalEditor(self._tabbed_browser)
ed.editing_finished.connect(functools.partial( ed.editing_finished.connect(functools.partial(
self.on_editing_finished, elem)) self.on_editing_finished, elem))
ed.edit(text) ed.edit(text, caret_position)
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', scope='window')
scope='window')
def open_editor(self): def open_editor(self):
"""Open an external editor with the currently selected form field. """Open an external editor with the currently selected form field.
@ -2112,7 +2113,8 @@ class CommandDispatcher:
self._current_widget().clear_ssl_errors() self._current_widget().clear_ssl_errors()
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def edit_url(self, url=None, bg=False, tab=False, window=False): def edit_url(self, url=None, bg=False, tab=False, window=False,
private=False, related=False):
"""Navigate to a url formed in an external editor. """Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the The editor which should be launched can be configured via the
@ -2123,6 +2125,9 @@ class CommandDispatcher:
bg: Open in a new background tab. bg: Open in a new background tab.
tab: Open in a new tab. tab: Open in a new tab.
window: Open in a new window. window: Open in a new window.
private: Open a new window in private browsing mode.
related: If opening a new tab, position the tab as related to the
current one (like clicking on a link).
""" """
cmdutils.check_exclusive((tab, bg, window), 'tbw') cmdutils.check_exclusive((tab, bg, window), 'tbw')
@ -2133,7 +2138,7 @@ class CommandDispatcher:
# Passthrough for openurl args (e.g. -t, -b, -w) # Passthrough for openurl args (e.g. -t, -b, -w)
ed.editing_finished.connect(functools.partial( ed.editing_finished.connect(functools.partial(
self._open_if_changed, old_url=old_url, bg=bg, tab=tab, self._open_if_changed, old_url=old_url, bg=bg, tab=tab,
window=window)) window=window, private=private, related=related))
ed.edit(url or old_url) ed.edit(url or old_url)
@ -2158,7 +2163,7 @@ class CommandDispatcher:
self._tabbed_browser.jump_mark(key) self._tabbed_browser.jump_mark(key)
def _open_if_changed(self, url=None, old_url=None, bg=False, tab=False, def _open_if_changed(self, url=None, old_url=None, bg=False, tab=False,
window=False): window=False, private=False, related=False):
"""Open a URL unless it's already open in the tab. """Open a URL unless it's already open in the tab.
Args: Args:
@ -2167,9 +2172,13 @@ class CommandDispatcher:
bg: Open in a new background tab. bg: Open in a new background tab.
tab: Open in a new tab. tab: Open in a new tab.
window: Open in a new window. window: Open in a new window.
private: Open a new window in private browsing mode.
related: If opening a new tab, position the tab as related to the
current one (like clicking on a link).
""" """
if bg or tab or window or url != old_url: if bg or tab or window or private or related or url != old_url:
self.openurl(url=url, bg=bg, tab=tab, window=window) self.openurl(url=url, bg=bg, tab=tab, window=window,
private=private, related=related)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def fullscreen(self, leave=False): def fullscreen(self, leave=False):

View File

@ -27,6 +27,7 @@ import collections
import functools import functools
import pathlib import pathlib
import tempfile import tempfile
import enum
import sip import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
@ -38,8 +39,7 @@ from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
qtutils) qtutils)
ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole, ModelRole = enum.IntEnum('ModelRole', ['item'], start=Qt.UserRole)
is_int=True)
# Remember the last used directory # Remember the last used directory

View File

@ -24,6 +24,7 @@ import functools
import math import math
import re import re
import html import html
import enum
from string import ascii_lowercase from string import ascii_lowercase
import attr import attr
@ -37,10 +38,9 @@ from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
Target = usertypes.enum('Target', ['normal', 'current', 'tab', 'tab_fg', Target = enum.Enum('Target', ['normal', 'current', 'tab', 'tab_fg', 'tab_bg',
'tab_bg', 'window', 'yank', 'yank_primary', 'window', 'yank', 'yank_primary', 'run', 'fill',
'run', 'fill', 'hover', 'download', 'hover', 'download', 'userscript', 'spawn'])
'userscript', 'spawn'])
class HintingError(Exception): class HintingError(Exception):

View File

@ -24,6 +24,7 @@ Module attributes:
SELECTORS: CSS selectors for different groups of elements. SELECTORS: CSS selectors for different groups of elements.
""" """
import enum
import collections.abc import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
@ -35,7 +36,7 @@ from qutebrowser.mainwindow import mainwindow
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'inputs']) Group = enum.Enum('Group', ['all', 'links', 'images', 'url', 'inputs'])
SELECTORS = { SELECTORS = {

View File

@ -47,6 +47,7 @@ class WebEngineElement(webelem.AbstractWebElement):
'class_name': str, 'class_name': str,
'rects': list, 'rects': list,
'attributes': dict, 'attributes': dict,
'caret_position': int,
} }
assert set(js_dict.keys()).issubset(js_dict_types.keys()) assert set(js_dict.keys()).issubset(js_dict_types.keys())
for name, typ in js_dict_types.items(): for name, typ in js_dict_types.items():
@ -132,6 +133,10 @@ class WebEngineElement(webelem.AbstractWebElement):
def set_value(self, value): def set_value(self, value):
self._js_call('set_value', value) self._js_call('set_value', value)
def caret_position(self):
"""Get the text caret position for the current element."""
return self._js_dict.get('caret_position', 0)
def insert_text(self, text): def insert_text(self, text):
if not self.is_editable(strict=True): if not self.is_editable(strict=True):
raise webelem.Error("Element is not editable!") raise webelem.Error("Element is not editable!")

View File

@ -126,6 +126,14 @@ class WebKitElement(webelem.AbstractWebElement):
value = javascript.string_escape(value) value = javascript.string_escape(value)
self._elem.evaluateJavaScript("this.value='{}'".format(value)) self._elem.evaluateJavaScript("this.value='{}'".format(value))
def caret_position(self):
"""Get the text caret position for the current element."""
self._check_vanished()
pos = self._elem.evaluateJavaScript('this.selectionStart')
if pos is None:
return 0
return int(pos)
def insert_text(self, text): def insert_text(self, text):
self._check_vanished() self._check_vanished()
if not self.is_editable(strict=True): if not self.is_editable(strict=True):

View File

@ -81,8 +81,6 @@ class Command:
deprecated=False, no_cmd_split=False, deprecated=False, no_cmd_split=False,
star_args_optional=False, scope='global', backend=None, star_args_optional=False, scope='global', backend=None,
no_replace_variables=False): no_replace_variables=False):
# I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-locals
if modes is not None and not_modes is not None: if modes is not None and not_modes is not None:
raise ValueError("Only modes or not_modes can be given!") raise ValueError("Only modes or not_modes can be given!")
if modes is not None: if modes is not None:

View File

@ -43,7 +43,6 @@ class Completer(QObject):
Attributes: Attributes:
_cmd: The statusbar Command object this completer belongs to. _cmd: The statusbar Command object this completer belongs to.
_ignore_change: Whether to ignore the next completion update.
_timer: The timer used to trigger the completion update. _timer: The timer used to trigger the completion update.
_last_cursor_pos: The old cursor position so we avoid double completion _last_cursor_pos: The old cursor position so we avoid double completion
updates. updates.
@ -54,7 +53,6 @@ class Completer(QObject):
def __init__(self, cmd, parent=None): def __init__(self, cmd, parent=None):
super().__init__(parent) super().__init__(parent)
self._cmd = cmd self._cmd = cmd
self._ignore_change = False
self._timer = QTimer() self._timer = QTimer()
self._timer.setSingleShot(True) self._timer.setSingleShot(True)
self._timer.setInterval(0) self._timer.setInterval(0)
@ -178,13 +176,15 @@ class Completer(QObject):
text = self._quote(text) text = self._quote(text)
model = self._model() model = self._model()
if model.count() == 1 and config.val.completion.quick: if model.count() == 1 and config.val.completion.quick:
# If we only have one item, we want to apply it immediately # If we only have one item, we want to apply it immediately and go
# and go on to the next part. # on to the next part, unless we are quick-completing the part
self._change_completed_part(text, before, after, immediate=True) # after maxsplit, so that we don't keep offering completions
# (see issue #1519)
if maxsplit is not None and maxsplit < len(before): if maxsplit is not None and maxsplit < len(before):
# If we are quick-completing the part after maxsplit, don't self._change_completed_part(text, before, after)
# keep offering completions (see issue #1519) else:
self._ignore_change = True self._change_completed_part(text, before, after,
immediate=True)
else: else:
self._change_completed_part(text, before, after) self._change_completed_part(text, before, after)
@ -219,12 +219,6 @@ class Completer(QObject):
@pyqtSlot() @pyqtSlot()
def _update_completion(self): def _update_completion(self):
"""Check if completions are available and activate them.""" """Check if completions are available and activate them."""
if self._ignore_change:
log.completion.debug("Ignoring completion update because "
"ignore_change is True.")
self._ignore_change = False
return
completion = self.parent() completion = self.parent()
if self._cmd.prefix() != ':': if self._cmd.prefix() != ':':

View File

@ -766,11 +766,23 @@ editor.command:
type: type:
name: ShellCommand name: ShellCommand
placeholder: true placeholder: true
default: ["gvim", "-f", "{}"] default: ["gvim", "-f", "{file}", "-c", "normal {line}G{column0}l"]
desc: >- desc: >-
The editor (and arguments) to use for the `open-editor` command. The editor (and arguments) to use for the `open-editor` command.
`{}` gets replaced by the filename of the file to be edited. Several placeholders are supported. Placeholders are substituted by the
respective value when executing the command.
`{file}` gets replaced by the filename of the file to be edited.
`{line}` gets replaced by the line in which the caret is found in the text.
`{column}` gets replaced by the column in which the caret is found in the text.
`{line0}` same as `{line}`, but starting from index 0.
`{column0}` same as `{column}`, but starting from index 0.
editor.encoding: editor.encoding:
type: Encoding type: Encoding

View File

@ -755,6 +755,6 @@ def get_diff():
lexer = pygments.lexers.DiffLexer() lexer = pygments.lexers.DiffLexer()
formatter = pygments.formatters.HtmlFormatter( formatter = pygments.formatters.HtmlFormatter(
full=True, linenos='table', full=True, linenos='table',
title='Config diff') title='Diffing pre-1.0 default config with pre-1.0 modified config')
# pylint: enable=no-member # pylint: enable=no-member
return pygments.highlight(conf_diff + key_diff, lexer, formatter) return pygments.highlight(conf_diff + key_diff, lexer, formatter)

View File

@ -1341,9 +1341,12 @@ class ShellCommand(List):
if not value: if not value:
return value return value
if self.placeholder and '{}' not in ' '.join(value): if (self.placeholder and
'{}' not in ' '.join(value) and
'{file}' not in ' '.join(value)):
raise configexc.ValidationError(value, "needs to contain a " raise configexc.ValidationError(value, "needs to contain a "
"{}-placeholder.") "{}-placeholder or a "
"{file}-placeholder.")
return value return value

View File

@ -2,7 +2,7 @@ env:
browser: true browser: true
parserOptions: parserOptions:
ecmaVersion: 3 ecmaVersion: 6
extends: extends:
"eslint:all" "eslint:all"
@ -13,13 +13,9 @@ rules:
padded-blocks: ["error", "never"] padded-blocks: ["error", "never"]
space-before-function-paren: ["error", "never"] space-before-function-paren: ["error", "never"]
no-underscore-dangle: "off" no-underscore-dangle: "off"
no-var: "off"
vars-on-top: "off"
newline-after-var: "off"
camelcase: "off" camelcase: "off"
require-jsdoc: "off" require-jsdoc: "off"
func-style: ["error", "declaration"] func-style: ["error", "declaration"]
newline-before-return: "off"
init-declarations: "off" init-declarations: "off"
no-plusplus: "off" no-plusplus: "off"
no-extra-parens: off no-extra-parens: off

View File

@ -21,22 +21,22 @@
window.loadHistory = (function() { window.loadHistory = (function() {
// Date of last seen item. // Date of last seen item.
var lastItemDate = null; let lastItemDate = null;
// Each request for new items includes the time of the last item and an // Each request for new items includes the time of the last item and an
// offset. The offset is equal to the number of items from the previous // offset. The offset is equal to the number of items from the previous
// request that had time=nextTime, and causes the next request to skip // request that had time=nextTime, and causes the next request to skip
// those items to avoid duplicates. // those items to avoid duplicates.
var nextTime = null; let nextTime = null;
var nextOffset = 0; let nextOffset = 0;
// The URL to fetch data from. // The URL to fetch data from.
var DATA_URL = "qute://history/data"; const DATA_URL = "qute://history/data";
// Various fixed elements // Various fixed elements
var EOF_MESSAGE = document.getElementById("eof"); const EOF_MESSAGE = document.getElementById("eof");
var LOAD_LINK = document.getElementById("load"); const LOAD_LINK = document.getElementById("load");
var HIST_CONTAINER = document.getElementById("hist-container"); const HIST_CONTAINER = document.getElementById("hist-container");
/** /**
* Finds or creates the session table>tbody to which item with given date * Finds or creates the session table>tbody to which item with given date
@ -47,17 +47,17 @@ window.loadHistory = (function() {
*/ */
function getSessionNode(date) { function getSessionNode(date) {
// Find/create table // Find/create table
var tableId = ["hist", date.getDate(), date.getMonth(), const tableId = ["hist", date.getDate(), date.getMonth(),
date.getYear()].join("-"); date.getYear()].join("-");
var table = document.getElementById(tableId); let table = document.getElementById(tableId);
if (table === null) { if (table === null) {
table = document.createElement("table"); table = document.createElement("table");
table.id = tableId; table.id = tableId;
// Caption contains human-readable date // Caption contains human-readable date
var caption = document.createElement("caption"); const caption = document.createElement("caption");
caption.className = "date"; caption.className = "date";
var options = { const options = {
"weekday": "long", "weekday": "long",
"year": "numeric", "year": "numeric",
"month": "long", "month": "long",
@ -71,7 +71,7 @@ window.loadHistory = (function() {
} }
// Find/create tbody // Find/create tbody
var tbody = table.lastChild; let tbody = table.lastChild;
if (tbody.tagName !== "TBODY") { if (tbody.tagName !== "TBODY") {
tbody = document.createElement("tbody"); tbody = document.createElement("tbody");
table.appendChild(tbody); table.appendChild(tbody);
@ -80,10 +80,10 @@ window.loadHistory = (function() {
// Create session-separator and new tbody if necessary // Create session-separator and new tbody if necessary
if (tbody.lastChild !== null && lastItemDate !== null && if (tbody.lastChild !== null && lastItemDate !== null &&
window.GAP_INTERVAL > 0) { window.GAP_INTERVAL > 0) {
var interval = lastItemDate.getTime() - date.getTime(); const interval = lastItemDate.getTime() - date.getTime();
if (interval > window.GAP_INTERVAL) { if (interval > window.GAP_INTERVAL) {
// Add session-separator // Add session-separator
var sessionSeparator = document.createElement("td"); const sessionSeparator = document.createElement("td");
sessionSeparator.className = "session-separator"; sessionSeparator.className = "session-separator";
sessionSeparator.colSpan = 2; sessionSeparator.colSpan = 2;
sessionSeparator.innerHTML = "&#167;"; sessionSeparator.innerHTML = "&#167;";
@ -108,20 +108,20 @@ window.loadHistory = (function() {
* @returns {Element} the completed tr. * @returns {Element} the completed tr.
*/ */
function makeHistoryRow(itemUrl, itemTitle, itemTime) { function makeHistoryRow(itemUrl, itemTitle, itemTime) {
var row = document.createElement("tr"); const row = document.createElement("tr");
var title = document.createElement("td"); const title = document.createElement("td");
title.className = "title"; title.className = "title";
var link = document.createElement("a"); const link = document.createElement("a");
link.href = itemUrl; link.href = itemUrl;
link.innerHTML = itemTitle; link.innerHTML = itemTitle;
var host = document.createElement("span"); const host = document.createElement("span");
host.className = "hostname"; host.className = "hostname";
host.innerHTML = link.hostname; host.innerHTML = link.hostname;
title.appendChild(link); title.appendChild(link);
title.appendChild(host); title.appendChild(host);
var time = document.createElement("td"); const time = document.createElement("td");
time.className = "time"; time.className = "time";
time.innerHTML = itemTime; time.innerHTML = itemTime;
@ -139,11 +139,11 @@ window.loadHistory = (function() {
* @returns {void} * @returns {void}
*/ */
function getJSON(url, callback) { function getJSON(url, callback) {
var xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("GET", url, true); xhr.open("GET", url, true);
xhr.responseType = "json"; xhr.responseType = "json";
xhr.onload = function() { xhr.onload = () => {
var status = xhr.status; const status = xhr.status;
callback(status, xhr.response); callback(status, xhr.response);
}; };
xhr.send(); xhr.send();
@ -172,10 +172,10 @@ window.loadHistory = (function() {
nextTime = history[history.length - 1].time; nextTime = history[history.length - 1].time;
nextOffset = 0; nextOffset = 0;
for (var i = 0, len = history.length; i < len; i++) { for (let i = 0, len = history.length; i < len; i++) {
var item = history[i]; const item = history[i];
// python's time.time returns seconds, but js Date expects ms // python's time.time returns seconds, but js Date expects ms
var currentItemDate = new Date(item.time * 1000); const currentItemDate = new Date(item.time * 1000);
getSessionNode(currentItemDate).appendChild(makeHistoryRow( getSessionNode(currentItemDate).appendChild(makeHistoryRow(
item.url, item.title, currentItemDate.toLocaleTimeString() item.url, item.title, currentItemDate.toLocaleTimeString()
)); ));
@ -191,7 +191,7 @@ window.loadHistory = (function() {
* @return {void} * @return {void}
*/ */
function loadHistory() { function loadHistory() {
var url = DATA_URL.concat("?offset=", nextOffset.toString()); let url = DATA_URL.concat("?offset=", nextOffset.toString());
if (nextTime === null) { if (nextTime === null) {
getJSON(url, receiveHistory); getJSON(url, receiveHistory);
} else { } else {

View File

@ -27,16 +27,15 @@
*/ */
"use strict"; "use strict";
(function() { (function() {
// FIXME:qtwebengine integrate this with other window._qutebrowser code? // FIXME:qtwebengine integrate this with other window._qutebrowser code?
function isElementInViewport(node) { // eslint-disable-line complexity function isElementInViewport(node) { // eslint-disable-line complexity
var i; let i;
var boundingRect = (node.getClientRects()[0] || let boundingRect = (node.getClientRects()[0] ||
node.getBoundingClientRect()); node.getBoundingClientRect());
if (boundingRect.width <= 1 && boundingRect.height <= 1) { if (boundingRect.width <= 1 && boundingRect.height <= 1) {
var rects = node.getClientRects(); const rects = node.getClientRects();
for (i = 0; i < rects.length; i++) { for (i = 0; i < rects.length; i++) {
if (rects[i].width > rects[0].height && if (rects[i].width > rects[0].height &&
rects[i].height > rects[0].height) { rects[i].height > rects[0].height) {
@ -51,8 +50,8 @@
return null; return null;
} }
if (boundingRect.width <= 1 || boundingRect.height <= 1) { if (boundingRect.width <= 1 || boundingRect.height <= 1) {
var children = node.children; const children = node.children;
var visibleChildNode = false; let visibleChildNode = false;
for (i = 0; i < children.length; ++i) { for (i = 0; i < children.length; ++i) {
boundingRect = (children[i].getClientRects()[0] || boundingRect = (children[i].getClientRects()[0] ||
children[i].getBoundingClientRect()); children[i].getBoundingClientRect());
@ -69,7 +68,7 @@
boundingRect.left + boundingRect.width < -10) { boundingRect.left + boundingRect.width < -10) {
return null; return null;
} }
var computedStyle = window.getComputedStyle(node, null); const computedStyle = window.getComputedStyle(node, null);
if (computedStyle.visibility !== "visible" || if (computedStyle.visibility !== "visible" ||
computedStyle.display === "none" || computedStyle.display === "none" ||
node.hasAttribute("disabled") || node.hasAttribute("disabled") ||
@ -81,27 +80,27 @@
} }
function positionCaret() { function positionCaret() {
var walker = document.createTreeWalker(document.body, 4, null); const walker = document.createTreeWalker(document.body, 4, null);
var node; let node;
var textNodes = []; const textNodes = [];
var el; let el;
while ((node = walker.nextNode())) { while ((node = walker.nextNode())) {
if (node.nodeType === 3 && node.data.trim() !== "") { if (node.nodeType === 3 && node.data.trim() !== "") {
textNodes.push(node); textNodes.push(node);
} }
} }
for (var i = 0; i < textNodes.length; i++) { for (let i = 0; i < textNodes.length; i++) {
var element = textNodes[i].parentElement; const element = textNodes[i].parentElement;
if (isElementInViewport(element.parentElement)) { if (isElementInViewport(element.parentElement)) {
el = element; el = element;
break; break;
} }
} }
if (el !== undefined) { if (el !== undefined) {
var range = document.createRange(); const range = document.createRange();
range.setStart(el, 0); range.setStart(el, 0);
range.setEnd(el, 0); range.setEnd(el, 0);
var sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange(range); sel.addRange(range);
} }

View File

@ -20,19 +20,19 @@
"use strict"; "use strict";
window._qutebrowser.scroll = (function() { window._qutebrowser.scroll = (function() {
var funcs = {}; const funcs = {};
funcs.to_perc = function(x, y) { funcs.to_perc = (x, y) => {
var x_px = window.scrollX; let x_px = window.scrollX;
var y_px = window.scrollY; let y_px = window.scrollY;
var width = Math.max( const width = Math.max(
document.body.scrollWidth, document.body.scrollWidth,
document.body.offsetWidth, document.body.offsetWidth,
document.documentElement.scrollWidth, document.documentElement.scrollWidth,
document.documentElement.offsetWidth document.documentElement.offsetWidth
); );
var height = Math.max( const height = Math.max(
document.body.scrollHeight, document.body.scrollHeight,
document.body.offsetHeight, document.body.offsetHeight,
document.documentElement.scrollHeight, document.documentElement.scrollHeight,
@ -65,9 +65,9 @@ window._qutebrowser.scroll = (function() {
window.scroll(x_px, y_px); window.scroll(x_px, y_px);
}; };
funcs.delta_page = function(x, y) { funcs.delta_page = (x, y) => {
var dx = window.innerWidth * x; const dx = window.innerWidth * x;
var dy = window.innerHeight * y; const dy = window.innerHeight * y;
window.scrollBy(dx, dy); window.scrollBy(dx, dy);
}; };

View File

@ -37,22 +37,37 @@
"use strict"; "use strict";
window._qutebrowser.webelem = (function() { window._qutebrowser.webelem = (function() {
var funcs = {}; const funcs = {};
var elements = []; const elements = [];
function serialize_elem(elem) { function serialize_elem(elem) {
if (!elem) { if (!elem) {
return null; return null;
} }
var id = elements.length; const id = elements.length;
elements[id] = elem; elements[id] = elem;
var out = { // InvalidStateError will be thrown if elem doesn't have selectionStart
let caret_position = 0;
try {
caret_position = elem.selectionStart;
} catch (err) {
if (err instanceof DOMException &&
err.name === "InvalidStateError") {
// nothing to do, caret_position is already 0
} else {
// not the droid we're looking for
throw err;
}
}
const out = {
"id": id, "id": id,
"value": elem.value, "value": elem.value,
"outer_xml": elem.outerHTML, "outer_xml": elem.outerHTML,
"rects": [], // Gets filled up later "rects": [], // Gets filled up later
"caret_position": caret_position,
}; };
// https://github.com/qutebrowser/qutebrowser/issues/2569 // https://github.com/qutebrowser/qutebrowser/issues/2569
@ -77,16 +92,16 @@ window._qutebrowser.webelem = (function() {
out.text = elem.text; out.text = elem.text;
} // else: don't add the text at all } // else: don't add the text at all
var attributes = {}; const attributes = {};
for (var i = 0; i < elem.attributes.length; ++i) { for (let i = 0; i < elem.attributes.length; ++i) {
var attr = elem.attributes[i]; const attr = elem.attributes[i];
attributes[attr.name] = attr.value; attributes[attr.name] = attr.value;
} }
out.attributes = attributes; out.attributes = attributes;
var client_rects = elem.getClientRects(); const client_rects = elem.getClientRects();
for (var k = 0; k < client_rects.length; ++k) { for (let k = 0; k < client_rects.length; ++k) {
var rect = client_rects[k]; const rect = client_rects[k];
out.rects.push({ out.rects.push({
"top": rect.top, "top": rect.top,
"right": rect.right, "right": rect.right,
@ -111,8 +126,8 @@ window._qutebrowser.webelem = (function() {
// the cVim implementation here? // the cVim implementation here?
// https://github.com/1995eaton/chromium-vim/blob/1.2.85/content_scripts/dom.js#L74-L134 // https://github.com/1995eaton/chromium-vim/blob/1.2.85/content_scripts/dom.js#L74-L134
var win = elem.ownerDocument.defaultView; const win = elem.ownerDocument.defaultView;
var rect = elem.getBoundingClientRect(); let rect = elem.getBoundingClientRect();
if (!rect || if (!rect ||
rect.top > window.innerHeight || rect.top > window.innerHeight ||
@ -127,7 +142,7 @@ window._qutebrowser.webelem = (function() {
return false; return false;
} }
var style = win.getComputedStyle(elem, null); const style = win.getComputedStyle(elem, null);
if (style.getPropertyValue("visibility") !== "visible" || if (style.getPropertyValue("visibility") !== "visible" ||
style.getPropertyValue("display") === "none" || style.getPropertyValue("display") === "none" ||
style.getPropertyValue("opacity") === "0") { style.getPropertyValue("opacity") === "0") {
@ -144,11 +159,11 @@ window._qutebrowser.webelem = (function() {
return true; return true;
} }
funcs.find_css = function(selector, only_visible) { funcs.find_css = (selector, only_visible) => {
var elems = document.querySelectorAll(selector); const elems = document.querySelectorAll(selector);
var out = []; const out = [];
for (var i = 0; i < elems.length; ++i) { for (let i = 0; i < elems.length; ++i) {
if (!only_visible || is_visible(elems[i])) { if (!only_visible || is_visible(elems[i])) {
out.push(serialize_elem(elems[i])); out.push(serialize_elem(elems[i]));
} }
@ -157,13 +172,13 @@ window._qutebrowser.webelem = (function() {
return out; return out;
}; };
funcs.find_id = function(id) { funcs.find_id = (id) => {
var elem = document.getElementById(id); const elem = document.getElementById(id);
return serialize_elem(elem); return serialize_elem(elem);
}; };
funcs.find_focused = function() { funcs.find_focused = () => {
var elem = document.activeElement; const elem = document.activeElement;
if (!elem || elem === document.body) { if (!elem || elem === document.body) {
// "When there is no selection, the active element is the page's // "When there is no selection, the active element is the page's
@ -174,43 +189,43 @@ window._qutebrowser.webelem = (function() {
return serialize_elem(elem); return serialize_elem(elem);
}; };
funcs.find_at_pos = function(x, y) { funcs.find_at_pos = (x, y) => {
// FIXME:qtwebengine // FIXME:qtwebengine
// If the element at the specified point belongs to another document // If the element at the specified point belongs to another document
// (for example, an iframe's subdocument), the subdocument's parent // (for example, an iframe's subdocument), the subdocument's parent
// element is returned (the iframe itself). // element is returned (the iframe itself).
var elem = document.elementFromPoint(x, y); const elem = document.elementFromPoint(x, y);
return serialize_elem(elem); return serialize_elem(elem);
}; };
// Function for returning a selection to python (so we can click it) // Function for returning a selection to python (so we can click it)
funcs.find_selected_link = function() { funcs.find_selected_link = () => {
var elem = window.getSelection().anchorNode; const elem = window.getSelection().anchorNode;
if (!elem) { if (!elem) {
return null; return null;
} }
return serialize_elem(elem.parentNode); return serialize_elem(elem.parentNode);
}; };
funcs.set_value = function(id, value) { funcs.set_value = (id, value) => {
elements[id].value = value; elements[id].value = value;
}; };
funcs.insert_text = function(id, text) { funcs.insert_text = (id, text) => {
var elem = elements[id]; const elem = elements[id];
elem.focus(); elem.focus();
document.execCommand("insertText", false, text); document.execCommand("insertText", false, text);
}; };
funcs.set_attribute = function(id, name, value) { funcs.set_attribute = (id, name, value) => {
elements[id].setAttribute(name, value); elements[id].setAttribute(name, value);
}; };
funcs.remove_blank_target = function(id) { funcs.remove_blank_target = (id) => {
var elem = elements[id]; let elem = elements[id];
while (elem !== null) { while (elem !== null) {
var tag = elem.tagName.toLowerCase(); const tag = elem.tagName.toLowerCase();
if (tag === "a" || tag === "area") { if (tag === "a" || tag === "area") {
if (elem.getAttribute("target") === "_blank") { if (elem.getAttribute("target") === "_blank") {
elem.setAttribute("target", "_top"); elem.setAttribute("target", "_top");
@ -221,18 +236,18 @@ window._qutebrowser.webelem = (function() {
} }
}; };
funcs.click = function(id) { funcs.click = (id) => {
var elem = elements[id]; const elem = elements[id];
elem.click(); elem.click();
}; };
funcs.focus = function(id) { funcs.focus = (id) => {
var elem = elements[id]; const elem = elements[id];
elem.focus(); elem.focus();
}; };
funcs.move_cursor_to_end = function(id) { funcs.move_cursor_to_end = (id) => {
var elem = elements[id]; const elem = elements[id];
elem.selectionStart = elem.value.length; elem.selectionStart = elem.value.length;
elem.selectionEnd = elem.value.length; elem.selectionEnd = elem.value.length;
}; };

View File

@ -19,6 +19,7 @@
"""Base class for vim-like key sequence parser.""" """Base class for vim-like key sequence parser."""
import enum
import re import re
import unicodedata import unicodedata
@ -75,8 +76,8 @@ class BaseKeyParser(QObject):
do_log = True do_log = True
passthrough = False passthrough = False
Match = usertypes.enum('Match', ['partial', 'definitive', 'other', 'none']) Match = enum.Enum('Match', ['partial', 'definitive', 'other', 'none'])
Type = usertypes.enum('Type', ['chain', 'special']) Type = enum.Enum('Type', ['chain', 'special'])
def __init__(self, win_id, parent=None, supports_count=None, def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False): supports_chains=False):

View File

@ -24,6 +24,7 @@ Module attributes:
""" """
import traceback import traceback
import enum
from PyQt5.QtCore import pyqtSlot, Qt from PyQt5.QtCore import pyqtSlot, Qt
@ -34,7 +35,7 @@ from qutebrowser.utils import usertypes, log, message, objreg, utils
STARTCHARS = ":/?" STARTCHARS = ":/?"
LastPress = usertypes.enum('LastPress', ['none', 'filtertext', 'keystring']) LastPress = enum.Enum('LastPress', ['none', 'filtertext', 'keystring'])
class NormalKeyParser(keyparser.CommandKeyParser): class NormalKeyParser(keyparser.CommandKeyParser):

View File

@ -19,6 +19,7 @@
"""The main statusbar widget.""" """The main statusbar widget."""
import enum
import attr import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
@ -46,7 +47,7 @@ class ColorFlags:
passthrough: If we're currently in passthrough-mode. passthrough: If we're currently in passthrough-mode.
""" """
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection']) CaretMode = enum.Enum('CaretMode', ['off', 'on', 'selection'])
prompt = attr.ib(False) prompt = attr.ib(False)
insert = attr.ib(False) insert = attr.ib(False)
command = attr.ib(False) command = attr.ib(False)

View File

@ -19,10 +19,12 @@
"""Text displayed in the statusbar.""" """Text displayed in the statusbar."""
import enum
from PyQt5.QtCore import pyqtSlot from PyQt5.QtCore import pyqtSlot
from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.utils import usertypes, log from qutebrowser.utils import log
class Text(textbase.TextBase): class Text(textbase.TextBase):
@ -37,7 +39,7 @@ class Text(textbase.TextBase):
available. If not, the permanent text is shown. available. If not, the permanent text is shown.
""" """
Text = usertypes.enum('Text', ['normal', 'temp']) Text = enum.Enum('Text', ['normal', 'temp'])
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)

View File

@ -19,6 +19,8 @@
"""URL displayed in the statusbar.""" """URL displayed in the statusbar."""
import enum
from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl
from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.mainwindow.statusbar import textbase
@ -27,8 +29,8 @@ from qutebrowser.utils import usertypes, urlutils
# Note this has entries for success/error/warn from widgets.webview:LoadStatus # Note this has entries for success/error/warn from widgets.webview:LoadStatus
UrlType = usertypes.enum('UrlType', ['success', 'success_https', 'error', UrlType = enum.Enum('UrlType', ['success', 'success_https', 'error', 'warn',
'warn', 'hover', 'normal']) 'hover', 'normal'])
class UrlText(textbase.TextBase): class UrlText(textbase.TextBase):

View File

@ -259,13 +259,14 @@ class TabbedBrowser(tabwidget.TabWidget):
def tab_close_prompt_if_pinned(self, tab, force, yes_action): def tab_close_prompt_if_pinned(self, tab, force, yes_action):
"""Helper method for tab_close. """Helper method for tab_close.
If tab is pinned, prompt. If everything is good, run yes_action. If tab is pinned, prompt. If not, run yes_action.
If tab is destroyed, abort question.
""" """
if tab.data.pinned and not force: if tab.data.pinned and not force:
message.confirm_async( message.confirm_async(
title='Pinned Tab', title='Pinned Tab',
text="Are you sure you want to close a pinned tab?", text="Are you sure you want to close a pinned tab?",
yes_action=yes_action, default=False) yes_action=yes_action, default=False, abort_on=[tab.destroyed])
else: else:
yes_action() yes_action()

View File

@ -20,6 +20,7 @@
"""The tab widget used for TabbedBrowser from browser.py.""" """The tab widget used for TabbedBrowser from browser.py."""
import functools import functools
import enum
import attr import attr
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,
@ -34,8 +35,8 @@ from qutebrowser.config import config
from qutebrowser.misc import objects from qutebrowser.misc import objects
PixelMetrics = usertypes.enum('PixelMetrics', ['icon_padding'], PixelMetrics = enum.IntEnum('PixelMetrics', ['icon_padding'],
start=QStyle.PM_CustomBase, is_int=True) start=QStyle.PM_CustomBase)
class TabWidget(QTabWidget): class TabWidget(QTabWidget):
@ -336,7 +337,7 @@ class TabBar(QTabBar):
return self.parent().currentWidget() return self.parent().currentWidget()
@pyqtSlot(str) @pyqtSlot(str)
def _on_config_changed(self, option): def _on_config_changed(self, option: str):
if option == 'fonts.tabs': if option == 'fonts.tabs':
self._set_font() self._set_font()
elif option == 'tabs.favicons.scale': elif option == 'tabs.favicons.scale':
@ -351,6 +352,12 @@ class TabBar(QTabBar):
if option.startswith('colors.tabs.'): if option.startswith('colors.tabs.'):
self.update() self.update()
# Clear _minimum_tab_size_hint_helper cache when appropriate
if option in ["tabs.indicator_padding",
"tabs.padding",
"tabs.width.indicator"]:
self._minimum_tab_size_hint_helper.cache_clear()
def _on_show_switching_delay_changed(self): def _on_show_switching_delay_changed(self):
"""Set timer interval when tabs.show_switching_delay got changed.""" """Set timer interval when tabs.show_switching_delay got changed."""
self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay) self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay)
@ -459,7 +466,7 @@ class TabBar(QTabBar):
return return
super().mousePressEvent(e) super().mousePressEvent(e)
def minimumTabSizeHint(self, index, ellipsis: bool = True): def minimumTabSizeHint(self, index, ellipsis: bool = True) -> QSize:
"""Set the minimum tab size to indicator/icon/... text. """Set the minimum tab size to indicator/icon/... text.
Args: Args:
@ -469,38 +476,47 @@ class TabBar(QTabBar):
Return: Return:
A QSize of the smallest tab size we can make. A QSize of the smallest tab size we can make.
""" """
text = '\u2026' if ellipsis else self.tabText(index) icon = self.tabIcon(index)
extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None, self)
if icon.isNull():
icon_width = 0
else:
icon_width = icon.actualSize(QSize(extent, extent)).width()
return self._minimum_tab_size_hint_helper(self.tabText(index),
icon_width,
ellipsis)
@functools.lru_cache(maxsize=2**9)
def _minimum_tab_size_hint_helper(self, tab_text: str,
icon_width: int,
ellipsis: bool) -> QSize:
"""Helper function to cache tab results.
Config values accessed in here should be added to _on_config_changed to
ensure cache is flushed when needed.
"""
text = '\u2026' if ellipsis else tab_text
# Don't ever shorten if text is shorter than the ellipsis # Don't ever shorten if text is shorter than the ellipsis
text_width = min(self.fontMetrics().width(text), text_width = min(self.fontMetrics().width(text),
self.fontMetrics().width(self.tabText(index))) self.fontMetrics().width(tab_text))
icon = self.tabIcon(index)
padding = config.val.tabs.padding padding = config.val.tabs.padding
indicator_padding = config.val.tabs.indicator_padding indicator_padding = config.val.tabs.indicator_padding
padding_h = padding.left + padding.right padding_h = padding.left + padding.right
padding_h += indicator_padding.left + indicator_padding.right padding_h += indicator_padding.left + indicator_padding.right
padding_v = padding.top + padding.bottom padding_v = padding.top + padding.bottom
if icon.isNull():
icon_size = QSize(0, 0)
else:
extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None,
self)
icon_size = icon.actualSize(QSize(extent, extent))
height = self.fontMetrics().height() + padding_v height = self.fontMetrics().height() + padding_v
width = (text_width + icon_size.width() + width = (text_width + icon_width +
padding_h + config.val.tabs.width.indicator) padding_h + config.val.tabs.width.indicator)
return QSize(width, height) return QSize(width, height)
def _tab_total_width_pinned(self): def _pinned_statistics(self) -> (int, int):
"""Get the current total width of pinned tabs. """Get the number of pinned tabs and the total width of pinned tabs."""
pinned_list = [idx for idx in range(self.count())
This width is calculated assuming no shortening due to ellipsis.""" if self._tab_pinned(idx)]
return sum(self.minimumTabSizeHint(idx, ellipsis=False).width() pinned_count = len(pinned_list)
for idx in range(self.count()) pinned_width = sum(self.minimumTabSizeHint(idx, ellipsis=False).width()
if self._tab_pinned(idx)) for idx in pinned_list)
return (pinned_count, pinned_width)
def _pinnedCount(self) -> int:
"""Get the number of pinned tabs."""
return sum(self._tab_pinned(idx) for idx in range(self.count()))
def _tab_pinned(self, index: int) -> bool: def _tab_pinned(self, index: int) -> bool:
"""Return True if tab is pinned.""" """Return True if tab is pinned."""
@ -539,8 +555,8 @@ class TabBar(QTabBar):
return QSize() return QSize()
else: else:
pinned = self._tab_pinned(index) pinned = self._tab_pinned(index)
no_pinned_count = self.count() - self._pinnedCount() pinned_count, pinned_width = self._pinned_statistics()
pinned_width = self._tab_total_width_pinned() no_pinned_count = self.count() - pinned_count
no_pinned_width = self.width() - pinned_width no_pinned_width = self.width() - pinned_width
if pinned: if pinned:

View File

@ -25,6 +25,7 @@ import functools
import html import html
import ctypes import ctypes
import ctypes.util import ctypes.util
import enum
import attr import attr
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@ -37,10 +38,10 @@ from qutebrowser.utils import usertypes, objreg, version, qtutils, log, utils
from qutebrowser.misc import objects, msgbox from qutebrowser.misc import objects, msgbox
_Result = usertypes.enum( _Result = enum.IntEnum(
'_Result', '_Result',
['quit', 'restart', 'restart_webkit', 'restart_webengine'], ['quit', 'restart', 'restart_webkit', 'restart_webengine'],
is_int=True, start=QDialog.Accepted + 1) start=QDialog.Accepted + 1)
@attr.s @attr.s

View File

@ -27,6 +27,7 @@ import getpass
import fnmatch import fnmatch
import traceback import traceback
import datetime import datetime
import enum
import pkg_resources import pkg_resources
from PyQt5.QtCore import pyqtSlot, Qt, QSize from PyQt5.QtCore import pyqtSlot, Qt, QSize
@ -35,13 +36,13 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QDialogButtonBox, QApplication, QMessageBox) QDialogButtonBox, QApplication, QMessageBox)
import qutebrowser import qutebrowser
from qutebrowser.utils import version, log, utils, objreg, usertypes from qutebrowser.utils import version, log, utils, objreg
from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient, from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
pastebin) pastebin)
from qutebrowser.config import config, configfiles from qutebrowser.config import config, configfiles
Result = usertypes.enum('Result', ['restore', 'no_restore'], is_int=True, Result = enum.IntEnum('Result', ['restore', 'no_restore'],
start=QDialog.Accepted + 1) start=QDialog.Accepted + 1)

View File

@ -96,11 +96,12 @@ class ExternalEditor(QObject):
def on_proc_error(self, _err): def on_proc_error(self, _err):
self._cleanup() self._cleanup()
def edit(self, text): def edit(self, text, caret_position=0):
"""Edit a given text. """Edit a given text.
Args: Args:
text: The initial text to edit. text: The initial text to edit.
caret_position: The position of the caret in the text.
""" """
if self._filename is not None: if self._filename is not None:
raise ValueError("Already editing a file!") raise ValueError("Already editing a file!")
@ -121,7 +122,9 @@ class ExternalEditor(QObject):
return return
self._remove_file = True self._remove_file = True
self._start_editor()
line, column = self._calc_line_and_column(text, caret_position)
self._start_editor(line=line, column=column)
def edit_file(self, filename): def edit_file(self, filename):
"""Edit the file with the given filename.""" """Edit the file with the given filename."""
@ -129,13 +132,82 @@ class ExternalEditor(QObject):
self._remove_file = False self._remove_file = False
self._start_editor() self._start_editor()
def _start_editor(self): def _start_editor(self, line=1, column=1):
"""Start the editor with the file opened as self._filename.""" """Start the editor with the file opened as self._filename.
Args:
line: the line number to pass to the editor
column: the column number to pass to the editor
"""
self._proc = guiprocess.GUIProcess(what='editor', parent=self) self._proc = guiprocess.GUIProcess(what='editor', parent=self)
self._proc.finished.connect(self.on_proc_closed) self._proc.finished.connect(self.on_proc_closed)
self._proc.error.connect(self.on_proc_error) self._proc.error.connect(self.on_proc_error)
editor = config.val.editor.command editor = config.val.editor.command
executable = editor[0] executable = editor[0]
args = [arg.replace('{}', self._filename) for arg in editor[1:]]
args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]]
log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
self._proc.start(executable, args) self._proc.start(executable, args)
def _calc_line_and_column(self, text, caret_position):
r"""Calculate line and column numbers given a text and caret position.
Both line and column are 1-based indexes, because that's what most
editors use as line and column starting index. By "most" we mean at
least vim, nvim, gvim, emacs, atom, sublimetext, notepad++, brackets,
visual studio, QtCreator and so on.
To find the line we just count how many newlines there are before the
caret and add 1.
To find the column we calculate the difference between the caret and
the last newline before the caret.
For example in the text `aaa\nbb|bbb` (| represents the caret):
caret_position = 6
text[:caret_position] = `aaa\nbb`
text[:caret_position].count('\n') = 1
caret_position - text[:caret_position].rfind('\n') = 3
Thus line, column = 2, 3, and the caret is indeed in the second
line, third column
Args:
text: the text for which the numbers must be calculated
caret_position: the position of the caret in the text
Return:
A (line, column) tuple of (int, int)
"""
line = text[:caret_position].count('\n') + 1
column = caret_position - text[:caret_position].rfind('\n')
return line, column
def _sub_placeholder(self, arg, line, column):
"""Substitute a single placeholder.
If the `arg` input to this function is a valid placeholder it will
be substituted with the appropriate value, otherwise it will be left
unchanged.
Args:
arg: an argument of editor.command.
line: the previously-calculated line number for the text caret.
column: the previously-calculated column number for the text caret.
Return:
The substituted placeholder or the original argument.
"""
replacements = {
'{}': self._filename,
'{file}': self._filename,
'{line}': str(line),
'{line0}': str(line-1),
'{column}': str(column),
'{column0}': str(column-1)
}
for old, new in replacements.items():
arg = arg.replace(old, new)
return arg

View File

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

View File

@ -182,9 +182,17 @@ def debug_cache_stats():
except ImportError: except ImportError:
pass pass
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
# pylint: disable=protected-access
tab_bar = tabbed_browser.tabBar()
tabbed_browser_info = tab_bar._minimum_tab_size_hint_helper.cache_info()
# pylint: enable=protected-access
log.misc.debug('is_valid_prefix: {}'.format(prefix_info)) log.misc.debug('is_valid_prefix: {}'.format(prefix_info))
log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info)) log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info))
log.misc.debug('history: {}'.format(history_info)) log.misc.debug('history: {}'.format(history_info))
log.misc.debug('tab width cache: {}'.format(tabbed_browser_info))
@cmdutils.register(debug=True) @cmdutils.register(debug=True)

View File

@ -24,9 +24,10 @@ import sys
import inspect import inspect
import os.path import os.path
import collections import collections
import enum
import qutebrowser import qutebrowser
from qutebrowser.utils import usertypes, log, utils from qutebrowser.utils import log, utils
def is_git_repo(): def is_git_repo():
@ -75,7 +76,7 @@ class DocstringParser:
arg_descs: A dict of argument names to their descriptions arg_descs: A dict of argument names to their descriptions
""" """
State = usertypes.enum('State', ['short', 'desc', 'desc_hidden', State = enum.Enum('State', ['short', 'desc', 'desc_hidden',
'arg_start', 'arg_inside', 'misc']) 'arg_start', 'arg_inside', 'misc'])
def __init__(self, func): def __init__(self, func):

View File

@ -24,17 +24,18 @@ import sys
import shutil import shutil
import os.path import os.path
import contextlib import contextlib
import enum
from PyQt5.QtCore import QStandardPaths from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import log, debug, usertypes, message, utils from qutebrowser.utils import log, debug, message, utils
# The cached locations # The cached locations
_locations = {} _locations = {}
Location = usertypes.enum('Location', ['config', 'auto_config', Location = enum.Enum('Location', ['config', 'auto_config',
'data', 'system_data', 'data', 'system_data',
'cache', 'download', 'runtime']) 'cache', 'download', 'runtime'])

View File

@ -25,7 +25,7 @@ Module attributes:
import operator import operator
import collections.abc import collections.abc
import enum as pyenum import enum
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
@ -35,22 +35,6 @@ from qutebrowser.utils import log, qtutils, utils
_UNSET = object() _UNSET = object()
def enum(name, items, start=1, is_int=False):
"""Factory for simple enumerations.
Args:
name: Name of the enum
items: Iterable of items to be sequentially enumerated.
start: The number to use for the first value.
We use 1 as default so enum members are always True.
is_init: True if the enum should be a Python IntEnum
"""
enums = [(v, i) for (i, v) in enumerate(items, start)]
base = pyenum.IntEnum if is_int else pyenum.Enum
base = pyenum.unique(base)
return base(name, enums)
class NeighborList(collections.abc.Sequence): class NeighborList(collections.abc.Sequence):
"""A list of items which saves its current position. """A list of items which saves its current position.
@ -65,7 +49,7 @@ class NeighborList(collections.abc.Sequence):
_mode: The current mode. _mode: The current mode.
""" """
Modes = enum('Modes', ['edge', 'exception']) Modes = enum.Enum('Modes', ['edge', 'exception'])
def __init__(self, items=None, default=_UNSET, mode=Modes.exception): def __init__(self, items=None, default=_UNSET, mode=Modes.exception):
"""Constructor. """Constructor.
@ -221,45 +205,46 @@ class NeighborList(collections.abc.Sequence):
# The mode of a Question. # The mode of a Question.
PromptMode = enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert', PromptMode = enum.Enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
'download']) 'download'])
# Where to open a clicked link. # Where to open a clicked link.
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window', ClickTarget = enum.Enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window',
'hover']) 'hover'])
# Key input modes # Key input modes
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', KeyMode = enum.Enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough', 'caret', 'set_mark', 'insert', 'passthrough', 'caret', 'set_mark',
'jump_mark', 'record_macro', 'run_macro']) 'jump_mark', 'record_macro', 'run_macro'])
# Exit statuses for errors. Needs to be an int for sys.exit. # Exit statuses for errors. Needs to be an int for sys.exit.
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', Exit = enum.IntEnum('Exit', ['ok', 'reserved', 'exception', 'err_ipc',
'err_config', 'err_key_config'], is_int=True, start=0) 'err_init', 'err_config', 'err_key_config'],
start=0)
# Load status of a tab # Load status of a tab
LoadStatus = enum('LoadStatus', ['none', 'success', 'success_https', 'error', LoadStatus = enum.Enum('LoadStatus', ['none', 'success', 'success_https',
'warn', 'loading']) 'error', 'warn', 'loading'])
# Backend of a tab # Backend of a tab
Backend = enum('Backend', ['QtWebKit', 'QtWebEngine']) Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine'])
# JS world for QtWebEngine # JS world for QtWebEngine
JsWorld = enum('JsWorld', ['main', 'application', 'user', 'jseval']) JsWorld = enum.Enum('JsWorld', ['main', 'application', 'user', 'jseval'])
# Log level of a JS message. This needs to match up with the keys allowed for # Log level of a JS message. This needs to match up with the keys allowed for
# the content.javascript.log setting. # the content.javascript.log setting.
JsLogLevel = enum('JsLogLevel', ['unknown', 'info', 'warning', 'error']) JsLogLevel = enum.Enum('JsLogLevel', ['unknown', 'info', 'warning', 'error'])
MessageLevel = enum('MessageLevel', ['error', 'warning', 'info']) MessageLevel = enum.Enum('MessageLevel', ['error', 'warning', 'info'])
class Question(QObject): class Question(QObject):

View File

@ -882,7 +882,7 @@ def yaml_load(f):
end = datetime.datetime.now() end = datetime.datetime.now()
delta = (end - start).total_seconds() delta = (end - start).total_seconds()
deadline = 3 if 'CI' in os.environ else 2 deadline = 5 if 'CI' in os.environ else 2
if delta > deadline: # pragma: no cover if delta > deadline: # pragma: no cover
log.misc.warning( log.misc.warning(
"YAML load took unusually long, please report this at " "YAML load took unusually long, please report this at "

View File

@ -27,6 +27,7 @@ import platform
import subprocess import subprocess
import importlib import importlib
import collections import collections
import enum
import pkg_resources import pkg_resources
import attr import attr
@ -63,7 +64,7 @@ class DistributionInfo:
pretty = attr.ib() pretty = attr.ib()
Distribution = usertypes.enum( Distribution = enum.Enum(
'Distribution', ['unknown', 'ubuntu', 'debian', 'void', 'arch', 'Distribution', ['unknown', 'ubuntu', 'debian', 'void', 'arch',
'gentoo', 'fedora', 'opensuse', 'linuxmint', 'manjaro']) 'gentoo', 'fedora', 'opensuse', 'linuxmint', 'manjaro'])
@ -151,12 +152,14 @@ def _git_str_subprocess(gitpath):
return None return None
try: try:
# https://stackoverflow.com/questions/21017300/21017394#21017394 # https://stackoverflow.com/questions/21017300/21017394#21017394
commit_hash = subprocess.check_output( commit_hash = subprocess.run(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'], ['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=gitpath).decode('UTF-8').strip() cwd=gitpath, check=True,
date = subprocess.check_output( stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
date = subprocess.run(
['git', 'show', '-s', '--format=%ci', 'HEAD'], ['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=gitpath).decode('UTF-8').strip() cwd=gitpath, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
return '{} ({})'.format(commit_hash, date) return '{} ({})'.format(commit_hash, date)
except (subprocess.CalledProcessError, OSError): except (subprocess.CalledProcessError, OSError):
return None return None

View File

@ -117,7 +117,6 @@ class AsciiDoc:
def _build_website_file(self, root, filename): def _build_website_file(self, root, filename):
"""Build a single website file.""" """Build a single website file."""
# pylint: disable=too-many-locals,too-many-statements
src = os.path.join(root, filename) src = os.path.join(root, filename)
src_basename = os.path.basename(src) src_basename = os.path.basename(src)
parts = [self._args.website[0]] parts = [self._args.website[0]]
@ -225,7 +224,7 @@ class AsciiDoc:
return self._args.asciidoc return self._args.asciidoc
try: try:
subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL, subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) stderr=subprocess.DEVNULL)
except OSError: except OSError:
pass pass
@ -233,7 +232,7 @@ class AsciiDoc:
return ['asciidoc'] return ['asciidoc']
try: try:
subprocess.call(['asciidoc.py'], stdout=subprocess.DEVNULL, subprocess.run(['asciidoc.py'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) stderr=subprocess.DEVNULL)
except OSError: except OSError:
pass pass
@ -259,7 +258,7 @@ class AsciiDoc:
try: try:
env = os.environ.copy() env = os.environ.copy()
env['HOME'] = self._homedir env['HOME'] = self._homedir
subprocess.check_call(cmdline, env=env) subprocess.run(cmdline, check=True, env=env)
except (subprocess.CalledProcessError, OSError) as e: except (subprocess.CalledProcessError, OSError) as e:
self._failed = True self._failed = True
utils.print_col(str(e), 'red') utils.print_col(str(e), 'red')

View File

@ -50,7 +50,7 @@ def call_script(name, *args, python=sys.executable):
python: The python interpreter to use. python: The python interpreter to use.
""" """
path = os.path.join(os.path.dirname(__file__), os.pardir, name) path = os.path.join(os.path.dirname(__file__), os.pardir, name)
subprocess.check_call([python, path] + list(args)) subprocess.run([python, path] + list(args), check=True)
def call_tox(toxenv, *args, python=sys.executable): def call_tox(toxenv, *args, python=sys.executable):
@ -64,9 +64,9 @@ def call_tox(toxenv, *args, python=sys.executable):
env = os.environ.copy() env = os.environ.copy()
env['PYTHON'] = python env['PYTHON'] = python
env['PATH'] = os.environ['PATH'] + os.pathsep + os.path.dirname(python) env['PATH'] = os.environ['PATH'] + os.pathsep + os.path.dirname(python)
subprocess.check_call( subprocess.run(
[sys.executable, '-m', 'tox', '-vv', '-e', toxenv] + list(args), [sys.executable, '-m', 'tox', '-vv', '-e', toxenv] + list(args),
env=env) env=env, check=True)
def run_asciidoc2html(args): def run_asciidoc2html(args):
@ -89,8 +89,9 @@ def _maybe_remove(path):
def smoke_test(executable): def smoke_test(executable):
"""Try starting the given qutebrowser executable.""" """Try starting the given qutebrowser executable."""
subprocess.check_call([executable, '--no-err-windows', '--nowindow', subprocess.run([executable, '--no-err-windows', '--nowindow',
'--temp-basedir', 'about:blank', ':later 500 quit']) '--temp-basedir', 'about:blank', ':later 500 quit'],
check=True)
def patch_mac_app(): def patch_mac_app():
@ -178,7 +179,7 @@ def build_mac():
utils.print_title("Patching .app") utils.print_title("Patching .app")
patch_mac_app() patch_mac_app()
utils.print_title("Building .dmg") utils.print_title("Building .dmg")
subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-dmg']) subprocess.run(['make', '-f', 'scripts/dev/Makefile-dmg'], check=True)
dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__) dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__)
os.rename('qutebrowser.dmg', dmg_name) os.rename('qutebrowser.dmg', dmg_name)
@ -187,14 +188,14 @@ def build_mac():
try: try:
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
subprocess.check_call(['hdiutil', 'attach', dmg_name, subprocess.run(['hdiutil', 'attach', dmg_name,
'-mountpoint', tmpdir]) '-mountpoint', tmpdir], check=True)
try: try:
binary = os.path.join(tmpdir, 'qutebrowser.app', 'Contents', binary = os.path.join(tmpdir, 'qutebrowser.app', 'Contents',
'MacOS', 'qutebrowser') 'MacOS', 'qutebrowser')
smoke_test(binary) smoke_test(binary)
finally: finally:
subprocess.call(['hdiutil', 'detach', tmpdir]) subprocess.run(['hdiutil', 'detach', tmpdir])
except PermissionError as e: except PermissionError as e:
print("Failed to remove tempdir: {}".format(e)) print("Failed to remove tempdir: {}".format(e))
@ -242,13 +243,13 @@ def build_windows():
patch_windows(out_64) patch_windows(out_64)
utils.print_title("Building installers") utils.print_title("Building installers")
subprocess.check_call(['makensis.exe', subprocess.run(['makensis.exe',
'/DVERSION={}'.format(qutebrowser.__version__), '/DVERSION={}'.format(qutebrowser.__version__),
'misc/qutebrowser.nsi']) 'misc/qutebrowser.nsi'], check=True)
subprocess.check_call(['makensis.exe', subprocess.run(['makensis.exe',
'/DX64', '/DX64',
'/DVERSION={}'.format(qutebrowser.__version__), '/DVERSION={}'.format(qutebrowser.__version__),
'misc/qutebrowser.nsi']) 'misc/qutebrowser.nsi'], check=True)
name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__) name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__)
name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__) name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
@ -292,12 +293,12 @@ def build_sdist():
_maybe_remove('dist') _maybe_remove('dist')
subprocess.check_call([sys.executable, 'setup.py', 'sdist']) subprocess.run([sys.executable, 'setup.py', 'sdist'], check=True)
dist_files = os.listdir(os.path.abspath('dist')) dist_files = os.listdir(os.path.abspath('dist'))
assert len(dist_files) == 1 assert len(dist_files) == 1
dist_file = os.path.join('dist', dist_files[0]) dist_file = os.path.join('dist', dist_files[0])
subprocess.check_call(['gpg', '--detach-sign', '-a', dist_file]) subprocess.run(['gpg', '--detach-sign', '-a', dist_file], check=True)
tar = tarfile.open(dist_file) tar = tarfile.open(dist_file)
by_ext = collections.defaultdict(list) by_ext = collections.defaultdict(list)
@ -366,7 +367,7 @@ def github_upload(artifacts, tag):
def pypi_upload(artifacts): def pypi_upload(artifacts):
"""Upload the given artifacts to PyPI using twine.""" """Upload the given artifacts to PyPI using twine."""
filenames = [a[0] for a in artifacts] filenames = [a[0] for a in artifacts]
subprocess.check_call(['twine', 'upload'] + filenames) subprocess.run(['twine', 'upload'] + filenames, check=True)
def main(): def main():

View File

@ -285,8 +285,8 @@ def main_check():
print(msg.text) print(msg.text)
print() print()
filters = ','.join('qutebrowser/' + msg.filename for msg in messages) filters = ','.join('qutebrowser/' + msg.filename for msg in messages)
subprocess.check_call([sys.executable, '-m', 'coverage', 'report', subprocess.run([sys.executable, '-m', 'coverage', 'report',
'--show-missing', '--include', filters]) '--show-missing', '--include', filters], check=True)
print() print()
print("To debug this, run 'tox -e py36-pyqt59-cov' " print("To debug this, run 'tox -e py36-pyqt59-cov' "
"(or py35-pyqt59-cov) locally and check htmlcov/index.html") "(or py35-pyqt59-cov) locally and check htmlcov/index.html")
@ -312,9 +312,9 @@ def main_check_all():
for test_file, src_file in PERFECT_FILES: for test_file, src_file in PERFECT_FILES:
if test_file is None: if test_file is None:
continue continue
subprocess.check_call( subprocess.run(
[sys.executable, '-m', 'pytest', '--cov', 'qutebrowser', [sys.executable, '-m', 'pytest', '--cov', 'qutebrowser',
'--cov-report', 'xml', test_file]) '--cov-report', 'xml', test_file], check=True)
with open('coverage.xml', encoding='utf-8') as f: with open('coverage.xml', encoding='utf-8') as f:
messages = check(f, [(test_file, src_file)]) messages = check(f, [(test_file, src_file)])
os.remove('coverage.xml') os.remove('coverage.xml')

View File

@ -24,7 +24,8 @@ import sys
import subprocess import subprocess
import os import os
code = subprocess.call(['git', '--no-pager', 'diff', '--exit-code', '--stat']) code = subprocess.run(['git', '--no-pager', 'diff',
'--exit-code', '--stat']).returncode
if os.environ.get('TRAVIS_PULL_REQUEST', 'false') != 'false': if os.environ.get('TRAVIS_PULL_REQUEST', 'false') != 'false':
if code != 0: if code != 0:
@ -42,6 +43,6 @@ if code != 0:
if 'TRAVIS' in os.environ: if 'TRAVIS' in os.environ:
print() print()
print("travis_fold:start:gitdiff") print("travis_fold:start:gitdiff")
subprocess.call(['git', '--no-pager', 'diff']) subprocess.run(['git', '--no-pager', 'diff'])
print("travis_fold:end:gitdiff") print("travis_fold:end:gitdiff")
sys.exit(code) sys.exit(code)

View File

@ -23,4 +23,4 @@
import subprocess import subprocess
with open('qutebrowser/resources.py', 'w', encoding='utf-8') as f: with open('qutebrowser/resources.py', 'w', encoding='utf-8') as f:
subprocess.check_call(['pyrcc5', 'qutebrowser.rcc'], stdout=f) subprocess.run(['pyrcc5', 'qutebrowser.rcc'], stdout=f, check=True)

View File

@ -84,7 +84,8 @@ def parse_coredumpctl_line(line):
def get_info(pid): def get_info(pid):
"""Get and parse "coredumpctl info" output for the given PID.""" """Get and parse "coredumpctl info" output for the given PID."""
data = {} data = {}
output = subprocess.check_output(['coredumpctl', 'info', str(pid)]) output = subprocess.run(['coredumpctl', 'info', str(pid)], check=True,
stdout=subprocess.PIPE).stdout
output = output.decode('utf-8') output = output.decode('utf-8')
for line in output.split('\n'): for line in output.split('\n'):
if not line.strip(): if not line.strip():
@ -117,12 +118,12 @@ def dump_infos_gdb(parsed):
"""Dump all needed infos for the given crash using gdb.""" """Dump all needed infos for the given crash using gdb."""
with tempfile.TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory() as tempdir:
coredump = os.path.join(tempdir, 'dump') coredump = os.path.join(tempdir, 'dump')
subprocess.check_call(['coredumpctl', 'dump', '-o', coredump, subprocess.run(['coredumpctl', 'dump', '-o', coredump,
str(parsed.pid)]) str(parsed.pid)], check=True)
subprocess.check_call(['gdb', parsed.exe, coredump, subprocess.run(['gdb', parsed.exe, coredump,
'-ex', 'info threads', '-ex', 'info threads',
'-ex', 'thread apply all bt full', '-ex', 'thread apply all bt full',
'-ex', 'quit']) '-ex', 'quit'], check=True)
def dump_infos(parsed): def dump_infos(parsed):
@ -143,7 +144,7 @@ def check_prerequisites():
"""Check if coredumpctl/gdb are installed.""" """Check if coredumpctl/gdb are installed."""
for binary in ['coredumpctl', 'gdb']: for binary in ['coredumpctl', 'gdb']:
try: try:
subprocess.check_call([binary, '--version']) subprocess.run([binary, '--version'], check=True)
except FileNotFoundError: except FileNotFoundError:
print("{} is needed to run this script!".format(binary), print("{} is needed to run this script!".format(binary),
file=sys.stderr) file=sys.stderr)
@ -158,7 +159,8 @@ def main():
action='store_true') action='store_true')
args = parser.parse_args() args = parser.parse_args()
coredumps = subprocess.check_output(['coredumpctl', 'list']) coredumps = subprocess.run(['coredumpctl', 'list'], check=True,
stdout=subprocess.PIPE).stdout
lines = coredumps.decode('utf-8').split('\n') lines = coredumps.decode('utf-8').split('\n')
for line in lines[1:]: for line in lines[1:]:
if not line.strip(): if not line.strip():

View File

@ -62,7 +62,8 @@ def check_git():
print() print()
return False return False
untracked = [] untracked = []
gitst = subprocess.check_output(['git', 'status', '--porcelain']) gitst = subprocess.run(['git', 'status', '--porcelain'], check=True,
stdout=subprocess.PIPE).stdout
gitst = gitst.decode('UTF-8').strip() gitst = gitst.decode('UTF-8').strip()
for line in gitst.splitlines(): for line in gitst.splitlines():
s, name = line.split(maxsplit=1) s, name = line.split(maxsplit=1)

View File

@ -116,9 +116,11 @@ def main():
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
pip_bin = os.path.join(tmpdir, 'bin', 'pip') pip_bin = os.path.join(tmpdir, 'bin', 'pip')
subprocess.check_call(['virtualenv', tmpdir]) subprocess.run(['virtualenv', tmpdir], check=True)
subprocess.check_call([pip_bin, 'install', '-r', filename]) subprocess.run([pip_bin, 'install', '-r', filename], check=True)
reqs = subprocess.check_output([pip_bin, 'freeze']).decode('utf-8') reqs = subprocess.run([pip_bin, 'freeze'], check=True,
stdout=subprocess.PIPE
).stdout.decode('utf-8')
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
comments = read_comments(f) comments = read_comments(f)

View File

@ -76,14 +76,15 @@ def main():
pass pass
elif args.profile_tool == 'gprof2dot': elif args.profile_tool == 'gprof2dot':
# yep, shell=True. I know what I'm doing. # yep, shell=True. I know what I'm doing.
subprocess.call('gprof2dot -f pstats {} | dot -Tpng | feh -F -'.format( subprocess.run(
'gprof2dot -f pstats {} | dot -Tpng | feh -F -'.format(
shlex.quote(profilefile)), shell=True) shlex.quote(profilefile)), shell=True)
elif args.profile_tool == 'kcachegrind': elif args.profile_tool == 'kcachegrind':
callgraphfile = os.path.join(tempdir, 'callgraph') callgraphfile = os.path.join(tempdir, 'callgraph')
subprocess.call(['pyprof2calltree', '-k', '-i', profilefile, subprocess.run(['pyprof2calltree', '-k', '-i', profilefile,
'-o', callgraphfile]) '-o', callgraphfile])
elif args.profile_tool == 'snakeviz': elif args.profile_tool == 'snakeviz':
subprocess.call(['snakeviz', profilefile]) subprocess.run(['snakeviz', profilefile])
shutil.rmtree(tempdir) shutil.rmtree(tempdir)

View File

@ -73,7 +73,7 @@ def main():
env = os.environ.copy() env = os.environ.copy()
env['PYTHONPATH'] = os.pathsep.join(pythonpath) env['PYTHONPATH'] = os.pathsep.join(pythonpath)
ret = subprocess.call(['pylint'] + args, env=env) ret = subprocess.run(['pylint'] + args, env=env).returncode
return ret return ret

View File

@ -92,22 +92,22 @@ def main():
utils.print_bold("==== {} ====".format(page)) utils.print_bold("==== {} ====".format(page))
if test_harfbuzz: if test_harfbuzz:
print("With system harfbuzz:") print("With system harfbuzz:")
ret = subprocess.call([sys.executable, '-c', SCRIPT, page]) ret = subprocess.run([sys.executable, '-c', SCRIPT, page]).returncode
print_ret(ret) print_ret(ret)
retvals.append(ret) retvals.append(ret)
if test_harfbuzz: if test_harfbuzz:
print("With QT_HARFBUZZ=old:") print("With QT_HARFBUZZ=old:")
env = dict(os.environ) env = dict(os.environ)
env['QT_HARFBUZZ'] = 'old' env['QT_HARFBUZZ'] = 'old'
ret = subprocess.call([sys.executable, '-c', SCRIPT, page], ret = subprocess.run([sys.executable, '-c', SCRIPT, page],
env=env) env=env).returncode
print_ret(ret) print_ret(ret)
retvals.append(ret) retvals.append(ret)
print("With QT_HARFBUZZ=new:") print("With QT_HARFBUZZ=new:")
env = dict(os.environ) env = dict(os.environ)
env['QT_HARFBUZZ'] = 'new' env['QT_HARFBUZZ'] = 'new'
ret = subprocess.call([sys.executable, '-c', SCRIPT, page], ret = subprocess.run([sys.executable, '-c', SCRIPT, page],
env=env) env=env).returncode
print_ret(ret) print_ret(ret)
retvals.append(ret) retvals.append(ret)
if all(r == 0 for r in retvals): if all(r == 0 for r in retvals):

View File

@ -529,9 +529,9 @@ def regenerate_cheatsheet():
] ]
for filename, x, y in files: for filename, x, y in files:
subprocess.check_call(['inkscape', '-e', filename, '-b', 'white', subprocess.run(['inkscape', '-e', filename, '-b', 'white',
'-w', str(x), '-h', str(y), '-w', str(x), '-h', str(y),
'misc/cheatsheet.svg']) 'misc/cheatsheet.svg'], check=True)
def main(): def main():

View File

@ -28,50 +28,176 @@ Currently only importing bookmarks from Netscape Bookmark files is supported.
import argparse import argparse
browser_default_input_format = {
'chromium': 'netscape',
'ie': 'netscape',
'firefox': 'netscape',
'seamonkey': 'netscape',
'palemoon': 'netscape'
}
def main(): def main():
args = get_args() args = get_args()
if args.browser in ['chromium', 'firefox', 'ie']: bookmark_types = []
import_netscape_bookmarks(args.bookmarks, args.bookmark_format) output_format = None
input_format = args.input_format
if args.search_output:
bookmark_types = ['search']
if args.oldconfig:
output_format = 'oldsearch'
else:
output_format = 'search'
else:
if args.bookmark_output:
output_format = 'bookmark'
elif args.quickmark_output:
output_format = 'quickmark'
if args.import_bookmarks:
bookmark_types.append('bookmark')
if args.import_keywords:
bookmark_types.append('keyword')
if not bookmark_types:
bookmark_types = ['bookmark', 'keyword']
if not output_format:
output_format = 'quickmark'
if not input_format:
if args.browser:
input_format = browser_default_input_format[args.browser]
else:
#default to netscape
input_format = 'netscape'
import_function = {'netscape': import_netscape_bookmarks}
import_function[input_format](args.bookmarks, bookmark_types,
output_format)
def get_args(): def get_args():
"""Get the argparse parser.""" """Get the argparse parser."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
epilog="To import bookmarks from Chromium, Firefox or IE, " epilog="To import bookmarks from Chromium, Firefox or IE, "
"export them to HTML in your browsers bookmark manager. " "export them to HTML in your browsers bookmark manager. ")
"By default, this script will output in a quickmarks format.") parser.add_argument(
parser.add_argument('browser', help="Which browser? (chromium, firefox)", 'browser',
choices=['chromium', 'firefox', 'ie'], help="Which browser? {%(choices)s}",
choices=browser_default_input_format.keys(),
nargs='?',
metavar='browser') metavar='browser')
parser.add_argument('-b', help="Output in bookmark format.", parser.add_argument(
dest='bookmark_format', action='store_true', '-i',
default=False, required=False) '--input-format',
help='Which input format? (overrides browser default; "netscape" if '
'neither given)',
choices=set(browser_default_input_format.values()),
required=False)
parser.add_argument(
'-b',
'--bookmark-output',
help="Output in bookmark format.",
action='store_true',
default=False,
required=False)
parser.add_argument(
'-q',
'--quickmark-output',
help="Output in quickmark format (default).",
action='store_true',
default=False,
required=False)
parser.add_argument(
'-s',
'--search-output',
help="Output config.py search engine format (negates -B and -K)",
action='store_true',
default=False,
required=False)
parser.add_argument(
'--oldconfig',
help="Output search engine format for old qutebrowser.conf format",
default=False,
action='store_true',
required=False)
parser.add_argument(
'-B',
'--import-bookmarks',
help="Import plain bookmarks (can be combiend with -K)",
action='store_true',
default=False,
required=False)
parser.add_argument(
'-K',
'--import-keywords',
help="Import keywords (can be combined with -B)",
action='store_true',
default=False,
required=False)
parser.add_argument('bookmarks', help="Bookmarks file (html format)") parser.add_argument('bookmarks', help="Bookmarks file (html format)")
args = parser.parse_args() args = parser.parse_args()
return args return args
def import_netscape_bookmarks(bookmarks_file, is_bookmark_format): def search_escape(url):
"""Escape URLs such that preexisting { and } are handled properly.
Will obviously trash a properly-formatted Qutebrowser URL.
"""
return url.replace('{', '{{').replace('}', '}}')
def import_netscape_bookmarks(bookmarks_file, bookmark_types, output_format):
"""Import bookmarks from a NETSCAPE-Bookmark-file v1. """Import bookmarks from a NETSCAPE-Bookmark-file v1.
Generated by Chromium, Firefox, IE and possibly more browsers Generated by Chromium, Firefox, IE and possibly more browsers. Not all
export all possible bookmark types:
- Firefox mostly works with everything
- Chrome doesn't support keywords at all; searches are a separate
database
""" """
import bs4 import bs4
with open(bookmarks_file, encoding='utf-8') as f: with open(bookmarks_file, encoding='utf-8') as f:
soup = bs4.BeautifulSoup(f, 'html.parser') soup = bs4.BeautifulSoup(f, 'html.parser')
bookmark_query = {
html_tags = soup.findAll('a') 'search': lambda tag: (
if is_bookmark_format: (tag.name == 'a') and
output_template = '{tag[href]} {tag.string}' ('shortcuturl' in tag.attrs) and
else: ('%s' in tag['href'])),
output_template = '{tag.string} {tag[href]}' 'keyword': lambda tag: (
(tag.name == 'a') and
('shortcuturl' in tag.attrs) and
('%s' not in tag['href'])),
'bookmark': lambda tag: (
(tag.name == 'a') and
('shortcuturl' not in tag.attrs) and
(tag.string)),
}
output_template = {
'search': {
'search':
"c.url.searchengines['{tag[shortcuturl]}'] = "
"'{tag[href]}' #{tag.string}"
},
'oldsearch': {
'search': '{tag[shortcuturl]} = {tag[href]} #{tag.string}',
},
'bookmark': {
'bookmark': '{tag[href]} {tag.string}',
'keyword': '{tag[href]} {tag.string}'
},
'quickmark': {
'bookmark': '{tag.string} {tag[href]}',
'keyword': '{tag[shortcuturl]} {tag[href]}'
}
}
bookmarks = [] bookmarks = []
for tag in html_tags: for typ in bookmark_types:
tags = soup.findAll(bookmark_query[typ])
for tag in tags:
if typ == 'search':
tag['href'] = search_escape(tag['href']).replace('%s', '{}')
if tag['href'] not in bookmarks: if tag['href'] not in bookmarks:
bookmarks.append(output_template.format(tag=tag)) bookmarks.append(
output_template[output_format][typ].format(tag=tag))
for bookmark in bookmarks: for bookmark in bookmarks:
print(bookmark) print(bookmark)

View File

@ -46,12 +46,14 @@ def run_py(executable, *code):
f.write('\n'.join(code)) f.write('\n'.join(code))
cmd = [executable, filename] cmd = [executable, filename]
try: try:
ret = subprocess.check_output(cmd, universal_newlines=True) ret = subprocess.run(cmd, universal_newlines=True, check=True,
stdout=subprocess.PIPE).stdout
finally: finally:
os.remove(filename) os.remove(filename)
else: else:
cmd = [executable, '-c', '\n'.join(code)] cmd = [executable, '-c', '\n'.join(code)]
ret = subprocess.check_output(cmd, universal_newlines=True) ret = subprocess.run(cmd, universal_newlines=True, check=True,
stdout=subprocess.PIPE).stdout
return ret.rstrip() return ret.rstrip()

View File

@ -51,12 +51,14 @@ def _git_str():
return None return None
try: try:
# https://stackoverflow.com/questions/21017300/21017394#21017394 # https://stackoverflow.com/questions/21017300/21017394#21017394
commit_hash = subprocess.check_output( commit_hash = subprocess.run(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'], ['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=BASEDIR).decode('UTF-8').strip() cwd=BASEDIR, check=True,
date = subprocess.check_output( stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
date = subprocess.run(
['git', 'show', '-s', '--format=%ci', 'HEAD'], ['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=BASEDIR).decode('UTF-8').strip() cwd=BASEDIR, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
return '{} ({})'.format(commit_hash, date) return '{} ({})'.format(commit_hash, date)
except (subprocess.CalledProcessError, OSError): except (subprocess.CalledProcessError, OSError):
return None return None

View File

@ -2,6 +2,9 @@
Feature: Opening external editors Feature: Opening external editors
Background:
Given I have a fresh instance
## :edit-url ## :edit-url
Scenario: Editing a URL Scenario: Editing a URL
@ -20,6 +23,16 @@ Feature: Opening external editors
- data/numbers/1.txt - data/numbers/1.txt
- data/numbers/2.txt (active) - data/numbers/2.txt (active)
Scenario: Editing a URL with -rt
When I set tabs.new_position.related to prev
And I open data/numbers/1.txt
And I set up a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -rt
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
- data/numbers/2.txt (active)
- data/numbers/1.txt
Scenario: Editing a URL with -b Scenario: Editing a URL with -b
When I run :tab-only When I run :tab-only
And I open data/numbers/1.txt And I open data/numbers/1.txt
@ -49,6 +62,26 @@ Feature: Opening external editors
- active: true - active: true
url: http://localhost:*/data/numbers/2.txt url: http://localhost:*/data/numbers/2.txt
Scenario: Editing a URL with -p
When I open data/numbers/1.txt in a new tab
And I run :tab-only
And I set up a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -p
Then data/numbers/2.txt should be loaded
And the session should look like:
windows:
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/numbers/1.txt
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/numbers/2.txt
private: true
Scenario: Editing a URL with -t and -b Scenario: Editing a URL with -t and -b
When I run :edit-url -t -b When I run :edit-url -t -b
Then the error "Only one of -t/-b/-w can be given!" should be shown Then the error "Only one of -t/-b/-w can be given!" should be shown

View File

@ -713,6 +713,7 @@ Feature: Tab management
Then the following tabs should be open: Then the following tabs should be open:
- data/hello.txt (active) - data/hello.txt (active)
@flaky
Scenario: Double-undo with single tab on tabs.last_close default page Scenario: Double-undo with single tab on tabs.last_close default page
Given I have a fresh instance Given I have a fresh instance
When I open about:blank When I open about:blank

View File

@ -47,10 +47,10 @@ def update_documentation():
return return
try: try:
subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL, subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) stderr=subprocess.DEVNULL)
except OSError: except OSError:
pytest.skip("Docs outdated and asciidoc unavailable!") pytest.skip("Docs outdated and asciidoc unavailable!")
update_script = os.path.join(script_path, 'asciidoc2html.py') update_script = os.path.join(script_path, 'asciidoc2html.py')
subprocess.call([sys.executable, update_script]) subprocess.run([sys.executable, update_script])

View File

@ -259,14 +259,13 @@ def test_command_on_start(request, quteproc_new):
def test_launching_with_python2(): def test_launching_with_python2():
try: try:
proc = subprocess.Popen(['python2', '-m', 'qutebrowser', proc = subprocess.run(['python2', '-m', 'qutebrowser',
'--no-err-windows'], stderr=subprocess.PIPE) '--no-err-windows'], stderr=subprocess.PIPE)
except FileNotFoundError: except FileNotFoundError:
pytest.skip("python2 not found") pytest.skip("python2 not found")
_stdout, stderr = proc.communicate()
assert proc.returncode == 1 assert proc.returncode == 1
error = "At least Python 3.5 is required to run qutebrowser" error = "At least Python 3.5 is required to run qutebrowser"
assert stderr.decode('ascii').startswith(error) assert proc.stderr.decode('ascii').startswith(error)
def test_initial_private_browsing(request, quteproc_new): def test_initial_private_browsing(request, quteproc_new):

View File

@ -53,7 +53,6 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None,
evaluateJavaScript. evaluateJavaScript.
zoom_text_only: Whether zoom.text_only is set in the config zoom_text_only: Whether zoom.text_only is set in the config
""" """
# pylint: disable=too-many-locals,too-many-branches
elem = mock.Mock() elem = mock.Mock()
elem.isNull.return_value = null elem.isNull.return_value = null
elem.geometry.return_value = geometry elem.geometry.return_value = geometry

View File

@ -20,15 +20,15 @@
"""Tests for qutebrowser.commands.argparser.""" """Tests for qutebrowser.commands.argparser."""
import inspect import inspect
import enum
import pytest import pytest
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from qutebrowser.commands import argparser, cmdexc from qutebrowser.commands import argparser, cmdexc
from qutebrowser.utils import usertypes
Enum = usertypes.enum('Enum', ['foo', 'foo_bar']) Enum = enum.Enum('Enum', ['foo', 'foo_bar'])
class TestArgumentParser: class TestArgumentParser:

View File

@ -25,6 +25,7 @@ import sys
import logging import logging
import types import types
import typing import typing
import enum
import pytest import pytest
@ -243,7 +244,7 @@ class TestRegister:
else: else:
assert pos_args == [('arg', 'arg')] assert pos_args == [('arg', 'arg')]
Enum = usertypes.enum('Test', ['x', 'y']) Enum = enum.Enum('Test', ['x', 'y'])
@pytest.mark.parametrize('typ, inp, choices, expected', [ @pytest.mark.parametrize('typ, inp, choices, expected', [
(int, '42', None, 42), (int, '42', None, 42),

View File

@ -274,7 +274,11 @@ def test_on_selection_changed(before, newtxt, after, completer_obj,
check(True, 2, after_txt, after_pos) check(True, 2, after_txt, after_pos)
# quick-completing a single item should move the cursor ahead by 1 and add # quick-completing a single item should move the cursor ahead by 1 and add
# a trailing space if at the end of the cmd string # a trailing space if at the end of the cmd string, unless the command has
# maxsplit < len(before) (such as :open in these tests)
if after_txt.startswith(':open'):
return
after_pos += 1 after_pos += 1
if after_pos > len(after_txt): if after_pos > len(after_txt):
after_txt += ' ' after_txt += ' '
@ -299,6 +303,11 @@ def test_quickcomplete_flicker(status_command_stub, completer_obj,
config_stub.val.completion.quick = True config_stub.val.completion.quick = True
_set_cmd_prompt(status_command_stub, ':open |') _set_cmd_prompt(status_command_stub, ':open |')
completer_obj.schedule_completion_update()
assert completion_widget_stub.set_model.called
completion_widget_stub.set_model.reset_mock()
# selecting a completion should not re-set the model
completer_obj.on_selection_changed('http://example.com') completer_obj.on_selection_changed('http://example.com')
completer_obj.schedule_completion_update() completer_obj.schedule_completion_update()
assert not completion_widget_stub.set_model.called assert not completion_widget_stub.set_model.called

View File

@ -60,8 +60,9 @@ class TestSet:
@pytest.mark.parametrize('option, old_value, inp, new_value', [ @pytest.mark.parametrize('option, old_value, inp, new_value', [
('url.auto_search', 'naive', 'dns', 'dns'), ('url.auto_search', 'naive', 'dns', 'dns'),
# https://github.com/qutebrowser/qutebrowser/issues/2962 # https://github.com/qutebrowser/qutebrowser/issues/2962
('editor.command', ['gvim', '-f', '{}'], '[emacs, "{}"]', ('editor.command',
['emacs', '{}']), ['gvim', '-f', '{file}', '-c', 'normal {line}G{column0}l'],
'[emacs, "{}"]', ['emacs', '{}']),
]) ])
def test_set_simple(self, monkeypatch, commands, config_stub, def test_set_simple(self, monkeypatch, commands, config_stub,
temp, option, old_value, inp, new_value): temp, option, old_value, inp, new_value):

View File

@ -111,7 +111,6 @@ class TestEarlyInit:
def test_autoconfig_yml(self, init_patch, config_tmpdir, caplog, args, def test_autoconfig_yml(self, init_patch, config_tmpdir, caplog, args,
load_autoconfig, config_py, invalid_yaml): load_autoconfig, config_py, invalid_yaml):
"""Test interaction between config.py and autoconfig.yml.""" """Test interaction between config.py and autoconfig.yml."""
# pylint: disable=too-many-locals,too-many-branches
# Prepare files # Prepare files
autoconfig_file = config_tmpdir / 'autoconfig.yml' autoconfig_file = config_tmpdir / 'autoconfig.yml'
config_py_file = config_tmpdir / 'config.py' config_py_file = config_tmpdir / 'config.py'

View File

@ -1815,9 +1815,12 @@ class TestShellCommand:
@pytest.mark.parametrize('kwargs, val, expected', [ @pytest.mark.parametrize('kwargs, val, expected', [
({}, '[foobar]', ['foobar']), ({}, '[foobar]', ['foobar']),
({'placeholder': '{}'}, '[foo, "{}", bar]', ['foo', '{}', 'bar']), ({'placeholder': True}, '[foo, "{}", bar]', ['foo', '{}', 'bar']),
({'placeholder': '{}'}, '["foo{}bar"]', ['foo{}bar']), ({'placeholder': True}, '["foo{}bar"]', ['foo{}bar']),
({'placeholder': '{}'}, '[foo, "bar {}"]', ['foo', 'bar {}']), ({'placeholder': True}, '[foo, "bar {}"]', ['foo', 'bar {}']),
({'placeholder': True}, '[f, "{file}", b]', ['f', '{file}', 'b']),
({'placeholder': True}, '["f{file}b"]', ['f{file}b']),
({'placeholder': True}, '[f, "b {file}"]', ['f', 'b {file}']),
]) ])
def test_valid(self, klass, kwargs, val, expected): def test_valid(self, klass, kwargs, val, expected):
cmd = klass(**kwargs) cmd = klass(**kwargs)
@ -1825,8 +1828,15 @@ class TestShellCommand:
assert cmd.to_py(expected) == expected assert cmd.to_py(expected) == expected
@pytest.mark.parametrize('kwargs, val', [ @pytest.mark.parametrize('kwargs, val', [
({'placeholder': '{}'}, '[foo, bar]'), ({'placeholder': True}, '[foo, bar]'),
({'placeholder': '{}'}, '[foo, "{", "}", bar'), ({'placeholder': True}, '[foo, "{", "}", bar'),
({'placeholder': True}, '[foo, bar]'),
({'placeholder': True}, '[foo, "{fi", "le}", bar'),
# Like valid ones but with wrong placeholder
({'placeholder': True}, '[f, "{wrong}", b]'),
({'placeholder': True}, '["f{wrong}b"]'),
({'placeholder': True}, '[f, "b {wrong}"]'),
]) ])
def test_from_str_invalid(self, klass, kwargs, val): def test_from_str_invalid(self, klass, kwargs, val):
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):

View File

@ -52,3 +52,16 @@ class TestTabWidget:
with qtbot.waitExposed(widget): with qtbot.waitExposed(widget):
widget.show() widget.show()
def test_update_tab_titles_benchmark(self, benchmark, widget,
qtbot, fake_web_tab):
"""Benchmark for update_tab_titles."""
widget.addTab(fake_web_tab(), 'foobar')
widget.addTab(fake_web_tab(), 'foobar2')
widget.addTab(fake_web_tab(), 'foobar3')
widget.addTab(fake_web_tab(), 'foobar4')
with qtbot.waitExposed(widget):
widget.show()
benchmark(widget._update_tab_titles)

View File

@ -36,15 +36,14 @@ TEXT = (r"At least Python 3.5 is required to run qutebrowser, but it's "
def test_python2(): def test_python2():
"""Run checkpyver with python 2.""" """Run checkpyver with python 2."""
try: try:
proc = subprocess.Popen( proc = subprocess.run(
['python2', checkpyver.__file__, '--no-err-windows'], ['python2', checkpyver.__file__, '--no-err-windows'],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
except FileNotFoundError: except FileNotFoundError:
pytest.skip("python2 not found") pytest.skip("python2 not found")
assert not stdout assert not proc.stdout
stderr = stderr.decode('utf-8') stderr = proc.stderr.decode('utf-8')
assert re.match(TEXT, stderr), stderr assert re.match(TEXT, stderr), stderr
assert proc.returncode == 1 assert proc.returncode == 1

View File

@ -177,3 +177,18 @@ def test_modify(qtbot, editor, initial_text, edited_text):
editor._proc.finished.emit(0, QProcess.NormalExit) editor._proc.finished.emit(0, QProcess.NormalExit)
assert blocker.args == [edited_text] assert blocker.args == [edited_text]
@pytest.mark.parametrize('text, caret_position, result', [
('', 0, (1, 1)),
('a', 0, (1, 1)),
('a\nb', 1, (1, 2)),
('a\nb', 2, (2, 1)),
('a\nb', 3, (2, 2)),
('a\nbb\nccc', 4, (2, 3)),
('a\nbb\nccc', 5, (3, 1)),
('a\nbb\nccc', 8, (3, 4)),
])
def test_calculation(editor, text, caret_position, result):
"""Test calculation for line and column given text and caret_position."""
assert editor._calc_line_and_column(text, caret_position) == result

View File

@ -555,8 +555,9 @@ def test_no_qapplication(qapp, tmpdir):
pyfile = tmpdir / 'sub.py' pyfile = tmpdir / 'sub.py'
pyfile.write_text(textwrap.dedent(sub_code), encoding='ascii') pyfile.write_text(textwrap.dedent(sub_code), encoding='ascii')
output = subprocess.check_output([sys.executable, str(pyfile)] + sys.path, output = subprocess.run([sys.executable, str(pyfile)] + sys.path,
universal_newlines=True) universal_newlines=True,
check=True, stdout=subprocess.PIPE).stdout
sub_locations = json.loads(output) sub_locations = json.loads(output)
standarddir._init_dirs() standarddir._init_dirs()

View File

@ -299,8 +299,8 @@ class TestGitStr:
def _has_git(): def _has_git():
"""Check if git is installed.""" """Check if git is installed."""
try: try:
subprocess.check_call(['git', '--version'], stdout=subprocess.DEVNULL, subprocess.run(['git', '--version'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) stderr=subprocess.DEVNULL, check=True)
except (OSError, subprocess.CalledProcessError): except (OSError, subprocess.CalledProcessError):
return False return False
else: else:
@ -337,12 +337,13 @@ class TestGitStrSubprocess:
# If we don't call this with shell=True it might fail under # If we don't call this with shell=True it might fail under
# some environments on Windows... # some environments on Windows...
# http://bugs.python.org/issue24493 # http://bugs.python.org/issue24493
subprocess.check_call( subprocess.run(
'git -C "{}" {}'.format(tmpdir, ' '.join(args)), 'git -C "{}" {}'.format(tmpdir, ' '.join(args)),
env=env, shell=True) env=env, check=True, shell=True)
else: else:
subprocess.check_call( subprocess.run(
['git', '-C', str(tmpdir)] + list(args), env=env) ['git', '-C', str(tmpdir)] + list(args),
check=True, env=env)
(tmpdir / 'file').write_text("Hello World!", encoding='utf-8') (tmpdir / 'file').write_text("Hello World!", encoding='utf-8')
_git('init') _git('init')
@ -368,14 +369,14 @@ class TestGitStrSubprocess:
subprocess.CalledProcessError(1, 'foobar') subprocess.CalledProcessError(1, 'foobar')
]) ])
def test_exception(self, exc, mocker, tmpdir): def test_exception(self, exc, mocker, tmpdir):
"""Test with subprocess.check_output raising an exception. """Test with subprocess.run raising an exception.
Args: Args:
exc: The exception to raise. exc: The exception to raise.
""" """
m = mocker.patch('qutebrowser.utils.version.os') m = mocker.patch('qutebrowser.utils.version.os')
m.path.isdir.return_value = True m.path.isdir.return_value = True
mocker.patch('qutebrowser.utils.version.subprocess.check_output', mocker.patch('qutebrowser.utils.version.subprocess.run',
side_effect=exc) side_effect=exc)
ret = version._git_str_subprocess(str(tmpdir)) ret = version._git_str_subprocess(str(tmpdir))
assert ret is None assert ret is None

View File

@ -1,74 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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/>.
"""Tests for the Enum class."""
from qutebrowser.utils import usertypes
import pytest
@pytest.fixture
def enum():
return usertypes.enum('Enum', ['one', 'two'])
def test_values(enum):
"""Test if enum members resolve to the right values."""
assert enum.one.value == 1
assert enum.two.value == 2
def test_name(enum):
"""Test .name mapping."""
assert enum.one.name == 'one'
assert enum.two.name == 'two'
def test_unknown(enum):
"""Test invalid values which should raise an AttributeError."""
with pytest.raises(AttributeError):
_ = enum.three # flake8: disable=F841
def test_start():
"""Test the start= argument."""
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
def test_is_int():
"""Test the is_int argument."""
int_enum = usertypes.enum('Enum', ['item'], is_int=True)
no_int_enum = usertypes.enum('Enum', ['item'])
assert isinstance(int_enum.item, int)
assert not isinstance(no_int_enum.item, int)
def test_unique():
"""Make sure elements need to be unique."""
with pytest.raises(TypeError):
usertypes.enum('Enum', ['item', 'item'])