# -*- coding: utf-8 -*-
#
# Copyright 2012 Pierre Raybaut
# Licensed under the terms of the MIT License
# (see winpython/__init__.py for details)
"""
WinPython Package Manager GUI
Created on Mon Aug 13 11:40:01 2012
"""
from pathlib import Path
import os
import sys
import platform
import locale
# winpython.qt becomes winpython._vendor.qtpy
from winpython._vendor.qtpy.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QLineEdit,
QHBoxLayout,
QVBoxLayout,
QMessageBox,
QAbstractItemView,
QProgressDialog,
QTableView,
QPushButton,
QLabel,
QTabWidget,
QToolTip,
)
from winpython._vendor.qtpy.QtGui import (
QColor,
QDesktopServices,
)
from winpython._vendor.qtpy.QtCore import (
Qt,
QAbstractTableModel,
QModelIndex,
Signal,
QThread,
QTimer,
QUrl,
)
from winpython._vendor.qtpy.compat import (
to_qvariant,
getopenfilenames,
getexistingdirectory,
)
import winpython._vendor.qtpy
from winpython.qthelpers import (
get_icon,
add_actions,
create_action,
keybinding,
get_std_icon,
action2button,
mimedata2url,
)
# Local imports
from winpython import __version__, __project_url__
from winpython import wppm, associate, utils
COLUMNS = ACTION, CHECK, NAME, VERSION, DESCRIPTION = list(
range(5)
)
class PackagesModel(QAbstractTableModel):
# Signals after PyQt4 old SIGNAL removal
dataChanged = Signal(QModelIndex, QModelIndex)
def __init__(self):
QAbstractTableModel.__init__(self)
self.packages = []
self.checked = set()
self.actions = {}
def sortByName(self):
self.packages = sorted(
self.packages, key=lambda x: x.name
)
self.reset()
def flags(self, index):
if not index.isValid():
return Qt.ItemIsEnabled
column = index.column()
if column in (NAME, VERSION, ACTION, DESCRIPTION):
return Qt.ItemFlags(
QAbstractTableModel.flags(self, index)
)
else:
return Qt.ItemFlags(
QAbstractTableModel.flags(self, index)
| Qt.ItemIsUserCheckable
| Qt.ItemIsEditable
)
def data(self, index, role=Qt.DisplayRole):
if not index.isValid() or not (
0 0
)
nbp = len(self.table.get_selected_packages())
for act in (
self.remove_action,
self.select_all_action,
):
act.setEnabled(nbp > 0)
self.show_drop_tip()
def show_drop_tip(self):
"""Show drop tip on install table"""
callback = lambda: QToolTip.showText(
self.table.mapToGlobal(self.table.pos()),
'Drop files here
'
'Executable installers (distutils) or source packages',
self,
)
QTimer.singleShot(500, callback)
def refresh_uninstall_button(self):
"""Refresh uninstall button enable state"""
nbp = len(self.untable.get_selected_packages())
self.uninstall_action.setEnabled(nbp > 0)
def toggle_repair(self, state):
"""Toggle repair mode"""
self.table.repair = state
self.refresh_install_button()
def register_distribution(self):
"""Register distribution"""
answer = QMessageBox.warning(
self,
"Register distribution",
"(experimental)\n"
"This will associate file extensions, icons and "
"Windows explorer's context menu entries ('Edit with IDLE', ...) "
"with selected Python distribution in Windows registry. "
"\n\nShortcuts for all WinPython launchers will be installed "
"in WinPython Start menu group (replacing existing "
"shortcuts)."
"\n\nNote: these actions are similar to those performed"
"when installing old Pythons with the official installer before 'py' "
"for Windows.\n\nDo you want to continue? ",
QMessageBox.Yes | QMessageBox.No,
)
if answer == QMessageBox.Yes:
associate.register(self.distribution.target)
def unregister_distribution(self):
"""Unregister distribution"""
answer = QMessageBox.warning(
self,
"Unregister distribution",
"(experimental)\n"
"This will remove file extensions associations, icons and "
"Windows explorer's context menu entries ('Edit with IDLE', ...) "
"with selected Python distribution in Windows registry. "
"\n\nShortcuts for all WinPython launchers will be removed "
"from WinPython Start menu group."
"\n\nDo you want to continue? ",
QMessageBox.Yes | QMessageBox.No,
)
if answer == QMessageBox.Yes:
associate.unregister(self.distribution.target)
@property
def command_prompt_path(self):
return str(Path(self.distribution.target).parent /
"WinPython Command Prompt.exe")
def distribution_changed(self, path):
"""Distribution path has just changed"""
for package in self.table.model.packages:
self.table.remove_package(package)
# dist = wppm.Distribution(to_text_string(path))
dist = wppm.Distribution(str(path))
self.table.refresh_distribution(dist)
self.untable.refresh_distribution(dist)
self.distribution = dist
self.selector.label.setText(
f'Python {dist.version} {dist.architecture}bit:'
)
def add_packages(self):
"""Add packages"""
basedir = (
self.basedir if self.basedir is not None else ''
)
fnames, _selfilter = getopenfilenames(
parent=self,
basedir=basedir,
caption='Add packages',
filters='*.exe *.zip *.tar.gz *.whl',
)
if fnames:
self.basedir = str(Path(fnames[0]).parent)
self.table.add_packages(fnames)
def get_packages_to_be_installed(self):
"""Return packages to be installed"""
return [
pack
for pack in self.table.get_selected_packages()
if self.table.model.actions[pack]
not in (NO_REPAIR_ACTION, NONE_ACTION)
]
def remove_packages(self):
"""Remove selected packages"""
for package in self.table.get_selected_packages():
self.table.remove_package(package)
def process_packages(self, action):
"""Install/uninstall packages"""
if action == 'install':
text, table = 'Installing', self.table
if not self.get_packages_to_be_installed():
return
elif action == 'uninstall':
text, table = 'Uninstalling', self.untable
else:
raise AssertionError
packages = table.get_selected_packages()
if not packages:
return
func = getattr(self.distribution, action)
thread = Thread(self)
for widget in self.children():
if isinstance(widget, QWidget):
widget.setEnabled(False)
try:
status = self.statusBar()
except AttributeError:
status = self.parent().statusBar()
progress = QProgressDialog(
self, Qt.FramelessWindowHint
)
progress.setMaximum(
len(packages)
) # old vicious bug:len(packages)-1
for index, package in enumerate(packages):
progress.setValue(index)
progress.setLabelText(
f"{text} {package.name} {package.version}..."
)
QApplication.processEvents()
if progress.wasCanceled():
break
if package in table.model.actions:
try:
thread.callback = lambda: func(package)
thread.start()
while thread.isRunning():
QApplication.processEvents()
if progress.wasCanceled():
status.setEnabled(True)
status.showMessage(
"Cancelling operation..."
)
table.remove_package(package)
error = thread.error
except Exception as error:
error = str(error) # to_text_string(error)
if error is not None:
pstr = (
package.name + ' ' + package.version
)
QMessageBox.critical(
self,
"Error",
f"Unable to {action} {pstr}/i>"
f"
Error message:
{error}"
,
)
progress.setValue(progress.maximum())
status.clearMessage()
for widget in self.children():
if isinstance(widget, QWidget):
widget.setEnabled(True)
thread = None
for table in (self.table, self.untable):
table.refresh_distribution(self.distribution)
def report_issue(self):
issue_template = f"""\
Python distribution: {python_distribution_infos()}
Control panel version: {__version__}
Python Version: {platform.python_version()}
Qt Version: {winpython._vendor.qtpy.QtCore.__version__}, {winpython.qt.API_NAME} {winpython._vendor.qtpy.__version__}
What steps will reproduce the problem?
1.
2.
3.
What is the expected output? What do you see instead?
Please provide any additional information below.
"""
url = QUrl(f"{__project_url__}/issues/entry")
url.addQueryItem("comment", issue_template)
QDesktopServices.openUrl(url)
def about(self):
"""About this program"""
QMessageBox.about(
self,
f"About {self.NAME}",
f"""{self.NAME} {__version__}
Package Manager and Advanced Tasks
Copyright © 2012 Pierre Raybaut
Licensed under the terms of the MIT License
Created, developed and maintained by Pierre Raybaut
WinPython at Github.io: downloads, bug reports,
discussions, etc.
This program is executed by:
{python_distribution_infos()}
Python {platform.python_version()}, Qt {winpython._vendor.qtpy.QtCore.__version__}, {winpython._vendor.qtpy.API_NAME} qtpy {winpython._vendor.qtpy.__version__}"""
,
)
def main(test=False):
app = QApplication([])
win = PMWindow()
win.show()
if test:
return app, win
else:
app.exec_()
def test():
app, win = main(test=True)
print(sys.modules)
app.exec_()
if __name__ == "__main__":
main()