python启动编辑器外部并获取文本

问题描述 投票:2回答:3

我正在写一个PyQt程序,我想让用户启动他们喜欢的编辑器来填写TextEdit字段。

因此,目标是在tmp文件外部启动编辑器(比如vim),并在编辑器关闭时将其上下文转换为python变量。

我发现了一些类似的问题,如Opening vi from Pythoncall up an EDITOR (vim) from a python scriptinvoke an editor ( vim ) in python。但它们都处于“阻塞”状态,就像git commit命令一样。我所追求的是一种“非阻塞”方式(因为它是一个GUI),类似于zimwiki中的“编辑源”功能。

我目前的尝试:

import os
import tempfile
import threading
import subprocess

def popenAndCall(onExit, popenArgs):

    def runInThread(onExit, popenArgs):
        tmppath=popenArgs[-1]
        proc = subprocess.Popen(popenArgs)
        # this immediately finishes OPENING vim.
        rec=proc.wait()
        print('# <runInThread>: rec=', rec)
        onExit(tmppath)
        os.remove(tmppath)
        return

    thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
    thread.start()
    return thread

def openEditor():

    fd, filepath=tempfile.mkstemp()
    print('filepath=',filepath)

    def cb(tmppath):
        print('# <cb>: cb tmppath=',tmppath)
        with open(tmppath, 'r') as tmp:
            lines=tmp.readlines()
            for ii in lines:
                print('# <cb>: ii',ii)
        return

    with os.fdopen(fd, 'w') as tmp:

        cmdflag='--'
        editor_cmd='vim'
        cmd=[os.environ['TERMCMD'], cmdflag, editor_cmd, filepath]
        print('#cmd = ',cmd)

        popenAndCall(cb, cmd)
        print('done')

    return


if __name__=='__main__':

    openEditor()

我认为它失败了因为Popen.wait()只等到编辑器打开,直到它关闭。所以它没有从编辑器中捕获任何内容。

不知道怎么解决这个问题?谢谢!

编辑:

我发现这个answer我认为是相关的。我正在努力让os等待process group,但它仍然无法正常工作。代码如下:

def popenAndCall(onExit, popenArgs):

    def runInThread(onExit, popenArgs):
        tmppath=popenArgs[-1]
        proc = subprocess.Popen(popenArgs, preexec_fn=os.setsid)
        pid=proc.pid
        gid=os.getpgid(pid)
        #rec=proc.wait()
        rec=os.waitid(os.P_PGID, gid, os.WEXITED | os.WSTOPPED)
        print('# <runInThread>: rec=', rec, 'pid=',pid, 'gid=',gid)

        onExit(tmppath)
        os.remove(tmppath)
        return

    thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
    thread.start()
    return thread

我假设这个gid=os.getpgid(pid)给了我这个组的id,而os.waitid()等着这个组。我也试过os.waitpid(gid, 0),也没用。

我走在正确的轨道上?

更新:

对于一些有效的编辑来说,似乎就像xed一样。 vimgvim都失败了。

python pyqt subprocess
3个回答
2
投票

使用QProcess,您可以在不阻塞Qt事件循环的情况下启动进程。

在这种情况下,我使用xterm,因为我不知道在TERMCMD中建立了哪个终端。

from PyQt5 import QtCore, QtGui, QtWidgets


class EditorWorker(QtCore.QObject):
    finished = QtCore.pyqtSignal()

    def __init__(self, command, parent=None):
        super(EditorWorker, self).__init__(parent)
        self._temp_file = QtCore.QTemporaryFile(self)
        self._process = QtCore.QProcess(self)
        self._process.finished.connect(self.on_finished)
        self._text = ""
        if self._temp_file.open():
            program, *arguments = command
            self._process.start(
                program, arguments + [self._temp_file.fileName()]
            )

    @QtCore.pyqtSlot()
    def on_finished(self):
        if self._temp_file.isOpen():
            self._text = self._temp_file.readAll().data().decode()
            self.finished.emit()

    @property
    def text(self):
        return self._text

    def __del__(self):
        self._process.kill()


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent)
        self._button = QtWidgets.QPushButton(
            "Launch VIM", clicked=self.on_clicked
        )
        self._text_edit = QtWidgets.QTextEdit(readOnly=True)

        lay = QtWidgets.QVBoxLayout(self)
        lay.addWidget(self._button)
        lay.addWidget(self._text_edit)

    @QtCore.pyqtSlot()
    def on_clicked(self):
        worker = EditorWorker("xterm -e vim".split(), self)
        worker.finished.connect(self.on_finished)

    @QtCore.pyqtSlot()
    def on_finished(self):
        worker = self.sender()
        prev_cursor = self._text_edit.textCursor()
        self._text_edit.moveCursor(QtGui.QTextCursor.End)
        self._text_edit.insertPlainText(worker.text)
        self._text_edit.setTextCursor(prev_cursor)
        worker.deleteLater()


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())

我想在你的情况下你应该改变

"xterm -e vim".split()

[os.environ['TERMCMD'], "--", "vim"]

可能的命令:

- xterm -e vim
- xfce4-terminal --disable-server -x vim

更新:

实现与pyinotify一样的逻辑,用于监视文件,但在这种情况下使用QFileSystemWatcher这是一个多平台解决方案:

from PyQt5 import QtCore, QtGui, QtWidgets


class EditorWorker(QtCore.QObject):
    finished = QtCore.pyqtSignal()

    def __init__(self, command, parent=None):
        super(EditorWorker, self).__init__(parent)
        self._temp_file = QtCore.QTemporaryFile(self)
        self._process = QtCore.QProcess(self)
        self._text = ""
        self._watcher = QtCore.QFileSystemWatcher(self)
        self._watcher.fileChanged.connect(self.on_fileChanged)

        if self._temp_file.open():
            self._watcher.addPath(self._temp_file.fileName())

            program, *arguments = command
            self._process.start(
                program, arguments + [self._temp_file.fileName()]
            )

    @QtCore.pyqtSlot()
    def on_fileChanged(self):
        if self._temp_file.isOpen():
            self._text = self._temp_file.readAll().data().decode()
            self.finished.emit()

    @property
    def text(self):
        return self._text

    def __del__(self):
        self._process.kill()


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent)
        self._button = QtWidgets.QPushButton(
            "Launch VIM", clicked=self.on_clicked
        )
        self._text_edit = QtWidgets.QTextEdit(readOnly=True)

        lay = QtWidgets.QVBoxLayout(self)
        lay.addWidget(self._button)
        lay.addWidget(self._text_edit)

    @QtCore.pyqtSlot()
    def on_clicked(self):
        worker = EditorWorker("gnome-terminal -- vim".split(), self)
        worker.finished.connect(self.on_finished)

    @QtCore.pyqtSlot()
    def on_finished(self):
        worker = self.sender()
        prev_cursor = self._text_edit.textCursor()
        self._text_edit.moveCursor(QtGui.QTextCursor.End)
        self._text_edit.insertPlainText(worker.text)
        self._text_edit.setTextCursor(prev_cursor)
        worker.deleteLater()


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())

1
投票

我再现的问题是proc是gnome-terminal进程而不是vim进程。

以下是适用于我的两个选项。

1)找到文本编辑器的过程而不是终端的过程。使用正确的进程ID,代码可以等待文本编辑器的完成过程。

用psutil(便携式)

Finds the latest editor process in the list of all running processes.

import psutil
def popenAndCall(onExit, popenArgs):

    def runInThread(onExit, popenArgs):
        tmppath=popenArgs[-1]
        editor_cmd=popenArgs[-2]  # vim
        proc = subprocess.Popen(popenArgs)
        proc.wait()

        # Find the latest editor process in the list of all running processes
        editor_processes = []

        for p in psutil.process_iter():
            try:
                process_name = p.name()
                if editor_cmd in process_name:
                    editor_processes.append((process_name, p.pid))
            except:
                pass

        editor_proc = psutil.Process(editor_processes[-1][1])

        rec=editor_proc.wait()
        print('# <runInThread>: rec=', rec)
        onExit(tmppath)
        os.remove(tmppath)
        return

    thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
    thread.start()
    return thread

没有psutil(适用于Linux,但不能移植到Mac OS或Windows)

https://stackoverflow.com/a/2704947/241866source code of psutil得出。

def popenAndCall(onExit, popenArgs):

    def runInThread(onExit, popenArgs):
        tmppath=popenArgs[-1]
        editor_cmd=popenArgs[-2]  # vim
        proc = subprocess.Popen(popenArgs)
        proc.wait()

        # Find the latest editor process in the list of all running processes

        pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]

        editor_processes = []
        for pid in pids:
            try:
                process_name = open(os.path.join('/proc', pid, 'cmdline'), 'rb').read().split('\0')[0]
                if editor_cmd in process_name:
                    editor_processes.append((process_name, int(pid)))
            except IOError:
                continue
        editor_proc_pid = editor_processes[-1][1]

        def pid_exists(pid):
            try:
                os.kill(pid, 0)
                return True
            except:
                return 

        while True:
            if pid_exists(editor_proc_pid):
                import time
                time.sleep(1)
            else:
                break

        onExit(tmppath)
        os.remove(tmppath)
        return

    thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
    thread.start()
    return thread

2)作为最后的手段,您可以在更新文本之前捕获UI事件:

def popenAndCall(onExit, popenArgs):

    def runInThread(onExit, popenArgs):
        tmppath=popenArgs[-1]
        proc = subprocess.Popen(popenArgs)
        # this immediately finishes OPENING vim.
        rec=proc.wait()
        raw_input("Press Enter")  # replace this with UI event
        print('# <runInThread>: rec=', rec)
        onExit(tmppath)
        os.remove(tmppath)
        return

    thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
    thread.start()
    return thread

0
投票

我认为@ eyllanesc的解决方案非常接近于zim正在做的事情(zim正在使用GObject.spawn_async()GObject.child_watch_add(),我没有使用GObject,我猜这相当于QProcess.start())。但是我们遇到一些关于某些终端(如gnome-terminal)处理新终端会话启动的问题。

我试图监视编辑器打开的临时文件,在写入/保存临时文件时我可以调用我的回调。监测是使用pyinotify完成的。我已经尝试过gnome-terminalxtermurxvt和普通的gvim,一切似乎都有效。

代码如下:

import threading
from PyQt5 import QtCore, QtGui, QtWidgets
import pyinotify


class EditorWorker(QtCore.QObject):
    file_close_sig = QtCore.pyqtSignal()
    edit_done_sig = QtCore.pyqtSignal()

    def __init__(self, command, parent=None):
        super(EditorWorker, self).__init__(parent)
        self._temp_file = QtCore.QTemporaryFile(self)
        self._process = QtCore.QProcess(self)
        #self._process.finished.connect(self.on_file_close)
        self.file_close_sig.connect(self.on_file_close)
        self._text = ""
        if self._temp_file.open():
            program, *arguments = command
            self._process.start(
                program, arguments + [self._temp_file.fileName()]
            )
            tmpfile=self._temp_file.fileName()
            # start a thread to monitor file saving/closing
            self.monitor_thread = threading.Thread(target=self.monitorFile,
                    args=(tmpfile, self.file_close_sig))
            self.monitor_thread.start()

    @QtCore.pyqtSlot()
    def on_file_close(self):
        if self._temp_file.isOpen():
            print('open')
            self._text = self._temp_file.readAll().data().decode()
            self.edit_done_sig.emit()
        else:
            print('not open')

    @property
    def text(self):
        return self._text

    def __del__(self):
        try:
            self._process.kill()
        except:
            pass

    def monitorFile(self, path, sig):

        class PClose(pyinotify.ProcessEvent):
            def my_init(self):
                self.sig=sig
                self.done=False

            def process_IN_CLOSE(self, event):
                f = event.name and os.path.join(event.path, event.name) or event.path
                self.sig.emit()
                self.done=True

        wm = pyinotify.WatchManager()
        eventHandler=PClose()
        notifier = pyinotify.Notifier(wm, eventHandler)
        wm.add_watch(path, pyinotify.IN_CLOSE_WRITE)

        try:
            while not eventHandler.done:
                notifier.process_events()
                if notifier.check_events():
                    notifier.read_events()
        except KeyboardInterrupt:
            notifier.stop()
            return


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent)
        self._button = QtWidgets.QPushButton(
            "Launch VIM", clicked=self.on_clicked
        )
        self._text_edit = QtWidgets.QTextEdit(readOnly=True)

        lay = QtWidgets.QVBoxLayout(self)
        lay.addWidget(self._button)
        lay.addWidget(self._text_edit)

    @QtCore.pyqtSlot()
    def on_clicked(self):
        worker = EditorWorker(["gnome-terminal", '--', "vim"], self)
        worker.edit_done_sig.connect(self.on_edit_done)

    @QtCore.pyqtSlot()
    def on_edit_done(self):
        worker = self.sender()
        prev_cursor = self._text_edit.textCursor()
        self._text_edit.moveCursor(QtGui.QTextCursor.End)
        self._text_edit.insertPlainText(worker.text)
        self._text_edit.setTextCursor(prev_cursor)
        worker.deleteLater()


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())

pyinotify仅适用于Linux。如果你能找到一个跨平台的解决方案(至少在Mac上),请告诉我。

更新:这似乎并不健全。 pyinotify报告文件写入而不仅仅是文件关闭。我很沮丧。

© www.soinside.com 2019 - 2024. All rights reserved.