class ProjectsWidget(WidgetBase): """Main projects widget.""" sig_saved = Signal() sig_login_requested = Signal() def __init__(self, *args, **kwargs): super(ProjectsWidget, self).__init__(*args, **kwargs) self.api = AnacondaAPI() self.timer = None self.timer_content_changed = QTimer() self.project_path = None self.original_content = None self.config = CONF self.timer = None # Widgets self.frame_projects_header = FrameProjectDetailsHeader() self.frame_projects_footer = FrameProjectDetailsFooter() self.button_upload = ButtonPrimary('Upload to Anaconda Cloud') self.button_cancel = ButtonDanger('Cancel') self.label_project_location = LabelProjectLocation( '<b>Project location</b>') self.label_status_message = LabelBase('') self.text_project_location = TextProjectLocation() self.tab_details = QTabWidget() self.file_explorer = ExplorerWidget() self.editor = ProjectEditor(parent=self) # Wigets setup tabbar = self.tab_details.tabBar() tabbar.setFocusPolicy(Qt.StrongFocus) self.tab_details.addTab(self.file_explorer, 'Files') self.tab_details.addTab(self.editor, 'Edit') self.timer_content_changed.setInterval(2000) self.timer_content_changed.timeout.connect(self.check_content_change) self.timer_content_changed.start() # Layouts layout_upload = QHBoxLayout() layout_upload.addWidget(SpacerHorizontal()) layout_upload.addWidget(SpacerHorizontal()) layout_upload.addWidget(self.label_status_message) layout_upload.addStretch() layout_upload.addWidget(self.button_cancel) layout_upload.addWidget(SpacerHorizontal()) layout_upload.addWidget(self.button_upload) layout_upload.addWidget(SpacerHorizontal()) layout_upload.addWidget(SpacerHorizontal()) layout_footer = QVBoxLayout() layout_footer.addWidget(SpacerVertical()) layout_footer.addWidget(self.tab_details) layout_footer.addLayout(layout_upload) layout_footer.addWidget(SpacerVertical()) layout_footer.addWidget(SpacerVertical()) self.frame_projects_footer.setLayout(layout_footer) layout = QVBoxLayout() layout.addWidget(self.frame_projects_footer) self.setLayout(layout) # Signals self.editor.sig_dirty_state.connect(self.set_dirty) self.editor.sig_saved.connect(self.save) self.button_upload.clicked.connect(self.upload) self.button_cancel.clicked.connect(self.cancel) self.file_explorer.sig_add_to_project.connect(self.add_to_project) self.button_cancel.setVisible(False) self.file_explorer.set_current_folder(HOME_PATH) def add_to_project(self, fname): """Add selected file to project.""" file_path = os.path.join( self.project_path, os.path.basename(fname), ) try: shutil.copyfile(fname, file_path) except Exception: pass def check_content_change(self): """Check if content of anaconda-project changed outside.""" if self.project_path: project_config_path = os.path.join(self.project_path, 'anaconda-project.yml') if os.path.isfile(project_config_path): current_content = self.editor.text() with open(project_config_path, 'r') as f: data = f.read() if (current_content != data and data != self.original_content): self.load_project(self.project_path) def set_dirty(self, state): """Set dirty state editor tab.""" text = 'Edit*' if state else 'Edit' self.tab_details.setTabText(1, text) def before_delete(self): """Before deleting a folder, ensure it is not the same as the cwd.""" self.file_explorer.set_current_folder(HOME_PATH) def clear(self): """Reset view for proect details.""" self.text_project_location.setText('') self.editor.set_text('') def cancel(self): """Cancel ongoing project process.""" # TODO: when running project. Cancel ongoing process! self.button_cancel.setVisible(False) self.button_upload.setEnabled(True) def _upload(self, worker, output, error): """Upload callback.""" error = error if error else '' errors = [] if output is not None: errors = output.errors # print(output.status_description) # print(output.logs) # print(errors) if error or errors: if errors: error_msg = error or '\n'.join(errors) elif error: error_msg = 'Upload failed!' self.update_status(error_msg) else: self.update_status('Project <b>{0}</b> upload successful'.format( worker.name)) self.timer = QTimer() self.timer.setSingleShot(True) self.timer.setInterval(10000) self.timer.timeout.connect(lambda: self.update_status('')) self.timer.start() self.button_upload.setEnabled(True) self.button_cancel.setVisible(False) def update_status(self, message): """Update Status Bar message.""" self.label_status_message.setText(message) def upload(self): """Upload project to Anaconda Cloud.""" # Check if is logged in? if not self.api.is_logged_in(): self.update_status('You need to log in to Anaconda Cloud') self.sig_login_requested.emit() self.update_status('') return project = self.api.project_load(self.project_path) name = project.name or os.path.basename(self.project_path) # Check if saved? if self.editor.is_dirty(): self.update_status('Saving project <b>{0}</b>'.format( project.name)) self.editor.save() project = self.api.project_load(self.project_path) if not project.problems: username, token = self.api.get_username_token() self.button_cancel.setVisible(True) worker = self.api.project_upload( project, username=username, token=token, ) worker.sig_finished.connect(self._upload) worker.name = project.name self.button_upload.setEnabled(False) msg = 'Uploading project <b>{0}</b> to Anaconda Cloud.'.format( project.name) self.update_status(msg) else: self.update_status( 'Problems must be fixed before uploading <b>{0}</b>' ''.format(name)) def save(self): """Save current edited project.""" project_config_path = os.path.join(self.project_path, 'anaconda-project.yml') data = self.editor.text() if os.path.isfile(project_config_path): with open(project_config_path, 'w') as f: data = f.write(data) self.load_project(self.project_path, overwrite=False) self.sig_saved.emit() def load_project(self, project_path, overwrite=True): """Load a conda project located at path.""" self.project_path = project_path project = self.api.project_load(project_path) self.project = project self.text_project_location.setText(project_path) self.file_explorer.set_current_folder(project_path) project_config_path = os.path.join(project_path, 'anaconda-project.yml') data = '' if os.path.isfile(project_config_path): with open(project_config_path, 'r') as f: data = f.read() self.original_content = data if overwrite: self.editor.set_text(data) self.set_dirty(False) self.file_explorer.set_home(project_path) self.update_error_status(project) self.update_status('') def ordered_widgets(self): """Return a list of the ordered widgets.""" tabbar = self.tab_details.tabBar() ordered_widgets = [tabbar] ordered_widgets += self.file_explorer.ordered_widgets() ordered_widgets += self.editor.ordered_widgets() ordered_widgets += [self.button_upload] return ordered_widgets def update_error_status(self, project): """Update problems and suggestions.""" if project: problems = project.problems suggestions = project.suggestions if problems or suggestions: icon = QIcon(WARNING_ICON) self.tab_details.setTabIcon(1, icon) else: self.tab_details.setTabIcon(1, QIcon()) self.editor.set_info(problems, suggestions)
class PackagesDialog(DialogBase): """Package dependencies dialog.""" sig_setup_ready = Signal() def __init__( self, parent=None, packages=None, pip_packages=None, remove_only=False, update_only=False, ): """About dialog.""" super(PackagesDialog, self).__init__(parent=parent) # Variables self.api = AnacondaAPI() self.actions = None self.packages = packages or [] self.pip_packages = pip_packages or [] # Widgets self.stack = QStackedWidget() self.table = QTableWidget() self.text = QTextEdit() self.label_description = LabelBase() self.label_status = LabelBase() self.progress_bar = QProgressBar() self.button_ok = ButtonPrimary('Apply') self.button_cancel = ButtonNormal('Cancel') # Widget setup self.text.setReadOnly(True) self.stack.addWidget(self.table) self.stack.addWidget(self.text) if remove_only: text = 'The following packages will be removed:<br>' else: text = 'The following packages will be modified:<br>' self.label_description.setText(text) self.label_description.setWordWrap(True) self.label_description.setWordWrap(True) self.label_status.setWordWrap(True) self.table.horizontalScrollBar().setVisible(False) self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setAlternatingRowColors(True) self.table.setSelectionMode(QAbstractItemView.NoSelection) self.table.setSortingEnabled(True) self._hheader = self.table.horizontalHeader() self._vheader = self.table.verticalHeader() self._hheader.setStretchLastSection(True) self._hheader.setDefaultAlignment(Qt.AlignLeft) self._hheader.setSectionResizeMode(self._hheader.Fixed) self._vheader.setSectionResizeMode(self._vheader.Fixed) self.button_ok.setMinimumWidth(70) self.button_ok.setDefault(True) self.base_minimum_width = 300 if remove_only else 420 if remove_only: self.setWindowTitle("Remove Packages") elif update_only: self.setWindowTitle("Update Packages") else: self.setWindowTitle("Install Packages") self.setMinimumWidth(self.base_minimum_width) # Layouts layout_progress = QHBoxLayout() layout_progress.addWidget(self.label_status) layout_progress.addWidget(SpacerHorizontal()) layout_progress.addWidget(self.progress_bar) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) layout = QVBoxLayout() layout.addWidget(self.label_description) layout.addWidget(SpacerVertical()) layout.addWidget(self.stack) layout.addWidget(SpacerVertical()) layout.addLayout(layout_progress) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_buttons) self.setLayout(layout) # Signals self.button_ok.clicked.connect(self.accept) self.button_cancel.clicked.connect(self.reject) self.button_ok.setDisabled(True) # Setup self.table.setDisabled(True) self.update_status('Solving package specifications', value=0, max_value=0) def setup(self, worker, output, error): """Setup the widget to include the list of dependencies.""" if not isinstance(output, dict): output = {} packages = sorted(pkg.split('==')[0] for pkg in self.packages) success = output.get('success') error = output.get('error', '') exception_name = output.get('exception_name', '') actions = output.get('actions', []) prefix = worker.prefix if exception_name: message = exception_name else: # All requested packages already installed message = output.get('message', ' ') navi_deps_error = self.api.check_navigator_dependencies( actions, prefix) description = self.label_description.text() if error: description = 'No packages will be modified.' self.stack.setCurrentIndex(1) self.button_ok.setDisabled(True) if self.api.is_offline(): error = ("Some of the functionality of Anaconda Navigator " "will be limited in <b>offline mode</b>. <br><br>" "Installation and upgrade actions will be subject to " "the packages currently available on your package " "cache.") self.text.setText(error) elif navi_deps_error: description = 'No packages will be modified.' error = ('Downgrading/removing these packages will modify ' 'Anaconda Navigator dependencies.') self.text.setText(error) self.stack.setCurrentIndex(1) message = 'NavigatorDependenciesError' self.button_ok.setDisabled(True) elif success and actions: self.stack.setCurrentIndex(0) # Conda 4.3.x if isinstance(actions, list): actions_link = actions[0].get('LINK', []) actions_unlink = actions[0].get('UNLINK', []) # Conda 4.4.x else: actions_link = actions.get('LINK', []) actions_unlink = actions.get('UNLINK', []) deps = set() deps = deps.union({p['name'] for p in actions_link}) deps = deps.union({p['name'] for p in actions_unlink}) deps = deps - set(packages) deps = sorted(list(deps)) count_total_packages = len(packages) + len(deps) plural_total = 's' if count_total_packages != 1 else '' plural_selected = 's' if len(packages) != 1 else '' self.table.setRowCount(count_total_packages) self.table.setColumnCount(4) if actions_link: description = '{0} package{1} will be installed'.format( count_total_packages, plural_total) self.table.showColumn(2) self.table.showColumn(3) elif actions_unlink and not actions_link: self.table.hideColumn(2) self.table.hideColumn(3) self.table.setHorizontalHeaderLabels( ['Name', 'Unlink', 'Link', 'Channel']) description = '{0} package{1} will be removed'.format( count_total_packages, plural_total) for row, pkg in enumerate(packages + deps): link_item = [p for p in actions_link if p['name'] == pkg] if not link_item: link_item = { 'version': '-'.center(len('link')), 'channel': '-'.center(len('channel')), } else: link_item = link_item[0] unlink_item = [p for p in actions_unlink if p['name'] == pkg] if not unlink_item: unlink_item = { 'version': '-'.center(len('link')), } else: unlink_item = unlink_item[0] unlink_version = str(unlink_item['version']) link_version = str(link_item['version']) item_unlink_v = QTableWidgetItem(unlink_version) item_link_v = QTableWidgetItem(link_version) item_link_c = QTableWidgetItem(link_item['channel']) if pkg in packages: item_name = QTableWidgetItem(pkg) else: item_name = QTableWidgetItem('*' + pkg) items = [item_name, item_unlink_v, item_link_v, item_link_c] for column, item in enumerate(items): item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self.table.setItem(row, column, item) if deps: message = ( '<b>*</b> indicates the package is a dependency of a ' 'selected package{0}<br>').format(plural_selected) self.button_ok.setEnabled(True) self.table.resizeColumnsToContents() unlink_width = self.table.columnWidth(1) if unlink_width < 60: self.table.setColumnWidth(1, 60) self.table.setHorizontalHeaderLabels( ['Name ', 'Unlink ', 'Link ', 'Channel ']) self.table.setEnabled(True) self.update_status(message=message) self.label_description.setText(description) # Adjust size after data has populated the table self.table.resizeColumnsToContents() width = sum( self.table.columnWidth(i) for i in range(self.table.columnCount())) delta = (self.width() - self.table.width() + self.table.verticalHeader().width() + 10) new_width = width + delta if new_width < self.base_minimum_width: new_width = self.base_minimum_width self.setMinimumWidth(new_width) self.setMaximumWidth(new_width) self.sig_setup_ready.emit() def update_status(self, message='', value=None, max_value=None): """Update status of packages dialog.""" self.label_status.setText(message) if max_value is None and value is None: self.progress_bar.setVisible(False) else: self.progress_bar.setVisible(True) self.progress_bar.setMaximum(max_value) self.progress_bar.setValue(value)
class DialogChannels(DialogBase): """Dialog to add delete and select active conda package channels.""" sig_channels_updated = Signal(object, object) # added, removed sig_setup_ready = Signal() sig_check_ready = Signal() WIDTH = 550 def __init__(self, parent=None): """Dialog to add delete and select active conda pacakge channels .""" super(DialogChannels, self).__init__(parent) self._parent = parent self._conda_url = 'https://conda.anaconda.org' self.api = AnacondaAPI() self.initial_sources = None self.config_sources = None self.style_sheet = None self._setup_ready = False self._conda_url_setup_ready = False # Widgets self.list = ListWidgetChannels(parent=self, api=self.api) self.label_info = LabelBase( 'Manage channels you want Navigator to include.') self.label_status = LabelBase('Collecting sources...') self.progress_bar = QProgressBar(self) self.button_add = ButtonNormal('Add...') self.button_cancel = ButtonNormal('Cancel') self.button_ok = ButtonPrimary('Update channels') # Widget setup self.frame_title_bar.setVisible(False) self.list.setFrameStyle(QFrame.NoFrame) self.list.setFrameShape(QFrame.NoFrame) self.setWindowFlags(self.windowFlags() | Qt.Popup) self.setWindowOpacity(0.96) self.setMinimumHeight(300) self.setMinimumWidth(self.WIDTH) self.setModal(True) # Layout layout_button = QHBoxLayout() layout_button.addWidget(self.label_info) layout_button.addStretch() layout_button.addWidget(self.button_add) layout_ok = QHBoxLayout() layout_ok.addWidget(self.label_status) layout_ok.addWidget(SpacerHorizontal()) layout_ok.addWidget(self.progress_bar) layout_ok.addWidget(SpacerHorizontal()) layout_ok.addStretch() layout_ok.addWidget(self.button_cancel) layout_ok.addWidget(SpacerHorizontal()) layout_ok.addWidget(self.button_ok) layout = QVBoxLayout() layout.addLayout(layout_button) layout.addWidget(SpacerVertical()) layout.addWidget(self.list) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_ok) self.setLayout(layout) # Signals self.button_add.clicked.connect(self.add_channel) self.button_ok.clicked.connect(self.update_channels) self.button_cancel.clicked.connect(self.reject) self.list.sig_status_updated.connect(self.update_status) self.list.sig_channel_added.connect( lambda v=None: self.set_tab_order()) self.list.sig_channel_added.connect( lambda v=None: self.button_ok.setFocus()) self.list.sig_channel_removed.connect( lambda v=None: self.set_tab_order()) self.list.sig_channel_removed.connect( lambda v=None: self.button_ok.setFocus()) self.list.sig_channel_checked.connect(self.sig_check_ready) self.list.sig_channel_status.connect(self.refresh) self.button_add.setDisabled(True) self.button_ok.setDisabled(True) self.button_cancel.setDisabled(True) self.update_status(action='Collecting sources...', value=0, max_value=0) @staticmethod def _group_sources_and_channels(sources): """ Flatten sources and channels dictionary to list of tuples. [(source, channel), (source, channel)...] """ grouped = [] for source, channels in sources.items(): for channel in channels: grouped.append((source, channel)) return grouped def keyPressEvent(self, event): """Override Qt method.""" key = event.key() if key in [Qt.Key_Escape]: if self.list.is_editing: self.refresh() self.list.is_editing = False else: self.reject() # --- Public API # ------------------------------------------------------------------------- def update_style_sheet(self, style_sheet=None): """Update custom css style sheets.""" if style_sheet is None: self.style_sheet = load_style_sheet() else: self.style_sheet = style_sheet self.setStyleSheet(self.style_sheet) self.setMinimumWidth(SASS_VARIABLES.WIDGET_CHANNEL_DIALOG_WIDTH) try: self.list.update_style_sheet(style_sheet) except Exception: pass def update_api(self, worker, api_info, error): """Update api info.""" self._conda_url = api_info.get('conda_url', 'https://conda.anaconda.org') self._conda_url_setup_ready = True if self._setup_ready: self.sig_setup_ready.emit() def setup(self, worker, conda_config_data, error): """Setup the channels widget.""" self.config_sources = conda_config_data.get('config_sources') self.button_add.setDisabled(False) for source, data in self.config_sources.items(): channels = data.get('channels', []) for channel in channels: item = ListWidgetItemChannel(channel=channel, location=source) item.set_editable(False) self.list.addItem(item) self.set_tab_order() self.button_add.setFocus() self.button_ok.setDefault(True) self.button_cancel.setEnabled(True) self.initial_sources = self.list.sources.copy() self.update_status() self._setup_ready = True if self._conda_url_setup_ready: self.sig_setup_ready.emit() def set_tab_order(self): """Fix the tab ordering in the list.""" if self.list._items: self.setTabOrder(self.button_add, self.list._items[0].button_remove) self.setTabOrder(self.list._items[-1].button_remove, self.button_cancel) self.setTabOrder(self.button_cancel, self.button_ok) self.refresh() def add_channel(self): """Add new conda channel.""" user_rc_path = self.api._conda_api.user_rc_path item = ListWidgetItemChannel(channel='', location=user_rc_path) self.list.addItem(item) self.refresh(False) def update_channels(self): """Update channels list and status.""" sources = self.list.sources original = self._group_sources_and_channels(self.initial_sources) updated = self._group_sources_and_channels(sources) if sorted(original) != sorted(updated): self.sig_channels_updated.emit(*self.sources) self.accept() else: self.reject() def refresh(self, channel_status=True): """Update enable/disable status based on item count.""" self.button_add.setEnabled(channel_status and bool(self.list.count)) self.button_ok.setEnabled(channel_status) self.button_cancel.setEnabled(True) if self.list.count() == 0: self.button_add.setEnabled(True) self.button_ok.setEnabled(False) def update_status(self, action='', message='', value=None, max_value=None): """Update the status and progress bar of the widget.""" visible = bool(action) self.label_status.setText(action) self.label_status.setVisible(visible) if value is not None and max_value is not None: self.progress_bar.setVisible(True) self.progress_bar.setRange(0, max_value) self.progress_bar.setValue(value) else: self.progress_bar.setVisible(False) @property def sources(self): """Return sources to add and remove from config.""" original = self._group_sources_and_channels(self.initial_sources) updated = self._group_sources_and_channels(self.list.sources) original = set(original) updated = set(updated) add = updated - original remove = original - updated return add, remove
class PasswordDialog(DialogBase): """Password dialog.""" def __init__(self, *args, **kwargs): """About dialog.""" super(PasswordDialog, self).__init__(*args, **kwargs) self.wm = WorkerManager() # Widgets self.label_text = LabelBase( 'VSCode will be installed through your system <br> package ' 'manager.<br><br>' 'This action requires elevated privileges. Please <br>provide a ' 'password to forward to sudo') self.lineedit = PasswordEdit() self.label_info = LabelBase() self.button_cancel = ButtonNormal('Cancel') self.button_ok = ButtonPrimary('Ok') self.worker = None self._valid = False self._timer = QTimer() self._timer.setInterval(3000) self._timer.timeout.connect(self.check) # Widgets setup self.button_ok.setMinimumWidth(70) self.button_ok.setDefault(True) self.setWindowTitle("Privilege Elevation Required") self.lineedit.setEchoMode(LineEditBase.Password) # Layouts layout_content = QVBoxLayout() layout_content.addWidget(self.label_text) layout_content.addWidget(SpacerVertical()) layout_content.addWidget(self.lineedit, 0, Qt.AlignBottom) layout_content.addWidget(SpacerVertical()) layout_content.addWidget(self.label_info, 0, Qt.AlignTop) layout_content.addWidget(SpacerVertical()) layout_buttons = QHBoxLayout() layout_buttons.addStretch() layout_buttons.addWidget(self.button_cancel) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_ok) layout_main = QVBoxLayout() layout_main.addLayout(layout_content) layout_main.addWidget(SpacerVertical()) layout_main.addWidget(SpacerVertical()) layout_main.addLayout(layout_buttons) self.setLayout(layout_main) # Signals self.button_ok.clicked.connect(self.accept2) self.button_cancel.clicked.connect(self.reject2) self.lineedit.textChanged.connect(self.refresh) # Setup self.lineedit.setFocus() self.refresh() def refresh(self): """Refresh state of buttons.""" self.button_ok.setEnabled(bool(self.password)) def _output(self, worker, output, error): """Callback.""" self._valid = True def check(self): """Check password.""" if self.worker._started and self._valid: self.lineedit.setEnabled(False) self.label_info.setText('') self.accept() elif self.worker._started: self._timer.stop() self.lineedit.setEnabled(True) self.button_ok.setEnabled(True) self.lineedit.setFocus() self.lineedit.selectAll() self.label_info.setText('<i>Invalid password</i>') def reject2(self): """Handle reject.""" if self.worker is not None: self.worker.terminate() self.reject() def accept2(self): """Handle accept.""" stdin = to_binary_string(self.password + '\n') if self.worker is not None: self.worker.terminate() self.worker = self.wm.create_process_worker(['sudo', '-kS', 'ls']) self.worker.sig_partial.connect(self._output) self.worker.sig_finished.connect(self._output) self._valid = False self._timer.start() self.worker.start() self.worker.write(stdin) self.lineedit.setEnabled(False) self.button_ok.setEnabled(False) @property def password(self): """Return password.""" return self.lineedit.text()