# -*- 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
"""
import os.path as osp
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
from winpython.py3compat import getcwd, to_text_string
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 <= index.row() < len(self.packages)):
return to_qvariant()
package = self.packages[index.row()]
column = index.column()
if role == Qt.CheckStateRole and column == CHECK:
return to_qvariant(package in self.checked)
elif role == Qt.DisplayRole:
if column == NAME:
return to_qvariant(package.name)
elif column == VERSION:
return to_qvariant(package.version)
elif column == ACTION:
action = self.actions.get(package)
if action is not None:
return to_qvariant(action)
elif column == DESCRIPTION:
return to_qvariant(package.description)
elif role == Qt.TextAlignmentRole:
if column == ACTION:
return to_qvariant(int(Qt.AlignRight | Qt.AlignVCenter))
else:
return to_qvariant(int(Qt.AlignLeft | Qt.AlignVCenter))
elif role == Qt.BackgroundColorRole:
if package in self.checked:
color = QColor(Qt.darkGreen)
color.setAlphaF(.1)
return to_qvariant(color)
else:
color = QColor(Qt.lightGray)
color.setAlphaF(.3)
return to_qvariant(color)
return to_qvariant()
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.TextAlignmentRole:
if orientation == Qt.Horizontal:
return to_qvariant(int(Qt.AlignHCenter | Qt.AlignVCenter))
return to_qvariant(int(Qt.AlignRight | Qt.AlignVCenter))
if role != Qt.DisplayRole:
return to_qvariant()
if orientation == Qt.Horizontal:
if section == NAME:
return to_qvariant("Name")
elif section == VERSION:
return to_qvariant("Version")
elif section == ACTION:
return to_qvariant("Action")
elif section == DESCRIPTION:
return to_qvariant("Description")
return to_qvariant()
def rowCount(self, index=QModelIndex()):
return len(self.packages)
def columnCount(self, index=QModelIndex()):
return len(COLUMNS)
def setData(self, index, value, role=Qt.EditRole):
if index.isValid() and 0 <= index.row() < len(self.packages)\
and role == Qt.CheckStateRole:
package = self.packages[index.row()]
if package in self.checked:
self.checked.remove(package)
else:
self.checked.add(package)
# PyQt4 old SIGNAL: self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"),
# PyQt4 old SIGNAL: index, index)
self.dataChanged.emit(index, index)
return True
return False
INSTALL_ACTION = 'Install'
REPAIR_ACTION = 'Repair (reinstall)'
NO_REPAIR_ACTION = 'None (Already installed)'
UPGRADE_ACTION = 'Upgrade from v'
NONE_ACTION = '-'
class PackagesTable(QTableView):
# Signals after PyQt4 old SIGNAL removal, to be emitted after package_added event
package_added = Signal()
def __init__(self, parent, process, winname):
QTableView.__init__(self, parent)
assert process in ('install', 'uninstall')
self.process = process
self.model = PackagesModel()
self.setModel(self.model)
self.winname = winname
self.repair = False
self.resizeColumnToContents(0)
self.setAcceptDrops(process == 'install')
if process == 'uninstall':
self.hideColumn(0)
self.distribution = None
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.verticalHeader().hide()
self.setShowGrid(False)
def reset_model(self):
# self.model.reset() is deprecated in Qt5
self.model.beginResetModel()
self.model.endResetModel()
self.horizontalHeader().setStretchLastSection(True)
for colnb in (ACTION, CHECK, NAME, VERSION):
self.resizeColumnToContents(colnb)
def get_selected_packages(self):
"""Return selected packages"""
return [pack for pack in self.model.packages
if pack in self.model.checked]
def add_packages(self, fnames):
"""Add packages"""
notsupported = []
notcompatible = []
dist = self.distribution
for fname in fnames:
bname = osp.basename(fname)
try:
package = wppm.Package(fname)
if package.is_compatible_with(dist):
self.add_package(package)
else:
notcompatible.append(bname)
except NotImplementedError:
notsupported.append(bname)
# PyQt4 old SIGNAL: self.emit(SIGNAL('package_added()'))
self.package_added.emit()
if notsupported:
QMessageBox.warning(self, "Warning",
"The following packages filenaming are not "
"recognized by %s:\n\n%s"
% (self.winname, "
".join(notsupported)),
QMessageBox.Ok)
if notcompatible:
QMessageBox.warning(self, "Warning", "The following packages "
"are not compatible with "
"Python %s %dbit:\n\n%s"
% (dist.version, dist.architecture,
"
".join(notcompatible)),
QMessageBox.Ok)
def add_package(self, package):
for pack in self.model.packages:
if pack.name == package.name:
return
self.model.packages.append(package)
self.model.packages.sort(key=lambda x: x.name)
self.model.checked.add(package)
self.reset_model()
def remove_package(self, package):
self.model.packages = [pack for pack in self.model.packages
if pack.fname != package.fname]
if package in self.model.checked:
self.model.checked.remove(package)
if package in self.model.actions:
self.model.actions.pop(package)
self.reset_model()
def refresh_distribution(self, dist):
self.distribution = dist
if self.process == 'install':
for package in self.model.packages:
pack = dist.find_package(package.name)
if pack is None:
action = INSTALL_ACTION
elif pack.version == package.version:
if self.repair:
action = REPAIR_ACTION
else:
action = NO_REPAIR_ACTION
else:
action = UPGRADE_ACTION + pack.version
self.model.actions[package] = action
else:
self.model.packages = self.distribution.get_installed_packages()
for package in self.model.packages:
self.model.actions[package] = NONE_ACTION
self.reset_model()
def select_all(self):
allpk = set(self.model.packages)
if self.model.checked == allpk:
self.model.checked = set()
else:
self.model.checked = allpk
self.model.reset()
def dragMoveEvent(self, event):
"""Reimplement Qt method, just to avoid default drag'n drop
implementation of QTableView to handle events"""
event.acceptProposedAction()
def dragEnterEvent(self, event):
"""Reimplement Qt method
Inform Qt about the types of data that the widget accepts"""
source = event.mimeData()
if source.hasUrls() and mimedata2url(source):
event.acceptProposedAction()
def dropEvent(self, event):
"""Reimplement Qt method
Unpack dropped data and handle it"""
source = event.mimeData()
fnames = [path for path in mimedata2url(source) if osp.isfile(path)]
self.add_packages(fnames)
event.acceptProposedAction()
class DistributionSelector(QWidget):
"""Python distribution selector widget"""
TITLE = 'Select a Python distribution path'
# Signals after PyQt4 old SIGNAL removal
selected_distribution = Signal(str)
def __init__(self, parent):
super(DistributionSelector, self).__init__(parent)
self.browse_btn = None
self.label = None
self.line_edit = None
self.setup_widget()
def set_distribution(self, path):
"""Set distribution directory"""
self.line_edit.setText(path)
def setup_widget(self):
"""Setup workspace selector widget"""
self.label = QLabel()
self.line_edit = QLineEdit()
self.line_edit.setAlignment(Qt.AlignRight)
self.line_edit.setReadOnly(True)
# self.line_edit.setDisabled(True)
self.browse_btn = QPushButton(get_std_icon('DirOpenIcon'), "", self)
self.browse_btn.setToolTip(self.TITLE)
# PyQt4 old SIGNAL:self.connect(self.browse_btn, SIGNAL("clicked()"),
# PyQt4 old SIGNAL: self.select_directory)
self.browse_btn.clicked.connect(self.select_directory)
layout = QHBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.line_edit)
layout.addWidget(self.browse_btn)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
def select_directory(self):
"""Select directory"""
basedir = to_text_string(self.line_edit.text())
if not osp.isdir(basedir):
basedir = getcwd()
while True:
directory = getexistingdirectory(self, self.TITLE, basedir)
if not directory:
break
if not utils.is_python_distribution(directory):
QMessageBox.warning(self, self.TITLE,
"The following directory is not a Python distribution.",
QMessageBox.Ok)
basedir = directory
continue
directory = osp.abspath(osp.normpath(directory))
self.set_distribution(directory)
# PyQt4 old SIGNAL: self.emit(SIGNAL('selected_distribution(QString)'), directory)
self.selected_distribution.emit(directory)
break
class Thread(QThread):
"""Installation/Uninstallation thread"""
def __init__(self, parent):
QThread.__init__(self, parent)
self.callback = None
self.error = None
def run(self):
try:
self.callback()
except Exception as error:
error_str = str(error)
fs_encoding = sys.getfilesystemencoding()\
or locale.getpreferredencoding()
try:
error_str = error_str.decode(fs_encoding)
except (UnicodeError, TypeError, AttributeError):
pass
self.error = error_str
def python_distribution_infos():
"""Return Python distribution infos (not selected distribution but
the one used to run this script)"""
winpyver = os.environ.get('WINPYVER')
if winpyver is None:
return 'Unknown Python distribution'
else:
return 'WinPython ' + winpyver
class PMWindow(QMainWindow):
NAME = 'WinPython Control Panel'
def __init__(self):
QMainWindow.__init__(self)
self.setAttribute(Qt.WA_DeleteOnClose)
self.distribution = None
self.tabwidget = None
self.selector = None
self.table = None
self.untable = None
self.basedir = None
self.select_all_action = None
self.install_action = None
self.uninstall_action = None
self.remove_action = None
self.packages_icon = get_std_icon('FileDialogContentsView')
self.setup_window()
def _add_table(self, table, title, icon):
"""Add table tab to main tab widget, return button layout"""
widget = QWidget()
tabvlayout = QVBoxLayout()
widget.setLayout(tabvlayout)
tabvlayout.addWidget(table)
btn_layout = QHBoxLayout()
tabvlayout.addLayout(btn_layout)
self.tabwidget.addTab(widget, icon, title)
return btn_layout
def setup_window(self):
"""Setup main window"""
self.setWindowTitle(self.NAME)
self.setWindowIcon(get_icon('winpython.svg'))
self.selector = DistributionSelector(self)
# PyQt4 old SIGNAL: self.connect(self.selector, SIGNAL('selected_distribution(QString)'),
# PyQt4 old SIGNAL: self.distribution_changed)
self.selector.selected_distribution.connect(self.distribution_changed)
self.table = PackagesTable(self, 'install', self.NAME)
# PyQt4 old SIGNAL:self.connect(self.table, SIGNAL('package_added()'),
# PyQt4 old SIGNAL: self.refresh_install_button)
self.table.package_added.connect(self.refresh_install_button)
# PyQt4 old SIGNAL: self.connect(self.table, SIGNAL("clicked(QModelIndex)"),
# PyQt4 old SIGNAL: lambda index: self.refresh_install_button())
self.table.clicked.connect(lambda index: self.refresh_install_button())
self.untable = PackagesTable(self, 'uninstall', self.NAME)
# PyQt4 old SIGNAL:self.connect(self.untable, SIGNAL("clicked(QModelIndex)"),
# PyQt4 old SIGNAL: lambda index: self.refresh_uninstall_button())
self.untable.clicked.connect(lambda index: self.refresh_uninstall_button())
self.selector.set_distribution(sys.prefix)
self.distribution_changed(sys.prefix)
self.tabwidget = QTabWidget()
# PyQt4 old SIGNAL:self.connect(self.tabwidget, SIGNAL('currentChanged(int)'),
# PyQt4 old SIGNAL: self.current_tab_changed)
self.tabwidget.currentChanged.connect(self.current_tab_changed)
btn_layout = self._add_table(self.table, "Install/upgrade packages",
get_std_icon("ArrowDown"))
unbtn_layout = self._add_table(self.untable, "Uninstall packages",
get_std_icon("DialogResetButton"))
central_widget = QWidget()
vlayout = QVBoxLayout()
vlayout.addWidget(self.selector)
vlayout.addWidget(self.tabwidget)
central_widget.setLayout(vlayout)
self.setCentralWidget(central_widget)
# Install tab
add_action = create_action(self, "&Add packages...",
icon=get_std_icon('DialogOpenButton'),
triggered=self.add_packages)
self.remove_action = create_action(self, "Remove",
shortcut=keybinding('Delete'),
icon=get_std_icon('TrashIcon'),
triggered=self.remove_packages)
self.remove_action.setEnabled(False)
self.select_all_action = create_action(self, "(Un)Select all",
shortcut=keybinding('SelectAll'),
icon=get_std_icon('DialogYesButton'),
triggered=self.table.select_all)
self.install_action = create_action(self, "&Install packages",
icon=get_std_icon('DialogApplyButton'),
triggered=lambda: self.process_packages('install'))
self.install_action.setEnabled(False)
quit_action = create_action(self, "&Quit",
icon=get_std_icon('DialogCloseButton'),
triggered=self.close)
packages_menu = self.menuBar().addMenu("&Packages")
add_actions(packages_menu, [add_action, self.remove_action,
self.install_action,
None, quit_action])
# Uninstall tab
self.uninstall_action = create_action(self, "&Uninstall packages",
icon=get_std_icon('DialogCancelButton'),
triggered=lambda: self.process_packages('uninstall'))
self.uninstall_action.setEnabled(False)
uninstall_btn = action2button(self.uninstall_action, autoraise=False,
text_beside_icon=True)
# Option menu
option_menu = self.menuBar().addMenu("&Options")
repair_action = create_action(self, "Repair packages",
tip="Reinstall packages even if version is unchanged",
toggled=self.toggle_repair)
add_actions(option_menu, (repair_action,))
# Advanced menu
option_menu = self.menuBar().addMenu("&Advanced")
register_action = create_action(self, "Register distribution...",
tip="Register file extensions, icons and context menu",
triggered=self.register_distribution)
unregister_action = create_action(self, "Unregister distribution...",
tip="Unregister file extensions, icons and context menu",
triggered=self.unregister_distribution)
open_console_action = create_action(self, "Open console here",
triggered=lambda: os.startfile(self.command_prompt_path))
open_console_action.setEnabled(osp.exists(self.command_prompt_path))
add_actions(option_menu, (register_action, unregister_action,
None, open_console_action))
# # View menu
# view_menu = self.menuBar().addMenu("&View")
# popmenu = self.createPopupMenu()
# add_actions(view_menu, popmenu.actions())
# Help menu
about_action = create_action(self, "About %s..." % self.NAME,
icon=get_std_icon('MessageBoxInformation'),
triggered=self.about)
report_action = create_action(self, "Report issue...",
icon=get_icon('bug.png'),
triggered=self.report_issue)
help_menu = self.menuBar().addMenu("?")
add_actions(help_menu, [about_action, None, report_action])
# Status bar
status = self.statusBar()
status.setObjectName("StatusBar")
status.showMessage("Welcome to %s!" % self.NAME, 5000)
# Button layouts
for act in (add_action, self.remove_action, None,
self.select_all_action, self.install_action):
if act is None:
btn_layout.addStretch()
else:
btn_layout.addWidget(action2button(act, autoraise=False,
text_beside_icon=True))
unbtn_layout.addWidget(uninstall_btn)
unbtn_layout.addStretch()
self.resize(400, 500)
def current_tab_changed(self, index):
"""Current tab has just changed"""
if index == 0:
self.show_drop_tip()
def refresh_install_button(self):
"""Refresh install button enable state"""
self.table.refresh_distribution(self.distribution)
self.install_action.setEnabled(
len(self.get_packages_to_be_installed()) > 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",
"This will associate file extensions, icons and "
"Windows explorer's context menu entries ('Edit with IDLE', ...) "
"with selected Python distribution in Windows registry. "
"
Shortcuts for all WinPython launchers will be installed "
"in WinPython Start menu group (replacing existing "
"shortcuts)."
"
If pywin32 is installed (it should be on any "
"WinPython distribution), the Python ActiveX Scripting client "
"will also be registered."
"
Warning: the only way to undo this change is to "
"register another Python distribution to Windows registry."
"
Note: these actions are exactly the same as those "
"performed when installing Python with the official installer "
"for Windows.
Do 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",
"This will remove file extensions associations, icons and "
"Windows explorer's context menu entries ('Edit with IDLE', ...) "
"with selected Python distribution in Windows registry. "
"
Shortcuts for all WinPython launchers will be removed "
"from WinPython Start menu group."
"
If pywin32 is installed (it should be on any "
"WinPython distribution), the Python ActiveX Scripting client "
"will also be unregistered."
"
Do you want to continue?",
QMessageBox.Yes | QMessageBox.No)
if answer == QMessageBox.Yes:
associate.unregister(self.distribution.target)
@property
def command_prompt_path(self):
return osp.join(self.distribution.target, osp.pardir,
"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))
self.table.refresh_distribution(dist)
self.untable.refresh_distribution(dist)
self.distribution = dist
self.selector.label.setText('Python %s %dbit:'
% (dist.version, dist.architecture))
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 = osp.dirname(fnames[0])
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("%s %s %s..."
% (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 = to_text_string(error)
if error is not None:
pstr = package.name + ' ' + package.version
QMessageBox.critical(self, "Error",
"Unable to %s %s"
"
Error message:
%s"
% (action, pstr, 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 = """\
Python distribution: %s
Control panel version: %s
Python Version: %s
Qt Version: %s, %s %s
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.
""" % (python_distribution_infos(),
__version__, platform.python_version(),
winpython._vendor.qtpy.QtCore.__version__, winpython.qt.API_NAME,
winpython._vendor.qtpy.__version__)
url = QUrl("%s/issues/entry" % __project_url__)
url.addQueryItem("comment", issue_template)
QDesktopServices.openUrl(url)
def about(self):
"""About this program"""
QMessageBox.about(self,
"About %s" % self.NAME,
"""%s %s
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:
%s
Python %s, Qt %s, %s qtpy %s"""
% (self.NAME, __version__, __project_url__,
python_distribution_infos(),
platform.python_version(), winpython._vendor.qtpy.QtCore.__version__,
winpython._vendor.qtpy.API_NAME, 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()