Reputation: 1495
I have a script that activates a virtual environment and runs pip
commands inside it. For that I first create a bash
script with the needed commands and pass the final command (which finally runs the script) to run_script()
which produces an output line by line. The subprocess works fine as well as printing the output to a console.
Now, what I'm trying to achieve is to display the catched realtime output (that comes line by line) of run_script()
(which shows the installation progress of pip install ...
) together with a QProgressBar
in a QDialog
.
So far, I tried to set the text of self.statusLabel
in ProgBarDialog
class, but that doesn't work as expected. I thought I could create a loop similar to this
for line in output:
self.statusLabel.setText(line)
and show each line of the process output after the other. But I don't know how exactly to catch each line from the output since the output comes as a big string and because of that, of course for line in output
catches the characters and not the lines.
How do I have to manipulate the output to format it the correct way to be able to show it in a widget (for example a QLabel
or something similar) inside a QDialog
?
(it might be that the way I coded this is stupid or inefficient, so any suggests are welcome)
Note: A virtual environment beside testfile.py
is needed to reproduce.
from subprocess import Popen, PIPE
import sys
import os
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QDialog, QVBoxLayout,
QHBoxLayout, QLabel, QProgressBar)
def has_bash():
"""
Test if bash is available. If present the string `/bin/bash` is returned,
an empty string otherwise.
"""
res = Popen(
["which", "bash"], stdout=PIPE, stderr=PIPE, text="utf-8"
)
out, _ = res.communicate()
shell = out.strip()
return shell
def run_script(command):
"""
Run the script and catch output of the subprocess line by line.
The `command` argument is set in `run_pip()`.
"""
process = Popen(command, stdout=PIPE, text="utf-8")
while True:
output = process.stdout.readline()
if output == "" and process.poll() is not None:
break
if output:
# TODO: show output in dialog together with a progressbar
print(f"[PIP]: {output.strip()}")
rc = process.poll()
return rc
def run_pip(cmd, opt, package, venv_dir, venv_name):
"""
Activate the virtual environment and run pip commands.
"""
current_dir = os.path.dirname(os.path.realpath(__file__))
script = os.path.join(current_dir, "run.sh")
if has_bash():
# create run script
with open(script, "w") as f:
f.write(
"#!/bin/bash\n"
f"source {venv_dir}/{venv_name}/bin/activate\n"
f"pip {cmd}{opt}{package}\n"
"deactivate\n"
)
# make it executable
os.system(f"chmod +x {script}")
# run script
command = ["/bin/bash", script]
run_script(command)
class ProgBarDialog(QDialog):
"""
Dialog showing output and a progress bar during the installation process.
"""
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(675, 365, 325, 80)
self.setFixedSize(350, 85)
self.setWindowFlag(Qt.WindowCloseButtonHint, False)
self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)
h_Layout = QHBoxLayout(self)
v_Layout = QVBoxLayout()
h_Layout.setContentsMargins(0, 15, 0, 0)
self.statusLabel = QLabel(self)
self.placeHolder = QLabel(self)
self.progressBar = QProgressBar(self)
self.progressBar.setFixedSize(325, 23)
self.progressBar.setRange(0, 0)
v_Layout.addWidget(self.statusLabel)
v_Layout.addWidget(self.progressBar)
v_Layout.addWidget(self.placeHolder)
h_Layout.addLayout(v_Layout)
self.setLayout(h_Layout)
if __name__ == "__main__":
cmd = ["install "]
opt = ["--upgrade "]
package = "pylint" # this could be any package
current_dir = os.path.dirname(os.path.realpath(__file__))
venv_name = "testenv" # a virtual env beside this test file
run_pip(cmd[0], opt[0], package, current_dir, venv_name)
#]=======================================================================[#
app = QApplication(sys.argv)
progBar = ProgBarDialog()
progBar.show()
sys.exit(app.exec_())
Upvotes: 2
Views: 937
Reputation: 243955
In this case it is better to use QProcess since it does not block the eventloop and notifies you through a signal when there is a new output:
import os
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess, Qt
from PyQt5.QtGui import QFontMetrics
from PyQt5.QtWidgets import QApplication, QDialog, QHBoxLayout, QLabel, QProgressBar, QVBoxLayout
def has_bash():
process = QProcess()
process.start("which bash")
process.waitForStarted()
process.waitForFinished()
if process.exitStatus() == QProcess.NormalExit:
return bool(process.readAll())
return False
class PipManager(QObject):
started = pyqtSignal()
finished = pyqtSignal()
textChanged = pyqtSignal(str)
def __init__(self, venv_dir, venv_name, parent=None):
super().__init__(parent)
self._venv_dir = venv_dir
self._venv_name = venv_name
self._process = QProcess(self)
self._process.readyReadStandardError.connect(self.onReadyReadStandardError)
self._process.readyReadStandardOutput.connect(self.onReadyReadStandardOutput)
self._process.stateChanged.connect(self.onStateChanged)
self._process.started.connect(self.started)
self._process.finished.connect(self.finished)
self._process.finished.connect(self.onFinished)
self._process.setWorkingDirectory(venv_dir)
def run_command(self, command="", options=None):
if has_bash():
if options is None:
options = []
script = f"""source {self._venv_name}/bin/activate; pip {command} {" ".join(options)}; deactivate;"""
self._process.start("bash", ["-c", script])
@pyqtSlot(QProcess.ProcessState)
def onStateChanged(self, state):
if state == QProcess.NotRunning:
print("not running")
elif state == QProcess.Starting:
print("starting")
elif state == QProcess.Running:
print("running")
@pyqtSlot(int, QProcess.ExitStatus)
def onFinished(self, exitCode, exitStatus):
print(exitCode, exitStatus)
@pyqtSlot()
def onReadyReadStandardError(self):
message = self._process.readAllStandardError().data().decode().strip()
print("error:", message)
self.finished.emit()
self._process.kill()
"""self.textChanged.emit(message)"""
@pyqtSlot()
def onReadyReadStandardOutput(self):
message = self._process.readAllStandardOutput().data().decode().strip()
self.textChanged.emit(message)
class ProgBarDialog(QDialog):
"""
Dialog showing output and a progress bar during the installation process.
"""
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setFixedWidth(400)
self.setWindowFlag(Qt.WindowCloseButtonHint, False)
self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)
self.statusLabel = QLabel()
self.placeHolder = QLabel()
self.progressBar = QProgressBar()
self.progressBar.setFixedHeight(23)
self.progressBar.setRange(0, 0)
v_Layout = QVBoxLayout(self)
v_Layout.addWidget(self.statusLabel)
v_Layout.addWidget(self.progressBar)
v_Layout.addWidget(self.placeHolder)
@pyqtSlot(str)
def update_status(self, status):
metrix = QFontMetrics(self.statusLabel.font())
clippedText = metrix.elidedText(status, Qt.ElideRight, self.statusLabel.width())
self.statusLabel.setText(clippedText)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
progBar = ProgBarDialog()
current_dir = os.path.dirname(os.path.realpath(__file__))
venv_name = "testenv"
manager = PipManager(current_dir, venv_name)
manager.textChanged.connect(progBar.update_status)
manager.started.connect(progBar.show)
manager.finished.connect(progBar.close)
manager.run_command("install", ["--upgrade", "pylint"])
sys.exit(app.exec_())
Upvotes: 4