# -*- 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)