Source code for pyspread.installer

# -*- coding: utf-8 -*-

# Copyright Martin Manns
# Distributed under the terms of the GNU General Public License

# --------------------------------------------------------------------
# pyspread is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyspread is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyspread.  If not, see <http://www.gnu.org/licenses/>.
# --------------------------------------------------------------------

"""

**Provides**

* :class:`Module`
* :class:`DependenciesDialog`
* :class:`InstallPackageDialog`

"""


try:
    from dataclasses import dataclass
except ImportError:
    # Python 3.6 compatibility
    from pyspread.lib.dataclasses import dataclass
import os

try:
    from pkg_resources import get_distribution, DistributionNotFound
except ImportError:
    get_distribution = None
from PyQt6.QtCore import QProcess, QSize
from PyQt6.QtGui import QColor, QTextCursor
from PyQt6.QtWidgets import (
        QDialog, QButtonGroup, QVBoxLayout, QHBoxLayout, QTreeWidgetItem,
        QToolButton, QGroupBox, QTreeWidget, QCheckBox, QLineEdit,
        QPlainTextEdit, QWidget, QPushButton)

try:
    from packaging import version
except ImportError:
    # We fall back to local library to remove the dependency
    try:
        from pyspread.lib.packaging import version
    except ImportError:
        from lib.packaging import version
try:
    from pyspread.lib.attrdict import AttrDict
except ImportError:
    from lib.attrdict import AttrDict


[docs] @dataclass class Module: """Module checker""" name: str description: str required_version: str # The minimum version number that is required @property def version(self) -> version: """Currently installed version number, False if not installed""" if get_distribution is None: return try: return version.parse(get_distribution(self.name).version) except DistributionNotFound: return False
[docs] def is_installed(self) -> bool: """True if the module is installed""" __version = self.version return bool(__version) if __version is not None else None
# Required dependencies # --------------------- # Required dependencies are checked by the cli REQUIRED_DEPENDENCIES = [ Module(name="markdown2", description="Python implementation of Markdown.", required_version=version.parse("2.3")), Module(name="numpy", description="Fundamental package for scientific computing.", required_version=version.parse("1.1")), Module(name="PyQt6", description="Python bindings for the Qt application framework.", required_version=version.parse("6.5")), Module(name="setuptools", description="Download, build, install, upgrade, and uninstall " "Python packages.", required_version=version.parse("40.0")), ] # Optional dependencies # --------------------- OPTIONAL_DEPENDENCIES = [ Module(name="matplotlib", description="Create visualizations in Python.", required_version=version.parse("3.4")), Module(name="openpyxl", description="Read Excel 2010 xlsx files.", required_version=version.parse("3.0.7")), Module(name="plotnine", description="Grammar of graphics for simpler R ggplot2 charts.", required_version=version.parse("0.8")), Module(name="pycel", description="Translate an Excel spreadsheet into executable python " "code.", required_version=version.parse("1.0b30")), Module(name="python-dateutil", description="Parse date strings.", required_version=version.parse("2.7.0")), Module(name="pyenchant", description="Bindings for the Enchant spellchecking library.", required_version=version.parse("1.1")), Module(name="py-moneyed", description="Money class for csv import and usage in cells.", required_version=version.parse("2.0")), Module(name="rpy2", description="Interface to R, required for R charts.", required_version=version.parse("3.4")), ] DEPENDENCIES = REQUIRED_DEPENDENCIES + OPTIONAL_DEPENDENCIES PIP_MODULE = Module(name="pip", description="pip installer", required_version=version.parse("17.0")) # Not yet implemented modules # Module(name="xlrd", # description="Load Excel files", # required_version="0.9.2"), # Module(name="xlwt", # description="Save Excel files", # required_version="0.9.2"),
[docs] class DependenciesDialog(QDialog): """Dependencies dialog for python dependencies""" column = AttrDict(zip(("button", "status", "name", "version", "required_version", "description"), range(6))) column_headers = ("", "Status", "Package", "Version", "Required", "Description") def __init__(self, parent: QWidget = None): """ :param parent: Parent widget """ super().__init__(parent) self.setWindowTitle("Installer") # Button group for install buttons self.buttGroup = QButtonGroup() self.buttGroup.buttonClicked.connect(self.on_butt_install) self.mainLayout = QVBoxLayout() self.mainLayout.setContentsMargins(10, 10, 10, 10) self.setLayout(self.mainLayout) self.tree = QTreeWidget() self.mainLayout.addWidget(self.tree, 4) self.tree.setHeaderLabels(self.column_headers) self.tree.setRootIsDecorated(False) self.tree.setSelectionMode(QTreeWidget.SelectionMode.NoSelection) self.update_load()
[docs] def sizeHint(self) -> QSize: """Overloaded method""" return QSize(700, 200)
[docs] def update_load(self): self.tree.clear() for module in DEPENDENCIES: item = QTreeWidgetItem() if module in REQUIRED_DEPENDENCIES: item.setText(self.column.button, "Required") else: item.setText(self.column.button, "Optional") item.setText(self.column.name, module.name) version = module.version if module.version else "Not installed" item.setText(self.column.version, str(version)) item.setText(self.column.required_version, str(module.required_version)) item.setText(self.column.description, module.description) self.tree.addTopLevelItem(item) if module.is_installed(): color = "#DBFEAC" status = "Installed" elif module.is_installed() is None: color = "#666666" status = "pkg_resources is missing" else: status = "Not installed" color = "#F3FFBB" butt = QToolButton() butt.setText("Install") butt.setEnabled(PIP_MODULE.is_installed()) self.tree.setItemWidget(item, self.column.button, butt) self.buttGroup.addButton(butt, DEPENDENCIES.index(module)) item.setText(self.column.status, status) item.setBackground(self.column.status, QColor(color))
[docs] def on_butt_install(self, butt: QPushButton): """One of install buttons pressed :param butt: The pressed button """ butt.setDisabled(True) idx = self.buttGroup.id(butt) dial = InstallPackageDialog(self, module=DEPENDENCIES[idx]) dial.exec() self.update_load()
[docs] class InstallPackageDialog(QDialog): """Shows a dialog to execute command""" line_str = "-" * 56 def __init__(self, parent=None, module=None): """ :param parent: Parent widget :param module: Module to be installed """ super().__init__(parent) self.module = module self.setWindowTitle("Install Package") self.setMinimumWidth(600) self.process = QProcess(self) self.process.readyReadStandardOutput.connect(self.on_read_standard) self.process.readyReadStandardError.connect(self.on_read_error) self.process.finished.connect(self.on_finished) self.mainLayout = QVBoxLayout() self.mainLayout.setContentsMargins(10, 10, 10, 10) self.setLayout(self.mainLayout) self.groupBox = QGroupBox() self.groupBox.setTitle("Shell Command") self.groupBoxLayout = QHBoxLayout() self.groupBox.setLayout(self.groupBoxLayout) self.mainLayout.addWidget(self.groupBox) self.buttSudo = QCheckBox() self.buttSudo.setText("sudo") self.groupBoxLayout.addWidget(self.buttSudo, 0) self.buttSudo.toggled.connect(self.update_cmd_line) self.buttSudo.setVisible(os.name != "nt") self.txtCommand = QLineEdit() self.groupBoxLayout.addWidget(self.txtCommand, 10) self.buttExecute = QPushButton() self.buttExecute.setText("Execute") self.groupBoxLayout.addWidget(self.buttExecute, 0) self.buttExecute.clicked.connect(self.on_butt_execute) self.txtStdOut = QPlainTextEdit() self.mainLayout.addWidget(self.txtStdOut) self.txtStdErr = QPlainTextEdit() self.mainLayout.addWidget(self.txtStdErr) self.update_cmd_line()
[docs] def update_cmd_line(self, *unused): """Update the commend line considering sudo button state""" cmd = "" if self.buttSudo.isChecked(): cmd += "pkexec " cmd += "pip3 install {modulename}".format(modulename=self.module.name) self.txtCommand.setText(cmd)
[docs] def on_butt_execute(self): """Execute button event handler""" self.buttSudo.setDisabled(True) self.buttExecute.setDisabled(True) self.txtStdOut.setPlainText("") self.txtStdErr.setPlainText("") self.process.start(self.txtCommand.text())
[docs] def on_read_standard(self): """Stdout read event handler""" msg_tpl = "{}\n{}\n{}" msg = msg_tpl.format(self.txtStdOut.toPlainText(), self.line_str, self.process.readAllStandardOutput()) self.txtStdOut.setPlainText(msg) self.txtStdOut.moveCursor(QTextCursor.End)
[docs] def on_read_error(self): """Stderr read event handler""" msg_tpl = "{}\n{}\n{}" msg = msg_tpl.format(self.txtStdErr.toPlainText(), self.line_str, self.process.readAllStandardError()) self.txtStdErr.setPlainText(msg) self.txtStdErr.moveCursor(QTextCursor.End)
[docs] def on_finished(self): """Execution finished event handler""" self.buttSudo.setDisabled(False) self.buttExecute.setDisabled(False)