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