I'm working on a GUI for creating and managing virtual environments for Python 3. For this I use Python 3.7.4 and PyQt5. I would like the Creation process of the virtual environment to be done by a wizard and by using the create()
method of Python's venv
module. So far everything works as expected. The virtual environment is created properly and the wizard switches to the next page.
Now, the step where the virtual environment is being created (this happens when switching from the first page to the second), I've included a widget that shows up containing a progress bar to bridge the few seconds while venv
creates the virtual environment. This works, but the widget shows only black content when it shows up.
I have tried to fix it with threads and also with multiprocessing (by calling the two functions at the same time), but that did not work. Although the widget appears, the animation does not run as usual and is already at 100% as soon as it's visible. Also it appears after the environment has been created.
Here is a screenshot:
Here are the parts of code to reproduce:
from subprocess import Popen, PIPE, CalledProcessError
from venv import create
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtCore import (Qt, QRect, QSize, QMetaObject, QDir, QFile, QRegExp,
QBasicTimer)
from PyQt5.QtGui import (QIcon, QFont, QPixmap, QStandardItemModel,
QStandardItem)
from PyQt5.QtWidgets import (QMainWindow, QApplication, QAction, QHeaderView,
QFileDialog, QWidget, QGridLayout, QVBoxLayout,
QLabel, QPushButton, QSpacerItem, QSizePolicy,
QTableView, QAbstractItemView, QMenuBar, QMenu,
QStatusBar, QMessageBox, QWizard, QWizardPage,
QRadioButton, QCheckBox, QLineEdit, QGroupBox,
QComboBox, QToolButton, QProgressBar, QDialog,
QHBoxLayout)
#]===========================================================================[#
#] FIND INSTALLED INTERPRETERS [#============================================[#
#]===========================================================================[#
# look for installed Python versions in common locations
versions = ['3.9', '3.8', '3.7', '3.6', '3.5', '3.4', '3.3', '3']
notFound = []
versFound = []
pathFound = []
for i, v in enumerate(versions):
try:
# get installed python3 versions
getVers = Popen(["python" + v, "-V"],
stdout=PIPE, universal_newlines=True)
version = getVers.communicate()[0].strip()
# get paths of the python executables
getPath = Popen(["which", "python" + v],
stdout=PIPE, universal_newlines=True)
path = getPath.communicate()[0].strip()
versFound.append(version)
pathFound.append(path)
except (CalledProcessError, FileNotFoundError):
notFound.append(i)
This is the progress bar:
#]===========================================================================[#
#] PROGRESS BAR [#===========================================================[#
#]===========================================================================[#
class ProgBarWidget(QWidget):
def __init__(self):
super().__init__()
self.initMe()
def initMe(self):
# basic window settings
self.setGeometry(600, 300, 300, 80)
self.setFixedSize(325, 80)
self.setWindowTitle("Creating")
self.setWindowFlag(Qt.WindowCloseButtonHint, False)
self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)
horizontalLayout = QHBoxLayout(self)
verticalLayout = QVBoxLayout()
statusLabel = QLabel(self)
statusLabel.setText("Creating virtual environment...")
self.progressBar = QProgressBar(self)
self.progressBar.setFixedSize(300, 23)
self.timer = QBasicTimer()
self.timer.start(0, self)
self.i = 0
verticalLayout.addWidget(statusLabel)
verticalLayout.addWidget(self.progressBar)
horizontalLayout.addLayout(verticalLayout)
self.setLayout(horizontalLayout)
def timerEvent(self, e):
if self.i >= 100:
self.timer.stop()
#self.close()
self.i += 1
self.progressBar.setValue(self.i)
This is the wizard part:
#]===========================================================================[#
#] VENV WIZARD [#============================================================[#
#]===========================================================================[#
class VenvWizard(QWizard):
def __init__(self):
super().__init__()
self.setWindowTitle("Venv Wizard")
self.resize(535, 430)
self.move(578, 183)
self.setStyleSheet(
"""
QToolTip {
background-color: rgb(47, 52, 63);
border: rgb(47, 52, 63);
color: rgb(210, 210, 210);
padding: 2px;
opacity: 325
}
"""
)
self.addPage(BasicSettings())
self.addPage(InstallPackages())
self.addPage(Summary())
First wizard page:
class BasicSettings(QWizardPage):
def __init__(self):
super().__init__()
folder_icon = QIcon.fromTheme("folder")
self.setTitle("Basic Settings")
self.setSubTitle("This wizard will help you to create and set up "
"a virtual environment for Python 3. ")
interpreterLabel = QLabel("&Interpreter:")
self.interprComboBox = QComboBox()
interpreterLabel.setBuddy(self.interprComboBox)
# add items from versFound to combobox
self.interprComboBox.addItem("---")
for i in range(len(versFound)):
self.interprComboBox.addItem(versFound[i], pathFound[i])
venvNameLabel = QLabel("Venv &name:")
self.venvNameLineEdit = QLineEdit()
venvNameLabel.setBuddy(self.venvNameLineEdit)
venvLocationLabel = QLabel("&Location:")
self.venvLocationLineEdit = QLineEdit()
venvLocationLabel.setBuddy(self.venvLocationLineEdit)
selectFolderToolButton = QToolButton()
selectFolderToolButton.setFixedSize(26, 27)
selectFolderToolButton.setIcon(folder_icon)
selectFolderToolButton.setToolTip("Browse")
# TODO: remove placeholder and add a spacer instead
placeHolder = QLabel()
# options groupbox
groupBox = QGroupBox("Options")
self.withPipCBox = QCheckBox("Install and update &Pip")
self.sysSitePkgsCBox = QCheckBox(
"&Make system (global) site-packages dir available to venv")
self.launchVenvCBox = QCheckBox(
"Launch a terminal with activated &venv after installation")
self.symlinksCBox = QCheckBox(
"Attempt to &symlink rather than copy files into venv")
# events
self.withPipCBox.toggled.connect(self.collectData)
self.sysSitePkgsCBox.toggled.connect(self.collectData)
self.launchVenvCBox.toggled.connect(self.collectData)
self.venvNameLineEdit.textChanged.connect(self.collectData)
self.venvLocationLineEdit.textChanged.connect(self.collectData)
self.interprComboBox.currentIndexChanged.connect(self.collectData)
self.symlinksCBox.toggled.connect(self.collectData)
selectFolderToolButton.clicked.connect(self.selectDir)
# store the collected values
self.interprVers = QLineEdit()
self.interprPath = QLineEdit()
self.venvName = QLineEdit()
self.venvLocation = QLineEdit()
self.withPip = QLineEdit()
self.sysSitePkgs = QLineEdit()
self.launchVenv = QLineEdit()
self.symlinks = QLineEdit()
# register fields
self.registerField("interprComboBox*", self.interprComboBox)
self.registerField("venvNameLineEdit*", self.venvNameLineEdit)
self.registerField("venvLocationLineEdit*", self.venvLocationLineEdit)
self.registerField("interprVers", self.interprVers)
self.registerField("interprPath", self.interprPath)
self.registerField("venvName", self.venvName)
self.registerField("venvLocation", self.venvLocation)
self.registerField("withPip", self.withPip)
self.registerField("sysSitePkgs", self.sysSitePkgs)
self.registerField("launchVenv", self.launchVenv)
self.registerField("symlinks", self.symlinks)
# grid layout
gridLayout = QGridLayout()
gridLayout.addWidget(interpreterLabel, 0, 0, 1, 1)
gridLayout.addWidget(self.interprComboBox, 0, 1, 1, 2)
gridLayout.addWidget(venvNameLabel, 1, 0, 1, 1)
gridLayout.addWidget(self.venvNameLineEdit, 1, 1, 1, 2)
gridLayout.addWidget(venvLocationLabel, 2, 0, 1, 1)
gridLayout.addWidget(self.venvLocationLineEdit, 2, 1, 1, 1)
gridLayout.addWidget(selectFolderToolButton, 2, 2, 1, 1)
gridLayout.addWidget(placeHolder, 3, 0, 1, 2)
gridLayout.addWidget(groupBox, 4, 0, 1, 3)
self.setLayout(gridLayout)
# options groupbox
groupBoxLayout = QVBoxLayout()
groupBoxLayout.addWidget(self.withPipCBox)
groupBoxLayout.addWidget(self.sysSitePkgsCBox)
groupBoxLayout.addWidget(self.launchVenvCBox)
groupBoxLayout.addWidget(self.symlinksCBox)
groupBox.setLayout(groupBoxLayout)
#]=======================================================================[#
#] SELECTIONS [#=========================================================[#
#]=======================================================================[#
def selectDir(self):
"""
Specify path where to create venv.
"""
fileDiag = QFileDialog()
folderName = fileDiag.getExistingDirectory()
self.venvLocationLineEdit.setText(folderName)
def collectData(self, i):
"""
Collect all input data.
"""
self.interprVers.setText(self.interprComboBox.currentText())
self.interprPath.setText(self.interprComboBox.currentData())
self.venvName.setText(self.venvNameLineEdit.text())
self.venvLocation.setText(self.venvLocationLineEdit.text())
# options
self.withPip.setText(str(self.withPipCBox.isChecked()))
self.sysSitePkgs.setText(str(self.sysSitePkgsCBox.isChecked()))
self.launchVenv.setText(str(self.launchVenvCBox.isChecked()))
self.symlinks.setText(str(self.symlinksCBox.isChecked()))
The second wizard page:
class InstallPackages(QWizardPage):
def __init__(self):
super().__init__()
self.setTitle("Install Packages")
self.setSubTitle("Specify the packages which you want Pip to "
"install into the virtual environment.")
# ...
self.progressBar = ProgBarWidget()
def initializePage(self):
#interprVers = self.field("interprVers")
interprPath = self.field("interprPath")
self.venvName = self.field("venvName")
self.venvLocation = self.field("venvLocation")
self.withPip = self.field("withPip")
self.sysSitePkgs = self.field("sysSitePkgs")
#launchVenv = self.field("launchVenv")
self.symlinks = self.field("symlinks")
# overwrite with the selected interpreter
sys.executable = interprPath
# run the create process
self.createProcess()
# tried threading, but didn't really change the behaviour
#Thread(target=self.progressBar.show).start()
#Thread(target=self.createProcess).start()
def createProcess(self):
"""
Create the virtual environment.
"""
print("Creating virtual environment...") # print to console
self.progressBar.show() # the window containing the progress bar
# the create method from Python's venv module
create('/'.join([self.venvLocation, self.venvName]),
system_site_packages=self.sysSitePkgs,
symlinks=self.symlinks, with_pip=self.withPip)
self.progressBar.close() # close when done
print("Done.") # print to console when done
The last wizard page (not relevant in this case.):
class Summary(QWizardPage):
def __init__(self):
super().__init__()
self.setTitle("Summary")
self.setSubTitle("...............")
# ...
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
ui = VenvWizard()
ui.show()
sys.exit(app.exec_())
My questions:
Is this the correct way to show up a prgress bar between two QWizardPage
s?
If not, what could be a better way achieving that?
In this case I have 2 observations:
setRange(0, 0)
class ProgBarWidget(QWidget):
def __init__(self):
super().__init__()
self.initMe()
def initMe(self):
# basic window settings
self.setGeometry(600, 300, 300, 80)
self.setFixedSize(325, 80)
self.setWindowTitle("Creating")
self.setWindowFlag(Qt.WindowCloseButtonHint, False)
self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)
horizontalLayout = QHBoxLayout(self)
verticalLayout = QVBoxLayout()
statusLabel = QLabel(self)
statusLabel.setText("Creating virtual environment...")
self.progressBar = QProgressBar(self)
self.progressBar.setFixedSize(300, 23)
self.progressBar.setRange(0, 0)
verticalLayout.addWidget(statusLabel)
verticalLayout.addWidget(self.progressBar)
horizontalLayout.addLayout(verticalLayout)
self.setLayout(horizontalLayout)
from functools import partial
from PyQt5.QtCore import QObject, QTimer, QThread, pyqtSignal, pyqtSlot
# ...
class InstallWorker(QObject):
started = pyqtSignal()
finished = pyqtSignal()
@pyqtSlot(tuple)
def install(self, args):
self.started.emit()
location, name, site_packages, symlinks, withPip = args
create(
"/".join([location, name]),
system_site_packages=site_packages,
symlinks=symlinks,
with_pip=withPip,
)
self.finished.emit()
# ...
class InstallPackages(QWizardPage):
def __init__(self):
super().__init__()
self.setTitle("Install Packages")
self.setSubTitle("Specify the packages which you want Pip to "
"install into the virtual environment.")
self.progressBar = ProgBarWidget()
thread = QThread(self)
thread.start()
self.m_install_worker = InstallWorker()
self.m_install_worker.moveToThread(thread)
self.m_install_worker.started.connect(self.progressBar.show)
self.m_install_worker.finished.connect(self.progressBar.close)
def initializePage(self):
# ...
# run the create process
self.createProcess()
def createProcess(self):
"""
Create the virtual environment.
"""
args = (
self.venvName,
self.venvLocation,
self.withPip,
self.sysSitePkgs,
self.symlinks,
)
wrapper = partial(self.m_install_worker.install, args)
QTimer.singleShot(0, wrapper)