def edit(self, text): """Edit a given text. Args: text: The initial text to edit. """ if self._text is not None: raise ValueError("Already editing a file!") self._text = text try: self._oshandle, self._filename = tempfile.mkstemp( text=True, prefix='qutebrowser-editor-') if text: encoding = config.get('general', 'editor-encoding') with open(self._filename, 'w', encoding=encoding) as f: f.write(text) # pragma: no branch except OSError as e: message.error(self._win_id, "Failed to create initial file: " "{}".format(e)) return self._proc = guiprocess.GUIProcess(self._win_id, what='editor', parent=self) self._proc.finished.connect(self.on_proc_closed) self._proc.error.connect(self.on_proc_error) editor = config.get('general', 'editor') executable = editor[0] args = [self._filename if arg == '{}' else arg for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) self._proc.start(executable, args)
def edit(self, text): """Edit a given text. Args: text: The initial text to edit. """ if self._text is not None: raise ValueError("Already editing a file!") self._text = text try: # Close while the external process is running, as otherwise systems # with exclusive write access (e.g. Windows) may fail to update # the file from the external editor, see # https://github.com/qutebrowser/qutebrowser/issues/1767 with tempfile.NamedTemporaryFile( mode='w', prefix='qutebrowser-editor-', encoding=config.val.editor.encoding, delete=False) as fobj: if text: fobj.write(text) self._file = fobj except OSError as e: message.error("Failed to create initial file: {}".format(e)) return 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._file.name) for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) self._proc.start(executable, args)
def _run_process(self, cmd, *args, env=None, verbose=False, output_messages=False): """Start the given command. Args: cmd: The command to be started. *args: The arguments to hand to the command env: A dictionary of environment variables to add. verbose: Show notifications when the command started/exited. output_messages: Show the output as messages. """ assert self._filepath is not None self._env['QUTE_FIFO'] = self._filepath if env is not None: self._env.update(env) self.proc = guiprocess.GUIProcess('userscript', additional_env=self._env, output_messages=output_messages, verbose=verbose, parent=self) self.proc.finished.connect(self.on_proc_finished) self.proc.error.connect(self.on_proc_error) self.proc.start(cmd, args)
def _execute_fileselect_command( command: List[str], qb_mode: FileSelectionMode, tmpfilename: Optional[str] = None ) -> List[str]: """Execute external command to choose file. Args: multiple: Should selecting multiple files be allowed. tmpfilename: Path to the temporary file if used, otherwise None. Return: A list of selected file paths, or empty list if no file is selected. If multiple is False, the return value will have at most 1 item. """ proc = guiprocess.GUIProcess(what='choose-file') proc.start(command[0], command[1:]) loop = qtutils.EventLoop() proc.finished.connect(lambda _code, _status: loop.exit()) loop.exec() if tmpfilename is None: selected_files = proc.stdout.splitlines() else: try: with open(tmpfilename, mode='r', encoding=sys.getfilesystemencoding()) as f: selected_files = f.read().splitlines() except OSError as e: message.error(f"Failed to open tempfile {tmpfilename} ({e})!") selected_files = [] return list(_validated_selected_files( qb_mode=qb_mode, selected_files=selected_files))
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] if self._watcher: assert self._filename is not None ok = self._watcher.addPath(self._filename) if not ok: log.procs.error("Failed to watch path: {}".format( self._filename)) self._watcher.fileChanged.connect( # type: ignore[attr-defined] self._on_file_changed) 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 open_file(self, cmdline=None): """Open the downloaded file. Args: cmdline: The command to use as string. A `{}` is expanded to the filename. None means to use the system's default application. If no `{}` is found, the filename is appended to the cmdline. """ assert self.successful filename = self._get_open_filename() if filename is None: # pragma: no cover log.downloads.error("No filename to open the download!") return if cmdline is None: log.downloads.debug( "Opening {} with the system application".format(filename)) url = QUrl.fromLocalFile(filename) QDesktopServices.openUrl(url) return cmd, *args = shlex.split(cmdline) args = [arg.replace('{}', filename) for arg in args] if '{}' not in cmdline: args.append(filename) log.downloads.debug("Opening {} with {}".format( filename, [cmd] + args)) proc = guiprocess.GUIProcess(what='download') proc.start_detached(cmd, args)
def test_process_completion(monkeypatch, stubs, info): monkeypatch.setattr(guiprocess, 'QProcess', stubs.FakeProcess) p1 = guiprocess.GUIProcess('testprocess') p2 = guiprocess.GUIProcess('testprocess') p3 = guiprocess.GUIProcess('editor') p1.pid = 1001 p1.cmd = 'cmd1' p1.args = [] p1.outcome.running = False p1.outcome.status = QProcess.NormalExit p1.outcome.code = 0 p2.pid = 1002 p2.cmd = 'cmd2' p2.args = [] p2.outcome.running = True p3.pid = 1003 p3.cmd = 'cmd3' p3.args = [] p3.outcome.running = False p3.outcome.status = QProcess.NormalExit p3.outcome.code = 1 monkeypatch.setattr(guiprocess, 'all_processes', { 1001: p1, 1002: p2, 1003: p3, 1004: None, # cleaned up }) model = miscmodels.process(info=info) model.set_pattern('') expected = { 'Testprocess': [ ('1002', 'running', 'cmd2'), ('1001', 'successful', 'cmd1'), ], 'Editor': [ ('1003', 'unsuccessful', 'cmd3'), ], } _check_completions(model, expected)
def proc(qtbot): """A fixture providing a GUIProcess and cleaning it up after the test.""" p = guiprocess.GUIProcess(0, 'testprocess') yield p if p._proc.state() == QProcess.Running: with qtbot.waitSignal(p.finished, timeout=10000) as blocker: p._proc.terminate() if not blocker.signal_triggered: p._proc.kill()
def open_file(filename: str, cmdline: str = None) -> None: """Open the given file. If cmdline is not given, downloads.open_dispatcher is used. If open_dispatcher is unset, the system's default application is used. Args: filename: The filename to open. cmdline: The command to use as string. A `{}` is expanded to the filename. None means to use the system's default application or `downloads.open_dispatcher` if set. If no `{}` is found, the filename is appended to the cmdline. """ # Import late to avoid circular imports: # - usertypes -> utils -> guiprocess -> message -> usertypes # - usertypes -> utils -> config -> configdata -> configtypes -> # cmdutils -> command -> message -> usertypes from qutebrowser.config import config from qutebrowser.misc import guiprocess from qutebrowser.utils import version, message # the default program to open downloads with - will be empty string # if we want to use the default override = config.val.downloads.open_dispatcher if version.is_sandboxed(): if cmdline: message.error("Cannot spawn download dispatcher from sandbox") return if override: message.warning("Ignoring download dispatcher from config in " "sandbox environment") override = None # precedence order: cmdline > downloads.open_dispatcher > openUrl if cmdline is None and not override: log.misc.debug("Opening {} with the system application" .format(filename)) url = QUrl.fromLocalFile(filename) QDesktopServices.openUrl(url) return if cmdline is None and override: cmdline = override assert cmdline is not None cmd, *args = shlex.split(cmdline) args = [arg.replace('{}', filename) for arg in args] if '{}' not in cmdline: args.append(filename) log.misc.debug("Opening {} with {}" .format(filename, [cmd] + args)) proc = guiprocess.GUIProcess(what='open-file') proc.start_detached(cmd, args)
def _start_editor(self): """Start the editor with the file opened as self._filename.""" 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:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) self._proc.start(executable, args)
def proc(qtbot, caplog): """A fixture providing a GUIProcess and cleaning it up after the test.""" p = guiprocess.GUIProcess('testprocess') yield p if p._proc.state() == QProcess.Running: with caplog.at_level(logging.ERROR): with qtbot.waitSignal(p.finished, timeout=10000, raising=False) as blocker: p._proc.terminate() if not blocker.signal_triggered: p._proc.kill()
def proc(qtbot): """A fixture providing a GUIProcess and cleaning it up after the test.""" if os.name == 'nt': # WORKAROUND for https://github.com/pytest-dev/pytest-qt/issues/67 pytest.skip("Test is flaky on Windows...") p = guiprocess.GUIProcess(0, 'test') yield p if p._proc.state() == QProcess.Running: with qtbot.waitSignal(p.finished, timeout=10000) as blocker: p._proc.terminate() if not blocker.signal_triggered: p._proc.kill()
def _spawn(self, url, context): """Spawn a simple command from a hint. Args: url: The URL to open as a QUrl. context: The HintContext to use. """ urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) args = context.get_args(urlstr) cmd, *args = args proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self) proc.start(cmd, args)
def test_existing_process(self, qtbot, py_proc): proc = guiprocess.GUIProcess('testprocess') with qtbot.wait_signal(proc.finished, timeout=5000): proc.start(*py_proc("print('AT&T')")) _mimetype, data = qutescheme.qute_process( QUrl(f'qute://process/{proc.pid}')) print(data) assert f'<title>Process {proc.pid}</title>' in data assert '-c 'print(' in data assert 'Testprocess exited successfully.' in data assert f'<pre>AT&T{os.linesep}</pre>' in data assert 'No output.' in data # stderr
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 = [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 _run_process(self, cmd, *args, env, verbose): """Start the given command. Args: cmd: The command to be started. *args: The arguments to hand to the command env: A dictionary of environment variables to add. verbose: Show notifications when the command started/exited. """ self._env = {'QUTE_FIFO': self._filepath} if env is not None: self._env.update(env) self._proc = guiprocess.GUIProcess(self._win_id, 'userscript', additional_env=self._env, verbose=verbose, parent=self) self._proc.finished.connect(self.on_proc_finished) self._proc.error.connect(self.on_proc_error) self._proc.start(cmd, args)
def open_file(filename, cmdline=None): """Open the given file. If cmdline is not given, general->default-open-dispatcher is used. If default-open-dispatcher is unset, the system's default application is used. Args: filename: The filename to open. cmdline: The command to use as string. A `{}` is expanded to the filename. None means to use the system's default application or `default-open-dispatcher` if set. If no `{}` is found, the filename is appended to the cmdline. """ # Import late to avoid circular imports: # utils -> config -> configdata -> configtypes -> cmdutils -> command -> # utils from qutebrowser.misc import guiprocess from qutebrowser.config import config # the default program to open downloads with - will be empty string # if we want to use the default override = config.get('general', 'default-open-dispatcher') # precedence order: cmdline > default-open-dispatcher > openUrl if cmdline is None and not override: log.misc.debug("Opening {} with the system application" .format(filename)) url = QUrl.fromLocalFile(filename) QDesktopServices.openUrl(url) return if cmdline is None and override: cmdline = override cmd, *args = shlex.split(cmdline) args = [arg.replace('{}', filename) for arg in args] if '{}' not in cmdline: args.append(filename) log.misc.debug("Opening {} with {}" .format(filename, [cmd] + args)) proc = guiprocess.GUIProcess(what='open-file') proc.start_detached(cmd, args)
def test_start_env(monkeypatch, qtbot, py_proc): monkeypatch.setenv('QUTEBROWSER_TEST_1', '1') env = {'QUTEBROWSER_TEST_2': '2'} proc = guiprocess.GUIProcess(0, 'testprocess', additional_env=env) argv = py_proc(""" import os import json env = dict(os.environ) print(json.dumps(env)) sys.exit(0) """) with qtbot.waitSignals([proc.started, proc.finished], raising=True, timeout=10000): proc.start(*argv) data = bytes(proc._proc.readAll()).decode('utf-8') ret_env = json.loads(data) assert 'QUTEBROWSER_TEST_1' in ret_env assert 'QUTEBROWSER_TEST_2' in ret_env
def test_start_env(monkeypatch, qtbot, py_proc): monkeypatch.setenv('QUTEBROWSER_TEST_1', '1') env = {'QUTEBROWSER_TEST_2': '2'} proc = guiprocess.GUIProcess('testprocess', additional_env=env) cmd, args = py_proc(""" import os import json import sys env = dict(os.environ) print(json.dumps(env)) sys.exit(0) """) with qtbot.wait_signals([proc.started, proc.finished], timeout=10000, order='strict'): proc.start(cmd, args) assert 'QUTEBROWSER_TEST_1' in proc.stdout assert 'QUTEBROWSER_TEST_2' in proc.stdout
def open_file(self, cmdline=None): """Open the downloaded file. Args: cmdline: The command to use as string. A `{}` is expanded to the filename. None means to use the system's default application or `default-open-dispatcher` if set. If no `{}` is found, the filename is appended to the cmdline. """ assert self.successful filename = self._get_open_filename() if filename is None: # pragma: no cover log.downloads.error("No filename to open the download!") return # the default program to open downloads with - will be empty string # if we want to use the default override = config.get('general', 'default-open-dispatcher') # precedence order: cmdline > default-open-dispatcher > openUrl if cmdline is None and not override: log.downloads.debug( "Opening {} with the system application".format(filename)) url = QUrl.fromLocalFile(filename) QDesktopServices.openUrl(url) return if cmdline is None and override: cmdline = override cmd, *args = shlex.split(cmdline) args = [arg.replace('{}', filename) for arg in args] if '{}' not in cmdline: args.append(filename) log.downloads.debug("Opening {} with {}".format( filename, [cmd] + args)) proc = guiprocess.GUIProcess(what='download') proc.start_detached(cmd, args)
def test_start_env(monkeypatch, qtbot, py_proc): monkeypatch.setenv('QUTEBROWSER_TEST_1', '1') env = {'QUTEBROWSER_TEST_2': '2'} proc = guiprocess.GUIProcess('testprocess', additional_env=env) argv = py_proc(""" import os import json import sys env = dict(os.environ) print(json.dumps(env)) sys.exit(0) """) with qtbot.waitSignal(proc.started, timeout=10000), \ qtbot.waitSignal(proc.finished, timeout=10000): proc.start(*argv) data = qutescheme.spawn_output assert 'QUTEBROWSER_TEST_1' in data assert 'QUTEBROWSER_TEST_2' in data
def choose_file(multiple: bool) -> List[str]: """Select file(s) for uploading, using external command defined in config. Args: multiple: Should selecting multiple files be allowed. Return: A list of selected file paths, or empty list if no file is selected. If multiple is False, the return value will have at most 1 item. """ handle = tempfile.NamedTemporaryFile(prefix='qutebrowser-fileselect-', delete=False) handle.close() tmpfilename = handle.name with utils.cleanup_file(tmpfilename): if multiple: command = config.val.fileselect.multiple_files.command else: command = config.val.fileselect.single_file.command proc = guiprocess.GUIProcess(what='choose-file') proc.start(command[0], [arg.replace('{}', tmpfilename) for arg in command[1:]]) loop = qtutils.EventLoop() proc.finished.connect(lambda _code, _status: loop.exit()) loop.exec() try: with open(tmpfilename, mode='r', encoding=sys.getfilesystemencoding()) as f: selected_files = f.read().splitlines() except OSError as e: message.error(f"Failed to open tempfile {tmpfilename} ({e})!") selected_files = [] if not multiple: if len(selected_files) > 1: message.warning("More than one file chosen, using only the first") return selected_files[:1] return selected_files
def fake_proc(monkeypatch, stubs): """A fixture providing a GUIProcess with a mocked QProcess.""" p = guiprocess.GUIProcess(0, 'testprocess') monkeypatch.setattr(p, '_proc', stubs.fake_qprocess()) return p