diff --git a/.travis.yml b/.travis.yml index 18d1dd416..65d917d73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,7 +51,7 @@ matrix: env: TESTENV=eslint language: node_js python: null - node_js: node + node_js: "lts/*" fast_finish: true cache: diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index f4e31d5e8..36079535c 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -34,6 +34,8 @@ Added 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 ~~~~~~~ @@ -76,6 +78,8 @@ Fixed ~~~~~ - 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 ------ diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index 75fe5ee8c..2091b9e0b 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -152,7 +152,7 @@ For QtWebEngine: `:set spellcheck.languages "['en-US', 'pl-PL']"` 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 protection that the Tor Browser does, but it's useful to be able to access `.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. 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 - 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?:: They are quite similar, but insert mode has some bindings (like `Ctrl-e` to diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index b72bb77ac..69a25e177 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1961,7 +1961,14 @@ Default: +pass:[-1]+ [[editor.command]] === 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: <> diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 2ed579f61..02eb9bf18 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -660,9 +660,9 @@ class Quitter: try: args, cwd = self._get_restart_args(pages, session, override_args) if cwd is None: - subprocess.Popen(args) + subprocess.run(args) else: - subprocess.Popen(args, cwd=cwd) + subprocess.run(args, cwd=cwd) except OSError: log.destroy.exception("Failed to restart") return False diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 93956157b..17179b6ff 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1618,10 +1618,12 @@ class CommandDispatcher: return assert isinstance(text, str), text + caret_position = elem.caret_position() + ed = editor.ExternalEditor(self._tabbed_browser) ed.editing_finished.connect(functools.partial( self.on_editing_finished, elem)) - ed.edit(text) + ed.edit(text, caret_position) @cmdutils.register(instance='command-dispatcher', hide=True, scope='window') diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index c294bebef..fb528a93d 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -47,6 +47,7 @@ class WebEngineElement(webelem.AbstractWebElement): 'class_name': str, 'rects': list, 'attributes': dict, + 'caret_position': int, } assert set(js_dict.keys()).issubset(js_dict_types.keys()) for name, typ in js_dict_types.items(): @@ -132,6 +133,10 @@ class WebEngineElement(webelem.AbstractWebElement): def set_value(self, 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): if not self.is_editable(strict=True): raise webelem.Error("Element is not editable!") diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 54dd6519d..2a1eafc9e 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -126,6 +126,14 @@ class WebKitElement(webelem.AbstractWebElement): value = javascript.string_escape(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): self._check_vanished() if not self.is_editable(strict=True): diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 39c8ec226..17e1c75b0 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -766,11 +766,11 @@ editor.command: type: name: ShellCommand placeholder: true - default: ["gvim", "-f", "{}"] + default: ["gvim", "-f", "{file}", "-c", "normal {line}G{column0}l"] desc: >- The editor (and arguments) to use for the `open-editor` command. - `{}` gets replaced by the filename of the file to be edited. + `{file}` gets replaced by the filename of the file to be edited. editor.encoding: type: Encoding diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py index 3f07daba1..020be2c8c 100644 --- a/qutebrowser/config/configdiff.py +++ b/qutebrowser/config/configdiff.py @@ -755,6 +755,6 @@ def get_diff(): lexer = pygments.lexers.DiffLexer() formatter = pygments.formatters.HtmlFormatter( full=True, linenos='table', - title='Config diff') + title='Diffing pre-1.0 default config with pre-1.0 modified config') # pylint: enable=no-member return pygments.highlight(conf_diff + key_diff, lexer, formatter) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index da77401ac..203c4e573 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1341,9 +1341,12 @@ class ShellCommand(List): if not 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 " - "{}-placeholder.") + "{}-placeholder or a " + "{file}-placeholder.") return value diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 6820a17b3..f0ec63bc0 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -48,11 +48,26 @@ window._qutebrowser.webelem = (function() { const id = elements.length; elements[id] = elem; + // 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, "value": elem.value, "outer_xml": elem.outerHTML, "rects": [], // Gets filled up later + "caret_position": caret_position, }; // https://github.com/qutebrowser/qutebrowser/issues/2569 diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 48803f1c8..5c3b10148 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -259,13 +259,14 @@ class TabbedBrowser(tabwidget.TabWidget): def tab_close_prompt_if_pinned(self, tab, force, yes_action): """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: message.confirm_async( title='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: yes_action() diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 3686e028f..2be96e012 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -96,11 +96,12 @@ class ExternalEditor(QObject): def on_proc_error(self, _err): self._cleanup() - def edit(self, text): + def edit(self, text, caret_position=0): """Edit a given text. Args: text: The initial text to edit. + caret_position: The position of the caret in the text. """ if self._filename is not None: raise ValueError("Already editing a file!") @@ -121,7 +122,9 @@ class ExternalEditor(QObject): return 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): """Edit the file with the given filename.""" @@ -129,13 +132,82 @@ class ExternalEditor(QObject): self._remove_file = False self._start_editor() - def _start_editor(self): - """Start the editor with the file opened as self._filename.""" + def _start_editor(self, line=1, column=1): + """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.finished.connect(self.on_proc_closed) self._proc.error.connect(self.on_proc_error) editor = config.val.editor.command 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)) 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 diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index bc04c7524..547185623 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -151,12 +151,14 @@ def _git_str_subprocess(gitpath): return None try: # https://stackoverflow.com/questions/21017300/21017394#21017394 - commit_hash = subprocess.check_output( + commit_hash = subprocess.run( ['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'], - cwd=gitpath).decode('UTF-8').strip() - date = subprocess.check_output( + cwd=gitpath, check=True, + stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() + date = subprocess.run( ['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) except (subprocess.CalledProcessError, OSError): return None diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index 51deb009f..4e9c7544b 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -224,16 +224,16 @@ class AsciiDoc: return self._args.asciidoc try: - subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) except OSError: pass else: return ['asciidoc'] try: - subprocess.call(['asciidoc.py'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.run(['asciidoc.py'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) except OSError: pass else: @@ -258,7 +258,7 @@ class AsciiDoc: try: env = os.environ.copy() env['HOME'] = self._homedir - subprocess.check_call(cmdline, env=env) + subprocess.run(cmdline, check=True, env=env) except (subprocess.CalledProcessError, OSError) as e: self._failed = True utils.print_col(str(e), 'red') diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 0dc45f2e2..e12ff5c93 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -50,7 +50,7 @@ def call_script(name, *args, python=sys.executable): python: The python interpreter to use. """ 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): @@ -64,9 +64,9 @@ def call_tox(toxenv, *args, python=sys.executable): env = os.environ.copy() env['PYTHON'] = 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), - env=env) + env=env, check=True) def run_asciidoc2html(args): @@ -89,8 +89,9 @@ def _maybe_remove(path): def smoke_test(executable): """Try starting the given qutebrowser executable.""" - subprocess.check_call([executable, '--no-err-windows', '--nowindow', - '--temp-basedir', 'about:blank', ':later 500 quit']) + subprocess.run([executable, '--no-err-windows', '--nowindow', + '--temp-basedir', 'about:blank', ':later 500 quit'], + check=True) def patch_mac_app(): @@ -178,7 +179,7 @@ def build_mac(): utils.print_title("Patching .app") patch_mac_app() 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__) os.rename('qutebrowser.dmg', dmg_name) @@ -187,14 +188,14 @@ def build_mac(): try: with tempfile.TemporaryDirectory() as tmpdir: - subprocess.check_call(['hdiutil', 'attach', dmg_name, - '-mountpoint', tmpdir]) + subprocess.run(['hdiutil', 'attach', dmg_name, + '-mountpoint', tmpdir], check=True) try: binary = os.path.join(tmpdir, 'qutebrowser.app', 'Contents', 'MacOS', 'qutebrowser') smoke_test(binary) finally: - subprocess.call(['hdiutil', 'detach', tmpdir]) + subprocess.run(['hdiutil', 'detach', tmpdir]) except PermissionError as e: print("Failed to remove tempdir: {}".format(e)) @@ -242,13 +243,13 @@ def build_windows(): patch_windows(out_64) utils.print_title("Building installers") - subprocess.check_call(['makensis.exe', - '/DVERSION={}'.format(qutebrowser.__version__), - 'misc/qutebrowser.nsi']) - subprocess.check_call(['makensis.exe', - '/DX64', - '/DVERSION={}'.format(qutebrowser.__version__), - 'misc/qutebrowser.nsi']) + subprocess.run(['makensis.exe', + '/DVERSION={}'.format(qutebrowser.__version__), + 'misc/qutebrowser.nsi'], check=True) + subprocess.run(['makensis.exe', + '/DX64', + '/DVERSION={}'.format(qutebrowser.__version__), + 'misc/qutebrowser.nsi'], check=True) name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__) name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__) @@ -292,12 +293,12 @@ def build_sdist(): _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')) assert len(dist_files) == 1 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) by_ext = collections.defaultdict(list) @@ -366,7 +367,7 @@ def github_upload(artifacts, tag): def pypi_upload(artifacts): """Upload the given artifacts to PyPI using twine.""" filenames = [a[0] for a in artifacts] - subprocess.check_call(['twine', 'upload'] + filenames) + subprocess.run(['twine', 'upload'] + filenames, check=True) def main(): diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index ba4e7a7a7..f2c9dd853 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -285,8 +285,8 @@ def main_check(): print(msg.text) print() filters = ','.join('qutebrowser/' + msg.filename for msg in messages) - subprocess.check_call([sys.executable, '-m', 'coverage', 'report', - '--show-missing', '--include', filters]) + subprocess.run([sys.executable, '-m', 'coverage', 'report', + '--show-missing', '--include', filters], check=True) print() print("To debug this, run 'tox -e py36-pyqt59-cov' " "(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: if test_file is None: continue - subprocess.check_call( + subprocess.run( [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: messages = check(f, [(test_file, src_file)]) os.remove('coverage.xml') diff --git a/scripts/dev/check_doc_changes.py b/scripts/dev/check_doc_changes.py index 7fc993283..1cc2a90f9 100755 --- a/scripts/dev/check_doc_changes.py +++ b/scripts/dev/check_doc_changes.py @@ -24,7 +24,8 @@ import sys import subprocess 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 code != 0: @@ -42,6 +43,6 @@ if code != 0: if 'TRAVIS' in os.environ: print() print("travis_fold:start:gitdiff") - subprocess.call(['git', '--no-pager', 'diff']) + subprocess.run(['git', '--no-pager', 'diff']) print("travis_fold:end:gitdiff") sys.exit(code) diff --git a/scripts/dev/gen_resources.py b/scripts/dev/gen_resources.py index 12bf5b97c..cbfc69b6f 100644 --- a/scripts/dev/gen_resources.py +++ b/scripts/dev/gen_resources.py @@ -23,4 +23,4 @@ import subprocess 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) diff --git a/scripts/dev/get_coredumpctl_traces.py b/scripts/dev/get_coredumpctl_traces.py index 6d1a41a11..711bc52c0 100644 --- a/scripts/dev/get_coredumpctl_traces.py +++ b/scripts/dev/get_coredumpctl_traces.py @@ -84,7 +84,8 @@ def parse_coredumpctl_line(line): def get_info(pid): """Get and parse "coredumpctl info" output for the given PID.""" 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') for line in output.split('\n'): if not line.strip(): @@ -117,12 +118,12 @@ def dump_infos_gdb(parsed): """Dump all needed infos for the given crash using gdb.""" with tempfile.TemporaryDirectory() as tempdir: coredump = os.path.join(tempdir, 'dump') - subprocess.check_call(['coredumpctl', 'dump', '-o', coredump, - str(parsed.pid)]) - subprocess.check_call(['gdb', parsed.exe, coredump, - '-ex', 'info threads', - '-ex', 'thread apply all bt full', - '-ex', 'quit']) + subprocess.run(['coredumpctl', 'dump', '-o', coredump, + str(parsed.pid)], check=True) + subprocess.run(['gdb', parsed.exe, coredump, + '-ex', 'info threads', + '-ex', 'thread apply all bt full', + '-ex', 'quit'], check=True) def dump_infos(parsed): @@ -143,7 +144,7 @@ def check_prerequisites(): """Check if coredumpctl/gdb are installed.""" for binary in ['coredumpctl', 'gdb']: try: - subprocess.check_call([binary, '--version']) + subprocess.run([binary, '--version'], check=True) except FileNotFoundError: print("{} is needed to run this script!".format(binary), file=sys.stderr) @@ -158,7 +159,8 @@ def main(): action='store_true') 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') for line in lines[1:]: if not line.strip(): diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 1bb263d15..e1517391d 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -62,7 +62,8 @@ def check_git(): print() return False 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() for line in gitst.splitlines(): s, name = line.split(maxsplit=1) diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index ddc85dfec..4718ca7ab 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -116,9 +116,11 @@ def main(): with tempfile.TemporaryDirectory() as tmpdir: pip_bin = os.path.join(tmpdir, 'bin', 'pip') - subprocess.check_call(['virtualenv', tmpdir]) - subprocess.check_call([pip_bin, 'install', '-r', filename]) - reqs = subprocess.check_output([pip_bin, 'freeze']).decode('utf-8') + subprocess.run(['virtualenv', tmpdir], check=True) + subprocess.run([pip_bin, 'install', '-r', filename], check=True) + reqs = subprocess.run([pip_bin, 'freeze'], check=True, + stdout=subprocess.PIPE + ).stdout.decode('utf-8') with open(filename, 'r', encoding='utf-8') as f: comments = read_comments(f) diff --git a/scripts/dev/run_profile.py b/scripts/dev/run_profile.py index 31fe539aa..2247bd842 100755 --- a/scripts/dev/run_profile.py +++ b/scripts/dev/run_profile.py @@ -76,14 +76,15 @@ def main(): pass elif args.profile_tool == 'gprof2dot': # yep, shell=True. I know what I'm doing. - subprocess.call('gprof2dot -f pstats {} | dot -Tpng | feh -F -'.format( - shlex.quote(profilefile)), shell=True) + subprocess.run( + 'gprof2dot -f pstats {} | dot -Tpng | feh -F -'.format( + shlex.quote(profilefile)), shell=True) elif args.profile_tool == 'kcachegrind': callgraphfile = os.path.join(tempdir, 'callgraph') - subprocess.call(['pyprof2calltree', '-k', '-i', profilefile, - '-o', callgraphfile]) + subprocess.run(['pyprof2calltree', '-k', '-i', profilefile, + '-o', callgraphfile]) elif args.profile_tool == 'snakeviz': - subprocess.call(['snakeviz', profilefile]) + subprocess.run(['snakeviz', profilefile]) shutil.rmtree(tempdir) diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py index e6263692b..1f99aa362 100644 --- a/scripts/dev/run_pylint_on_tests.py +++ b/scripts/dev/run_pylint_on_tests.py @@ -73,7 +73,7 @@ def main(): env = os.environ.copy() env['PYTHONPATH'] = os.pathsep.join(pythonpath) - ret = subprocess.call(['pylint'] + args, env=env) + ret = subprocess.run(['pylint'] + args, env=env).returncode return ret diff --git a/scripts/dev/segfault_test.py b/scripts/dev/segfault_test.py index c5e7c106f..642bb837d 100755 --- a/scripts/dev/segfault_test.py +++ b/scripts/dev/segfault_test.py @@ -92,22 +92,22 @@ def main(): utils.print_bold("==== {} ====".format(page)) if test_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) retvals.append(ret) if test_harfbuzz: print("With QT_HARFBUZZ=old:") env = dict(os.environ) env['QT_HARFBUZZ'] = 'old' - ret = subprocess.call([sys.executable, '-c', SCRIPT, page], - env=env) + ret = subprocess.run([sys.executable, '-c', SCRIPT, page], + env=env).returncode print_ret(ret) retvals.append(ret) print("With QT_HARFBUZZ=new:") env = dict(os.environ) env['QT_HARFBUZZ'] = 'new' - ret = subprocess.call([sys.executable, '-c', SCRIPT, page], - env=env) + ret = subprocess.run([sys.executable, '-c', SCRIPT, page], + env=env).returncode print_ret(ret) retvals.append(ret) if all(r == 0 for r in retvals): diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index a112b60d4..a58791297 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -529,9 +529,9 @@ def regenerate_cheatsheet(): ] for filename, x, y in files: - subprocess.check_call(['inkscape', '-e', filename, '-b', 'white', - '-w', str(x), '-h', str(y), - 'misc/cheatsheet.svg']) + subprocess.run(['inkscape', '-e', filename, '-b', 'white', + '-w', str(x), '-h', str(y), + 'misc/cheatsheet.svg'], check=True) def main(): diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index a7de598cd..57eeb9138 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -46,12 +46,14 @@ def run_py(executable, *code): f.write('\n'.join(code)) cmd = [executable, filename] try: - ret = subprocess.check_output(cmd, universal_newlines=True) + ret = subprocess.run(cmd, universal_newlines=True, check=True, + stdout=subprocess.PIPE).stdout finally: os.remove(filename) else: 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() diff --git a/scripts/setupcommon.py b/scripts/setupcommon.py index 1f85b225f..1832ead86 100644 --- a/scripts/setupcommon.py +++ b/scripts/setupcommon.py @@ -51,12 +51,14 @@ def _git_str(): return None try: # https://stackoverflow.com/questions/21017300/21017394#21017394 - commit_hash = subprocess.check_output( + commit_hash = subprocess.run( ['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'], - cwd=BASEDIR).decode('UTF-8').strip() - date = subprocess.check_output( + cwd=BASEDIR, check=True, + stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() + date = subprocess.run( ['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) except (subprocess.CalledProcessError, OSError): return None diff --git a/tests/end2end/features/test_qutescheme_bdd.py b/tests/end2end/features/test_qutescheme_bdd.py index 14bdc5cb3..1269d10ed 100644 --- a/tests/end2end/features/test_qutescheme_bdd.py +++ b/tests/end2end/features/test_qutescheme_bdd.py @@ -47,10 +47,10 @@ def update_documentation(): return try: - subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.run(['asciidoc'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) except OSError: pytest.skip("Docs outdated and asciidoc unavailable!") update_script = os.path.join(script_path, 'asciidoc2html.py') - subprocess.call([sys.executable, update_script]) + subprocess.run([sys.executable, update_script]) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 1a91be2e0..ef97d5317 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -259,14 +259,13 @@ def test_command_on_start(request, quteproc_new): def test_launching_with_python2(): try: - proc = subprocess.Popen(['python2', '-m', 'qutebrowser', - '--no-err-windows'], stderr=subprocess.PIPE) + proc = subprocess.run(['python2', '-m', 'qutebrowser', + '--no-err-windows'], stderr=subprocess.PIPE) except FileNotFoundError: pytest.skip("python2 not found") - _stdout, stderr = proc.communicate() assert proc.returncode == 1 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): diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index c33bb54d4..0fd1b6680 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -60,8 +60,9 @@ class TestSet: @pytest.mark.parametrize('option, old_value, inp, new_value', [ ('url.auto_search', 'naive', 'dns', 'dns'), # https://github.com/qutebrowser/qutebrowser/issues/2962 - ('editor.command', ['gvim', '-f', '{}'], '[emacs, "{}"]', - ['emacs', '{}']), + ('editor.command', + ['gvim', '-f', '{file}', '-c', 'normal {line}G{column0}l'], + '[emacs, "{}"]', ['emacs', '{}']), ]) def test_set_simple(self, monkeypatch, commands, config_stub, temp, option, old_value, inp, new_value): diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 9b5e23263..36988a0a7 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1815,9 +1815,12 @@ class TestShellCommand: @pytest.mark.parametrize('kwargs, val, expected', [ ({}, '[foobar]', ['foobar']), - ({'placeholder': '{}'}, '[foo, "{}", bar]', ['foo', '{}', 'bar']), - ({'placeholder': '{}'}, '["foo{}bar"]', ['foo{}bar']), - ({'placeholder': '{}'}, '[foo, "bar {}"]', ['foo', 'bar {}']), + ({'placeholder': True}, '[foo, "{}", bar]', ['foo', '{}', 'bar']), + ({'placeholder': True}, '["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): cmd = klass(**kwargs) @@ -1825,8 +1828,15 @@ class TestShellCommand: assert cmd.to_py(expected) == expected @pytest.mark.parametrize('kwargs, val', [ - ({'placeholder': '{}'}, '[foo, bar]'), - ({'placeholder': '{}'}, '[foo, "{", "}", bar'), + ({'placeholder': True}, '[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): with pytest.raises(configexc.ValidationError): diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index c5fa83a48..6487eb263 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -36,15 +36,14 @@ TEXT = (r"At least Python 3.5 is required to run qutebrowser, but it's " def test_python2(): """Run checkpyver with python 2.""" try: - proc = subprocess.Popen( + proc = subprocess.run( ['python2', checkpyver.__file__, '--no-err-windows'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = proc.communicate() except FileNotFoundError: pytest.skip("python2 not found") - assert not stdout - stderr = stderr.decode('utf-8') + assert not proc.stdout + stderr = proc.stderr.decode('utf-8') assert re.match(TEXT, stderr), stderr assert proc.returncode == 1 diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py index d97a39f7b..683b38841 100644 --- a/tests/unit/misc/test_editor.py +++ b/tests/unit/misc/test_editor.py @@ -177,3 +177,18 @@ def test_modify(qtbot, editor, initial_text, edited_text): editor._proc.finished.emit(0, QProcess.NormalExit) 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 diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index c0f63d410..5a3f74a66 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -555,8 +555,9 @@ def test_no_qapplication(qapp, tmpdir): pyfile = tmpdir / 'sub.py' pyfile.write_text(textwrap.dedent(sub_code), encoding='ascii') - output = subprocess.check_output([sys.executable, str(pyfile)] + sys.path, - universal_newlines=True) + output = subprocess.run([sys.executable, str(pyfile)] + sys.path, + universal_newlines=True, + check=True, stdout=subprocess.PIPE).stdout sub_locations = json.loads(output) standarddir._init_dirs() diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 9db7603e3..9e5f455ef 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -299,8 +299,8 @@ class TestGitStr: def _has_git(): """Check if git is installed.""" try: - subprocess.check_call(['git', '--version'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.run(['git', '--version'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=True) except (OSError, subprocess.CalledProcessError): return False else: @@ -337,12 +337,13 @@ class TestGitStrSubprocess: # If we don't call this with shell=True it might fail under # some environments on Windows... # http://bugs.python.org/issue24493 - subprocess.check_call( + subprocess.run( 'git -C "{}" {}'.format(tmpdir, ' '.join(args)), - env=env, shell=True) + env=env, check=True, shell=True) else: - subprocess.check_call( - ['git', '-C', str(tmpdir)] + list(args), env=env) + subprocess.run( + ['git', '-C', str(tmpdir)] + list(args), + check=True, env=env) (tmpdir / 'file').write_text("Hello World!", encoding='utf-8') _git('init') @@ -368,14 +369,14 @@ class TestGitStrSubprocess: subprocess.CalledProcessError(1, 'foobar') ]) def test_exception(self, exc, mocker, tmpdir): - """Test with subprocess.check_output raising an exception. + """Test with subprocess.run raising an exception. Args: exc: The exception to raise. """ m = mocker.patch('qutebrowser.utils.version.os') m.path.isdir.return_value = True - mocker.patch('qutebrowser.utils.version.subprocess.check_output', + mocker.patch('qutebrowser.utils.version.subprocess.run', side_effect=exc) ret = version._git_str_subprocess(str(tmpdir)) assert ret is None