class AddonManagerDialog(QDialog): """ A add-on manager dialog. """ #: cached packages list. __packages = None # type: List[Installable] __f_pypi_addons = None __config = None def __init__(self, parent=None, acceptDrops=True, **kwargs): super().__init__(parent, acceptDrops=acceptDrops, **kwargs) self.setLayout(QVBoxLayout()) self.addonwidget = AddonManagerWidget() self.addonwidget.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.addonwidget) buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel, ) addmore = QPushButton( "Add more...", toolTip="Add an add-on not listed below", autoDefault=False ) self.addonwidget.tophlayout.addWidget(addmore) addmore.clicked.connect(self.__run_add_package_dialog) buttons.accepted.connect(self.__accepted) buttons.rejected.connect(self.reject) self.layout().addWidget(buttons) self.__progress = None # type: Optional[QProgressDialog] self.__executor = ThreadPoolExecutor(max_workers=1) # The installer thread self.__thread = None # The installer object self.__installer = None self.__add_package_by_name_dialog = None # type: Optional[QDialog] def setConfig(self, config): self.__config = config def config(self): if self.__config is None: return config.default else: return self.__config @Slot() def start(self, config): # type: (config.Config) -> None """ Initialize the dialog/manager for the specified configuration namespace. Calling this method will start an async query of ... At the end the found items will be set using `setItems` overriding any previously set items. Parameters ---------- config : config.Config """ self.__config = config if self.__packages is not None: # method_queued(self.setItems, (object,))(self.__packages) installed = [ep.dist for ep in config.addon_entry_points()] items = installable_items(self.__packages, installed) self.setItems(items) return progress = self.progressDialog() self.show() progress.show() progress.setLabelText( self.tr("Retrieving package list") ) self.__f_pypi_addons = self.__executor.submit( lambda config=config: (config, list_available_versions(config)), ) self.__f_pypi_addons.add_done_callback( method_queued(self.__on_query_done, (object,)) ) @Slot(object) def __on_query_done(self, f): # type: (Future[Tuple[config.Config, List[Installable]]]) -> None assert f.done() if self.__progress is not None: self.__progress.hide() if f.exception(): exc = f.exception() etype, tb = type(exc), exc.__traceback__ log.error( "Error fetching package list", exc_info=(etype, exc, tb) ) message_warning( "Could not retrieve package list", title="Error", informative_text= "".join(traceback.format_exception_only(etype, exc)), details= "".join(traceback.format_exception(etype, exc, tb)), parent=self ) self.__f_pypi_addons = None self.__addon_items = None return config, packages = f.result() assert all(isinstance(p, Installable) for p in packages) AddonManagerDialog.__packages = packages installed = [ep.dist for ep in config.addon_entry_points()] items = installable_items(packages, installed) core_constraints = { r.project_name.casefold(): r for r in (Requirement.parse(r) for r in config.core_packages()) } def f(item): # type: (Item) -> Item """Include constraint in Installed when in core_constraint""" if isinstance(item, Installed): name = item.local.project_name.casefold() if name in core_constraints: return item._replace( required=True, constraint=core_constraints[name] ) return item self.setItems([f(item) for item in items]) @Slot(object) def setItems(self, items): # type: (List[Item]) -> None """ Set items Parameters ---------- items: List[Items] """ self.addonwidget.setItems(items) @Slot(object) def addInstallable(self, installable): # type: (Installable) -> None """ Add/append a single Installable item. Parameters ---------- installable: Installable """ items = self.addonwidget.items() if installable.name in {item.installable.name for item in items if item.installable is not None}: return installed = [ep.dist for ep in self.config().addon_entry_points()] new_ = installable_items([installable], installed) new = next( filter( lambda item: item.installable.name == installable.name, new_ ), None ) state = self.addonwidget.itemState() self.addonwidget.setItems(items + [new]) self.addonwidget.setItemState(state) # restore state def __run_add_package_dialog(self): self.__add_package_by_name_dialog = dlg = QDialog( self, windowTitle="Add add-on by name", ) dlg.setAttribute(Qt.WA_DeleteOnClose) vlayout = QVBoxLayout() form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) nameentry = QLineEdit( placeholderText="Package name", toolTip="Enter a package name as displayed on " "PyPI (capitalization is not important)") nameentry.setMinimumWidth(250) form.addRow("Name:", nameentry) vlayout.addLayout(form) buttons = QDialogButtonBox( standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) okb = buttons.button(QDialogButtonBox.Ok) okb.setEnabled(False) okb.setText("Add") def changed(name): okb.setEnabled(bool(name)) nameentry.textChanged.connect(changed) vlayout.addWidget(buttons) vlayout.setSizeConstraint(QVBoxLayout.SetFixedSize) dlg.setLayout(vlayout) f = None def query(): nonlocal f name = nameentry.text() def query_pypi(name): # type: (str) -> _QueryResult res = pypi_json_query_project_meta([name]) assert len(res) == 1 r = res[0] if r is not None: r = installable_from_json_response(r) return _QueryResult(queryname=name, installable=r) f = self.__executor.submit(query_pypi, name) okb.setDisabled(True) f.add_done_callback( method_queued(self.__on_add_single_query_finish, (object,)) ) buttons.accepted.connect(query) buttons.rejected.connect(dlg.reject) dlg.exec_() @Slot(str, str) def __show_error_for_query(self, text, error_details): message_error(text, title="Error", details=error_details) @Slot(object) def __on_add_single_query_finish(self, f): # type: (Future[_QueryResult]) -> None error_text = "" error_details = "" try: result = f.result() except Exception: log.error("Query error:", exc_info=True) error_text = "Failed to query package index" error_details = traceback.format_exc() pkg = None else: pkg = result.installable if pkg is None: error_text = "'{}' not was not found".format(result.queryname) dlg = self.__add_package_by_name_dialog if pkg: self.addInstallable(pkg) dlg.accept() else: dlg.reject() self.__show_error_for_query(error_text, error_details) def progressDialog(self): # type: () -> QProgressDialog if self.__progress is None: self.__progress = QProgressDialog( self, minimum=0, maximum=0, labelText=self.tr("Retrieving package list"), sizeGripEnabled=False, windowTitle="Progress" ) self.__progress.setWindowModality(Qt.WindowModal) self.__progress.hide() self.__progress.canceled.connect(self.reject) return self.__progress def done(self, retcode): super().done(retcode) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) def closeEvent(self, event): super().closeEvent(event) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) ADDON_EXTENSIONS = ('.zip', '.whl', '.tar.gz') def dragEnterEvent(self, event): # type: (QDragEnterEvent) -> None """Reimplemented.""" urls = event.mimeData().urls() if any(url.toLocalFile().endswith(self.ADDON_EXTENSIONS) for url in urls): event.acceptProposedAction() def dropEvent(self, event): # type: (QDropEvent) -> None """ Reimplemented. Allow dropping add-ons (zip or wheel archives) on this dialog to install them. """ packages = [] names = [] for url in event.mimeData().urls(): path = url.toLocalFile() if path.endswith(self.ADDON_EXTENSIONS): name, vers, summary, descr = (get_meta_from_archive(path) or (os.path.basename(path), '', '', '')) names.append(name) packages.append( Installable(name, vers, summary, descr or summary, path, [path])) for installable in packages: self.addInstallable(installable) items = self.addonwidget.items() # lookup items for the new entries new_items = [item for item in items if item.installable in packages] state_new = [(Install, item) if isinstance(item, Available) else (Upgrade, item) for item in new_items] state = self.addonwidget.itemState() self.addonwidget.setItemState(state + state_new) event.acceptProposedAction() def __accepted(self): steps = self.addonwidget.itemState() if steps: # Move all uninstall steps to the front steps = sorted( steps, key=lambda step: 0 if step[0] == Uninstall else 1 ) self.__installer = Installer(steps=steps) self.__thread = QThread(self) self.__thread.start() self.__installer.moveToThread(self.__thread) self.__installer.finished.connect(self.__on_installer_finished) self.__installer.error.connect(self.__on_installer_error) progress = self.progressDialog() self.__installer.installStatusChanged.connect(progress.setLabelText) progress.show() progress.setLabelText("Installing") self.__installer.start() else: self.accept() def __on_installer_error(self, command, pkg, retcode, output): if self.__progress is not None: self.__progress.close() self.__progress = None message_error( "An error occurred while running a subprocess", title="Error", informative_text="{} exited with non zero status.".format(command), details="".join(output), parent=self ) self.reject() def __on_installer_finished(self): if self.__progress is not None: self.__progress.close() self.__progress = None def message_restart(parent): icon = QMessageBox.Information buttons = QMessageBox.Ok | QMessageBox.Cancel title = 'Information' text = 'Orange needs to be restarted for the changes to take effect.' msg_box = QMessageBox(icon, title, text, buttons, parent) msg_box.setDefaultButton(QMessageBox.Ok) msg_box.setInformativeText('Press OK to close Orange now.') msg_box.button(QMessageBox.Cancel).setText('Close later') return msg_box.exec_() if QMessageBox.Ok == message_restart(self): self.accept() self.parent().close() else: self.reject()
class AddonManagerDialog(QDialog): _packages = None def __init__(self, parent=None, **kwargs): super().__init__(parent, acceptDrops=True, **kwargs) self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.addonwidget = AddonManagerWidget() self.layout().addWidget(self.addonwidget) info_bar = QWidget() info_layout = QHBoxLayout() info_bar.setLayout(info_layout) self.layout().addWidget(info_bar) buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) buttons.accepted.connect(self.__accepted) buttons.rejected.connect(self.reject) self.layout().addWidget(buttons) self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) if AddonManagerDialog._packages is None: self._f_pypi_addons = self._executor.submit(list_pypi_addons) else: self._f_pypi_addons = concurrent.futures.Future() self._f_pypi_addons.set_result(AddonManagerDialog._packages) self._f_pypi_addons.add_done_callback( method_queued(self._set_packages, (object,)) ) self.__progress = None # type: Optional[QProgressDialog] self.__thread = None self.__installer = None if not self._f_pypi_addons.done(): self.__progressDialog() def __progressDialog(self): if self.__progress is None: self.__progress = QProgressDialog( self, minimum=0, maximum=0, labelText=self.tr("Retrieving package list"), sizeGripEnabled=False, windowTitle="Progress", ) self.__progress.setWindowModality(Qt.WindowModal) self.__progress.canceled.connect(self.reject) self.__progress.hide() return self.__progress @Slot(object) def _set_packages(self, f): if self.__progress is not None: self.__progress.hide() self.__progress.deleteLater() self.__progress = None try: packages = f.result() except (IOError, OSError, ValueError) as err: message_warning( "Could not retrieve package list", title="Error", informative_text=str(err), parent=self ) log.error(str(err), exc_info=True) packages = [] except Exception: raise else: AddonManagerDialog._packages = packages installed = list_installed_addons() dists = {dist.project_name: dist for dist in installed} packages = {pkg.name: pkg for pkg in packages} # For every pypi available distribution not listed by # list_installed_addons, check if it is actually already # installed. ws = pkg_resources.WorkingSet() for pkg_name in set(packages.keys()).difference(set(dists.keys())): try: d = ws.find(pkg_resources.Requirement.parse(pkg_name)) except pkg_resources.VersionConflict: pass except ValueError: # Requirements.parse error ? pass else: if d is not None: dists[d.project_name] = d project_names = unique( itertools.chain(packages.keys(), dists.keys()) ) items = [] for name in project_names: if name in dists and name in packages: item = Installed(packages[name], dists[name]) elif name in dists: item = Installed(None, dists[name]) elif name in packages: item = Available(packages[name]) else: assert False items.append(item) self.addonwidget.set_items(items) def showEvent(self, event): super().showEvent(event) if not self._f_pypi_addons.done() and self.__progress is not None: QTimer.singleShot(0, self.__progress.show) def done(self, retcode): super().done(retcode) self._f_pypi_addons.cancel() self._executor.shutdown(wait=False) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) def closeEvent(self, event): super().closeEvent(event) if self.__progress is not None: self.__progress.hide() self._f_pypi_addons.cancel() self._executor.shutdown(wait=False) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) ADDON_EXTENSIONS = ('.zip', '.whl', '.tar.gz') def dragEnterEvent(self, event): urls = event.mimeData().urls() if any((OSX_NSURL_toLocalFile(url) or url.toLocalFile()) .endswith(self.ADDON_EXTENSIONS) for url in urls): event.acceptProposedAction() def dropEvent(self, event): """Allow dropping add-ons (zip or wheel archives) on this dialog to install them""" packages = [] names = [] for url in event.mimeData().urls(): path = OSX_NSURL_toLocalFile(url) or url.toLocalFile() if path.endswith(self.ADDON_EXTENSIONS): name, vers, summary, descr = (get_meta_from_archive(path) or (os.path.basename(path), '', '', '')) names.append(cleanup(name)) packages.append( Installable(name, vers, summary, descr or summary, path, [path])) future = concurrent.futures.Future() future.set_result((AddonManagerDialog._packages or []) + packages) self._set_packages(future) self.addonwidget.set_install_projects(names) def __accepted(self): steps = self.addonwidget.item_state() if steps: # Move all uninstall steps to the front steps = sorted( steps, key=lambda step: 0 if step[0] == Uninstall else 1 ) self.__installer = Installer(steps=steps) self.__thread = QThread(self) self.__thread.start() self.__installer.moveToThread(self.__thread) self.__installer.finished.connect(self.__on_installer_finished) self.__installer.error.connect(self.__on_installer_error) progress = self.__progressDialog() self.__installer.installStatusChanged.connect(progress.setLabelText) progress.show() progress.setLabelText("Installing") self.__installer.start() else: self.accept() def __on_installer_error(self, command, pkg, retcode, output): message_error( "An error occurred while running a subprocess", title="Error", informative_text="{} exited with non zero status.".format(command), details="".join(output), parent=self ) self.reject() def __on_installer_finished(self): message = "Please restart Orange for changes to take effect." message_information(message, parent=self) self.accept()
class AddonManagerDialog(QDialog): _packages = None def __init__(self, parent=None, **kwargs): super().__init__(parent, acceptDrops=True, **kwargs) self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.addonwidget = AddonManagerWidget() self.layout().addWidget(self.addonwidget) info_bar = QWidget() info_layout = QHBoxLayout() info_bar.setLayout(info_layout) self.layout().addWidget(info_bar) buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel, ) addmore = QPushButton("Add more...", toolTip="Add an add-on not listed below", autoDefault=False) self.addonwidget.tophlayout.addWidget(addmore) addmore.clicked.connect(self.__run_add_package_dialog) buttons.accepted.connect(self.__accepted) buttons.rejected.connect(self.reject) self.layout().addWidget(buttons) self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) if AddonManagerDialog._packages is None: self._f_pypi_addons = self._executor.submit( list_available_versions) else: self._f_pypi_addons = concurrent.futures.Future() self._f_pypi_addons.set_result(AddonManagerDialog._packages) self._f_pypi_addons.add_done_callback( method_queued(self._set_packages, (object, ))) self.__progress = None # type: Optional[QProgressDialog] self.__thread = None self.__installer = None if not self._f_pypi_addons.done(): self.__progressDialog() def __run_add_package_dialog(self): dlg = QDialog(self, windowTitle="Add add-on by name") dlg.setAttribute(Qt.WA_DeleteOnClose) vlayout = QVBoxLayout() form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) nameentry = QLineEdit(placeholderText="Package name", toolTip="Enter a package name as displayed on " "PyPI (capitalization is not important)") nameentry.setMinimumWidth(250) form.addRow("Name:", nameentry) vlayout.addLayout(form) buttons = QDialogButtonBox(standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel) okb = buttons.button(QDialogButtonBox.Ok) okb.setEnabled(False) okb.setText("Add") def changed(name): okb.setEnabled(bool(name)) nameentry.textChanged.connect(changed) vlayout.addWidget(buttons) vlayout.setSizeConstraint(QVBoxLayout.SetFixedSize) dlg.setLayout(vlayout) f = None def query(): nonlocal f name = nameentry.text() f = self._executor.submit(pypi_json_query_project_meta, [name]) okb.setDisabled(True) def ondone(f): error_text = "" error_details = "" try: pkgs = f.result() except Exception: log.error("Query error:", exc_info=True) error_text = "Failed to query package index" error_details = traceback.format_exc() pkg = None else: pkg = pkgs[0] if pkg is None: error_text = "'{}' not was not found".format(name) if pkg: method_queued(self.add_package, (object, ))(pkg) method_queued(dlg.accept, ())() else: method_queued(self.__show_error_for_query, (str, str)) \ (error_text, error_details) method_queued(dlg.reject, ())() f.add_done_callback(ondone) buttons.accepted.connect(query) buttons.rejected.connect(dlg.reject) dlg.exec_() @Slot(str, str) def __show_error_for_query(self, text, error_details): message_error(text, title="Error", details=error_details) @Slot(object) def add_package(self, installable): # type: (Installable) -> None if installable.name in {p.name for p in self._packages}: return else: packages = self._packages + [installable] state = self.addonwidget.item_state() self.set_packages(packages) self.addonwidget.set_item_state(state) def __progressDialog(self): if self.__progress is None: self.__progress = QProgressDialog( self, minimum=0, maximum=0, labelText=self.tr("Retrieving package list"), sizeGripEnabled=False, windowTitle="Progress", ) self.__progress.setWindowModality(Qt.WindowModal) self.__progress.canceled.connect(self.reject) self.__progress.hide() return self.__progress @Slot(object) def _set_packages(self, f): if self.__progress is not None: self.__progress.hide() self.__progress.deleteLater() self.__progress = None try: packages = f.result() except Exception as err: message_warning("Could not retrieve package list", title="Error", informative_text=str(err), parent=self) log.error(str(err), exc_info=True) packages = [] else: AddonManagerDialog._packages = packages self.set_packages(packages) @Slot(object) def set_packages(self, installable): # type: (List[Installable]) -> None self._packages = packages = installable # type: List[Installable] installed = list_installed_addons() dists = {dist.project_name: dist for dist in installed} packages = {pkg.name: pkg for pkg in packages} # For every pypi available distribution not listed by # list_installed_addons, check if it is actually already # installed. ws = pkg_resources.WorkingSet() for pkg_name in set(packages.keys()).difference(set(dists.keys())): try: d = ws.find(pkg_resources.Requirement.parse(pkg_name)) except pkg_resources.VersionConflict: pass except ValueError: # Requirements.parse error ? pass else: if d is not None: dists[d.project_name] = d project_names = unique(itertools.chain(packages.keys(), dists.keys())) items = [] for name in project_names: if name in dists and name in packages: item = Installed(packages[name], dists[name]) elif name in dists: item = Installed(None, dists[name]) elif name in packages: item = Available(packages[name]) else: assert False items.append(item) self.addonwidget.set_items(items) def showEvent(self, event): super().showEvent(event) if not self._f_pypi_addons.done() and self.__progress is not None: QTimer.singleShot(0, self.__progress.show) def done(self, retcode): super().done(retcode) self._f_pypi_addons.cancel() self._executor.shutdown(wait=False) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) def closeEvent(self, event): super().closeEvent(event) if self.__progress is not None: self.__progress.hide() self._f_pypi_addons.cancel() self._executor.shutdown(wait=False) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) ADDON_EXTENSIONS = ('.zip', '.whl', '.tar.gz') def dragEnterEvent(self, event): urls = event.mimeData().urls() if any((OSX_NSURL_toLocalFile(url) or url.toLocalFile() ).endswith(self.ADDON_EXTENSIONS) for url in urls): event.acceptProposedAction() def dropEvent(self, event): """Allow dropping add-ons (zip or wheel archives) on this dialog to install them""" packages = [] names = [] for url in event.mimeData().urls(): path = OSX_NSURL_toLocalFile(url) or url.toLocalFile() if path.endswith(self.ADDON_EXTENSIONS): name, vers, summary, descr = (get_meta_from_archive(path) or (os.path.basename(path), '', '', '')) names.append(cleanup(name)) packages.append( Installable(name, vers, summary, descr or summary, path, [path])) if packages: state = self.addonwidget.item_state() self.set_packages((self._packages or []) + packages) items = self.addonwidget.items() # mark for installation the added packages for item in items: if item.installable in packages: if isinstance(item, Available): state.append((Install, item)) elif isinstance(item, Installed) and is_updatable(item): state.append((Upgrade, item)) self.addonwidget.set_item_state(state) def __accepted(self): steps = self.addonwidget.item_state() if steps: # Move all uninstall steps to the front steps = sorted(steps, key=lambda step: 0 if step[0] == Uninstall else 1) self.__installer = Installer(steps=steps) self.__thread = QThread(self) self.__thread.start() self.__installer.moveToThread(self.__thread) self.__installer.finished.connect(self.__on_installer_finished) self.__installer.error.connect(self.__on_installer_error) progress = self.__progressDialog() self.__installer.installStatusChanged.connect( progress.setLabelText) progress.show() progress.setLabelText("Installing") self.__installer.start() else: self.accept() def __on_installer_error(self, command, pkg, retcode, output): message_error( "An error occurred while running a subprocess", title="Error", informative_text="{} exited with non zero status.".format(command), details="".join(output), parent=self) self.reject() def __on_installer_finished(self): message = "Please restart Orange for changes to take effect." message_information(message, parent=self) self.accept()
class AddonManagerDialog(QDialog): _packages = None def __init__(self, parent=None, **kwargs): super().__init__(parent, acceptDrops=True, **kwargs) self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.addonwidget = AddonManagerWidget() self.layout().addWidget(self.addonwidget) info_bar = QWidget() info_layout = QHBoxLayout() info_bar.setLayout(info_layout) self.layout().addWidget(info_bar) buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel, ) addmore = QPushButton( "Add more...", toolTip="Add an add-on not listed below", autoDefault=False ) self.addonwidget.tophlayout.addWidget(addmore) addmore.clicked.connect(self.__run_add_package_dialog) buttons.accepted.connect(self.__accepted) buttons.rejected.connect(self.reject) self.layout().addWidget(buttons) self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) if AddonManagerDialog._packages is None: self._f_pypi_addons = self._executor.submit(list_available_versions) else: self._f_pypi_addons = concurrent.futures.Future() self._f_pypi_addons.set_result(AddonManagerDialog._packages) self._f_pypi_addons.add_done_callback( method_queued(self._set_packages, (object,)) ) self.__progress = None # type: Optional[QProgressDialog] self.__thread = None self.__installer = None if not self._f_pypi_addons.done(): self.__progressDialog() def __run_add_package_dialog(self): dlg = QDialog(self, windowTitle="Add add-on by name") dlg.setAttribute(Qt.WA_DeleteOnClose) vlayout = QVBoxLayout() form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) nameentry = QLineEdit( placeholderText="Package name", toolTip="Enter a package name as displayed on " "PyPI (capitalization is not important)") nameentry.setMinimumWidth(250) form.addRow("Name:", nameentry) vlayout.addLayout(form) buttons = QDialogButtonBox( standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) okb = buttons.button(QDialogButtonBox.Ok) okb.setEnabled(False) okb.setText("Add") def changed(name): okb.setEnabled(bool(name)) nameentry.textChanged.connect(changed) vlayout.addWidget(buttons) vlayout.setSizeConstraint(QVBoxLayout.SetFixedSize) dlg.setLayout(vlayout) f = None def query(): nonlocal f name = nameentry.text() f = self._executor.submit(pypi_json_query_project_meta, [name]) okb.setDisabled(True) def ondone(f): error_text = "" error_details = "" try: pkgs = f.result() except Exception: log.error("Query error:", exc_info=True) error_text = "Failed to query package index" error_details = traceback.format_exc() pkg = None else: pkg = pkgs[0] if pkg is None: error_text = "'{}' not was not found".format(name) if pkg: method_queued(self.add_package, (object,))(pkg) method_queued(dlg.accept, ())() else: method_queued(self.__show_error_for_query, (str, str)) \ (error_text, error_details) method_queued(dlg.reject, ())() f.add_done_callback(ondone) buttons.accepted.connect(query) buttons.rejected.connect(dlg.reject) dlg.exec_() @Slot(str, str) def __show_error_for_query(self, text, error_details): message_error(text, title="Error", details=error_details) @Slot(object) def add_package(self, installable): # type: (Installable) -> None if installable.name in {p.name for p in self._packages}: return else: packages = self._packages + [installable] state = self.addonwidget.item_state() self.set_packages(packages) self.addonwidget.set_item_state(state) def __progressDialog(self): if self.__progress is None: self.__progress = QProgressDialog( self, minimum=0, maximum=0, labelText=self.tr("Retrieving package list"), sizeGripEnabled=False, windowTitle="Progress", ) self.__progress.setWindowModality(Qt.WindowModal) self.__progress.canceled.connect(self.reject) self.__progress.hide() return self.__progress @Slot(object) def _set_packages(self, f): if self.__progress is not None: self.__progress.hide() self.__progress.deleteLater() self.__progress = None try: packages = f.result() except Exception as err: message_warning( "Could not retrieve package list", title="Error", informative_text=str(err), parent=self ) log.error(str(err), exc_info=True) packages = [] else: AddonManagerDialog._packages = packages self.set_packages(packages) @Slot(object) def set_packages(self, installable): # type: (List[Installable]) -> None self._packages = packages = installable # type: List[Installable] installed = list_installed_addons() dists = {dist.project_name: dist for dist in installed} packages = {pkg.name: pkg for pkg in packages} # For every pypi available distribution not listed by # list_installed_addons, check if it is actually already # installed. ws = pkg_resources.WorkingSet() for pkg_name in set(packages.keys()).difference(set(dists.keys())): try: d = ws.find(pkg_resources.Requirement.parse(pkg_name)) except pkg_resources.VersionConflict: pass except ValueError: # Requirements.parse error ? pass else: if d is not None: dists[d.project_name] = d project_names = unique( itertools.chain(packages.keys(), dists.keys()) ) items = [] for name in project_names: if name in dists and name in packages: item = Installed(packages[name], dists[name]) elif name in dists: item = Installed(None, dists[name]) elif name in packages: item = Available(packages[name]) else: assert False items.append(item) self.addonwidget.set_items(items) def showEvent(self, event): super().showEvent(event) if not self._f_pypi_addons.done() and self.__progress is not None: QTimer.singleShot(0, self.__progress.show) def done(self, retcode): super().done(retcode) self._f_pypi_addons.cancel() self._executor.shutdown(wait=False) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) def closeEvent(self, event): super().closeEvent(event) if self.__progress is not None: self.__progress.hide() self._f_pypi_addons.cancel() self._executor.shutdown(wait=False) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) ADDON_EXTENSIONS = ('.zip', '.whl', '.tar.gz') def dragEnterEvent(self, event): urls = event.mimeData().urls() if any((OSX_NSURL_toLocalFile(url) or url.toLocalFile()) .endswith(self.ADDON_EXTENSIONS) for url in urls): event.acceptProposedAction() def dropEvent(self, event): """Allow dropping add-ons (zip or wheel archives) on this dialog to install them""" packages = [] names = [] for url in event.mimeData().urls(): path = OSX_NSURL_toLocalFile(url) or url.toLocalFile() if path.endswith(self.ADDON_EXTENSIONS): name, vers, summary, descr = (get_meta_from_archive(path) or (os.path.basename(path), '', '', '')) names.append(cleanup(name)) packages.append( Installable(name, vers, summary, descr or summary, path, [path])) if packages: state = self.addonwidget.item_state() self.set_packages((self._packages or []) + packages) items = self.addonwidget.items() # mark for installation the added packages for item in items: if item.installable in packages: if isinstance(item, Available): state.append((Install, item)) elif isinstance(item, Installed) and is_updatable(item): state.append((Upgrade, item)) self.addonwidget.set_item_state(state) def __accepted(self): steps = self.addonwidget.item_state() if steps: # Move all uninstall steps to the front steps = sorted( steps, key=lambda step: 0 if step[0] == Uninstall else 1 ) self.__installer = Installer(steps=steps) self.__thread = QThread(self) self.__thread.start() self.__installer.moveToThread(self.__thread) self.__installer.finished.connect(self.__on_installer_finished) self.__installer.error.connect(self.__on_installer_error) progress = self.__progressDialog() self.__installer.installStatusChanged.connect(progress.setLabelText) progress.show() progress.setLabelText("Installing") self.__installer.start() else: self.accept() def __on_installer_error(self, command, pkg, retcode, output): message_error( "An error occurred while running a subprocess", title="Error", informative_text="{} exited with non zero status.".format(command), details="".join(output), parent=self ) self.reject() def __on_installer_finished(self): def message_restart(parent): icon = QMessageBox.Information buttons = QMessageBox.Ok | QMessageBox.Cancel title = 'Information' text = 'Orange needs to be restarted for the changes to take effect.' msg_box = QMessageBox(icon, title, text, buttons, parent) msg_box.setDefaultButton(QMessageBox.Ok) msg_box.setInformativeText('Press OK to close Orange now.') msg_box.button(QMessageBox.Cancel).setText('Close later') return msg_box.exec_() if QMessageBox.Ok == message_restart(self): self.accept() self.parent().close() else: self.reject()
class AddonManagerDialog(QDialog): def __init__(self, parent=None, **kwargs): super(AddonManagerDialog, self).__init__(parent, **kwargs) self.setLayout(QVBoxLayout()) self.addonwidget = AddonManagerWidget() self.addonwidget.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.addonwidget) buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) buttons.accepted.connect(self.__accepted) buttons.rejected.connect(self.reject) self.layout().addWidget(buttons) self.__progress = None # type: QProgressDialog # The installer thread self.__thread = None # The installer object self.__installer = None @Slot(object) def setItems(self, items): self.addonwidget.setItems(items) def progressDialog(self): if self.__progress is None: self.__progress = QProgressDialog( self, minimum=0, maximum=0, labelText=self.tr("Retrieving package list"), sizeGripEnabled=False, windowTitle="Progress" ) self.__progress.setWindowModality(Qt.WindowModal) self.__progress.hide() self.__progress.canceled.connect(self.reject) return self.__progress def done(self, retcode): super(AddonManagerDialog, self).done(retcode) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) def closeEvent(self, event): super(AddonManagerDialog, self).closeEvent(event) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) def __accepted(self): steps = self.addonwidget.itemState() if steps: # Move all uninstall steps to the front steps = sorted( steps, key=lambda step: 0 if step[0] == Uninstall else 1 ) self.__installer = Installer(steps=steps) self.__thread = QThread(self) self.__thread.start() self.__installer.moveToThread(self.__thread) self.__installer.finished.connect(self.__on_installer_finished) self.__installer.error.connect(self.__on_installer_error) progress = self.progressDialog() self.__installer.installStatusChanged.connect(progress.setLabelText) progress.show() progress.setLabelText("Installing") self.__installer.start() else: self.accept() def __on_installer_error(self, command, pkg, retcode, output): message_error( "An error occurred while running a subprocess", title="Error", informative_text="{} exited with non zero status.".format(command), details="".join(output), parent=self ) self.reject() def __on_installer_finished(self): message_information( "Please restart the application for changes to take effect.", parent=self) self.accept()
class AddonManagerDialog(QDialog): """ A add-on manager dialog. """ #: cached packages list. __packages = None # type: List[Installable] __f_pypi_addons = None __config = None # type: Optional[Config] def __init__(self, parent=None, acceptDrops=True, **kwargs): super().__init__(parent, acceptDrops=acceptDrops, **kwargs) self.setLayout(QVBoxLayout()) self.addonwidget = AddonManagerWidget() self.addonwidget.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.addonwidget) buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel, ) addmore = QPushButton( "Add more...", toolTip="Add an add-on not listed below", autoDefault=False ) self.addonwidget.tophlayout.addWidget(addmore) addmore.clicked.connect(self.__run_add_package_dialog) buttons.accepted.connect(self.__accepted) buttons.rejected.connect(self.reject) self.layout().addWidget(buttons) self.__progress = None # type: Optional[QProgressDialog] self.__executor = ThreadPoolExecutor(max_workers=1) # The installer thread self.__thread = None # The installer object self.__installer = None self.__add_package_by_name_dialog = None # type: Optional[QDialog] def setConfig(self, config): self.__config = config def config(self): # type: () -> Config if self.__config is None: return config.default else: return self.__config @Slot() def start(self, config): # type: (Config) -> None """ Initialize the dialog/manager for the specified configuration namespace. Calling this method will start an async query of ... At the end the found items will be set using `setItems` overriding any previously set items. Parameters ---------- config : config.Config """ self.__config = config if self.__packages is not None: # method_queued(self.setItems, (object,))(self.__packages) installed = [ep.dist for ep in config.addon_entry_points() if ep.dist is not None] items = installable_items(self.__packages, installed) self.setItems(items) return progress = self.progressDialog() self.show() progress.show() progress.setLabelText( self.tr("Retrieving package list") ) self.__f_pypi_addons = self.__executor.submit( lambda config=config: (config, list_available_versions(config)), ) self.__f_pypi_addons.add_done_callback( method_queued(self.__on_query_done, (object,)) ) @Slot(object) def __on_query_done(self, f): # type: (Future[Tuple[Config, List[Installable]]]) -> None assert f.done() if self.__progress is not None: self.__progress.hide() if f.exception() is not None: exc = typing.cast(BaseException, f.exception()) etype, tb = type(exc), exc.__traceback__ log.error( "Error fetching package list", exc_info=(etype, exc, tb) ) message_warning( "Could not retrieve package list", title="Error", informative_text= "".join(traceback.format_exception_only(etype, exc)), details= "".join(traceback.format_exception(etype, exc, tb)), parent=self ) self.__f_pypi_addons = None self.__addon_items = None return config, packages = f.result() assert all(isinstance(p, Installable) for p in packages) AddonManagerDialog.__packages = packages installed = [ep.dist for ep in config.addon_entry_points() if ep.dist is not None] items = installable_items(packages, installed) core_constraints = { r.project_name.casefold(): r for r in (Requirement.parse(r) for r in config.core_packages()) } def constrain(item): # type: (Item) -> Item """Include constraint in Installed when in core_constraint""" if isinstance(item, Installed): name = item.local.project_name.casefold() if name in core_constraints: return item._replace( required=True, constraint=core_constraints[name] ) return item self.setItems([constrain(item) for item in items]) @Slot(object) def setItems(self, items): # type: (List[Item]) -> None """ Set items Parameters ---------- items: List[Items] """ self.addonwidget.setItems(items) @Slot(object) def addInstallable(self, installable): # type: (Installable) -> None """ Add/append a single Installable item. Parameters ---------- installable: Installable """ items = self.addonwidget.items() if installable.name in {item.installable.name for item in items if item.installable is not None}: return installed = [ep.dist for ep in self.config().addon_entry_points()] new_ = installable_items([installable], filter(None, installed)) def match(item): # type: (Item) -> bool if isinstance(item, Available): return item.installable.name == installable.name elif item.installable is not None: return item.installable.name == installable.name else: return item.local.project_name.lower() == installable.name.lower() new = next(filter(match, new_), None) assert new is not None state = self.addonwidget.itemState() self.addonwidget.setItems(items + [new]) self.addonwidget.setItemState(state) # restore state def __run_add_package_dialog(self): self.__add_package_by_name_dialog = dlg = QDialog( self, windowTitle="Add add-on by name", ) dlg.setAttribute(Qt.WA_DeleteOnClose) vlayout = QVBoxLayout() form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) nameentry = QLineEdit( placeholderText="Package name", toolTip="Enter a package name as displayed on " "PyPI (capitalization is not important)") nameentry.setMinimumWidth(250) form.addRow("Name:", nameentry) vlayout.addLayout(form) buttons = QDialogButtonBox( standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) okb = buttons.button(QDialogButtonBox.Ok) okb.setEnabled(False) okb.setText("Add") def changed(name): okb.setEnabled(bool(name)) nameentry.textChanged.connect(changed) vlayout.addWidget(buttons) vlayout.setSizeConstraint(QVBoxLayout.SetFixedSize) dlg.setLayout(vlayout) f = None def query(): nonlocal f name = nameentry.text() def query_pypi(name): # type: (str) -> _QueryResult res, = pypi_json_query_project_meta([name]) inst = None # type: Optional[Installable] if res is not None: inst = installable_from_json_response(res) else: inst = None return _QueryResult(queryname=name, installable=inst) f = self.__executor.submit(query_pypi, name) okb.setDisabled(True) f.add_done_callback( method_queued(self.__on_add_single_query_finish, (object,)) ) buttons.accepted.connect(query) buttons.rejected.connect(dlg.reject) dlg.exec_() @Slot(str, str) def __show_error_for_query(self, text, error_details): message_error(text, title="Error", details=error_details) @Slot(object) def __on_add_single_query_finish(self, f): # type: (Future[_QueryResult]) -> None error_text = "" error_details = "" try: result = f.result() except Exception: log.error("Query error:", exc_info=True) error_text = "Failed to query package index" error_details = traceback.format_exc() pkg = None else: pkg = result.installable if pkg is None: error_text = "'{}' not was not found".format(result.queryname) dlg = self.__add_package_by_name_dialog assert dlg is not None if pkg: self.addInstallable(pkg) dlg.accept() else: dlg.reject() self.__show_error_for_query(error_text, error_details) def progressDialog(self): # type: () -> QProgressDialog if self.__progress is None: self.__progress = QProgressDialog( self, minimum=0, maximum=0, labelText=self.tr("Retrieving package list"), sizeGripEnabled=False, windowTitle="Progress" ) self.__progress.setWindowModality(Qt.WindowModal) self.__progress.hide() self.__progress.canceled.connect(self.reject) return self.__progress def done(self, retcode): super().done(retcode) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) def closeEvent(self, event): super().closeEvent(event) if self.__thread is not None: self.__thread.quit() self.__thread.wait(1000) ADDON_EXTENSIONS = ('.zip', '.whl', '.tar.gz') def dragEnterEvent(self, event): # type: (QDragEnterEvent) -> None """Reimplemented.""" urls = event.mimeData().urls() if any(url.toLocalFile().endswith(self.ADDON_EXTENSIONS) for url in urls): event.acceptProposedAction() def dropEvent(self, event): # type: (QDropEvent) -> None """ Reimplemented. Allow dropping add-ons (zip or wheel archives) on this dialog to install them. """ packages = [] names = [] for url in event.mimeData().urls(): path = url.toLocalFile() if path.endswith(self.ADDON_EXTENSIONS): meta = get_meta_from_archive(path) or {} name = meta.get("Name", os.path.basename(path)) vers = meta.get("Version", "") summary = meta.get("Summary", "") descr = meta.get("Description", "") content_type = meta.get("Description-Content-Type", None) names.append(name) packages.append( Installable(name, vers, summary, descr or summary, path, [path], content_type) ) for installable in packages: self.addInstallable(installable) items = self.addonwidget.items() # lookup items for the new entries new_items = [item for item in items if item.installable in packages] state_new = [(Install, item) if isinstance(item, Available) else (Upgrade, item) for item in new_items] state = self.addonwidget.itemState() self.addonwidget.setItemState(state + state_new) event.acceptProposedAction() def __accepted(self): steps = self.addonwidget.itemState() if steps: # Move all uninstall steps to the front steps = sorted( steps, key=lambda step: 0 if step[0] == Uninstall else 1 ) self.__installer = Installer(steps=steps) self.__thread = QThread(self) self.__thread.start() self.__installer.moveToThread(self.__thread) self.__installer.finished.connect(self.__on_installer_finished) self.__installer.error.connect(self.__on_installer_error) progress = self.progressDialog() self.__installer.installStatusChanged.connect(progress.setLabelText) progress.show() progress.setLabelText("Installing") self.__installer.start() else: self.accept() def __on_installer_error(self, command, pkg, retcode, output): if self.__progress is not None: self.__progress.close() self.__progress = None message_error( "An error occurred while running a subprocess", title="Error", informative_text="{} exited with non zero status.".format(command), details="".join(output), parent=self ) self.reject() def __on_installer_finished(self): if self.__progress is not None: self.__progress.close() self.__progress = None def message_restart(parent): icon = QMessageBox.Information buttons = QMessageBox.Ok | QMessageBox.Cancel title = 'Information' text = 'Orange needs to be restarted for the changes to take effect.' msg_box = QMessageBox(icon, title, text, buttons, parent) msg_box.setDefaultButton(QMessageBox.Ok) msg_box.setInformativeText('Press OK to close Orange now.') msg_box.button(QMessageBox.Cancel).setText('Close later') return msg_box.exec_() if QMessageBox.Ok == message_restart(self): self.accept() QApplication.closeAllWindows() else: self.reject()