class MatplotlibDataViewer(MatplotlibViewerMixin, DataViewer): _state_cls = MatplotlibDataViewerState tools = ['mpl:home', 'mpl:pan', 'mpl:zoom'] subtools = {'save': ['mpl:save']} def __init__(self, session, parent=None, wcs=None, state=None): super(MatplotlibDataViewer, self).__init__(session, parent=parent, state=state) # Use MplWidget to set up a Matplotlib canvas inside the Qt window self.mpl_widget = MplWidget() self.setCentralWidget(self.mpl_widget) # TODO: shouldn't have to do this self.central_widget = self.mpl_widget self.figure, self.axes = init_mpl(self.mpl_widget.canvas.fig, wcs=wcs) MatplotlibViewerMixin.setup_callbacks(self) self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self._monitor_computation = QTimer() self._monitor_computation.setInterval(500) self._monitor_computation.timeout.connect(self._update_computation) def _update_computation(self, message=None): # If we get a ComputationStartedMessage and the timer isn't currently # active, then we start the timer but we then return straight away. # This is to avoid showing the 'Computing' message straight away in the # case of reasonably fast operations. if isinstance(message, ComputationStartedMessage): if not self._monitor_computation.isActive(): self._monitor_computation.start() return for layer_artist in self.layers: if layer_artist.is_computing: self.loading_rectangle.set_visible(True) text = self.loading_text.get_text() if text.count('.') > 2: text = 'Computing' else: text += '.' self.loading_text.set_text(text) self.loading_text.set_visible(True) self.redraw() return self.loading_rectangle.set_visible(False) self.loading_text.set_visible(False) self.redraw() # If we get here, the computation has stopped so we can stop the timer self._monitor_computation.stop()
class BaseTimerStatus(StatusBarWidget): """Status bar widget base for widgets that update based on timers.""" def __init__(self, parent, statusbar): """Status bar widget base for widgets that update based on timers.""" self.timer = None # Needs to come before parent call super(BaseTimerStatus, self).__init__(parent, statusbar) self._interval = 2000 # Widget setup fm = self.label_value.fontMetrics() self.label_value.setMinimumWidth(fm.width('000%')) # Setup if self.is_supported(): self.timer = QTimer() self.timer.timeout.connect(self.update_status) self.timer.start(self._interval) else: self.hide() def setVisible(self, value): """Override Qt method to stops timers if widget is not visible.""" if self.timer is not None: if value: self.timer.start(self._interval) else: self.timer.stop() super(BaseTimerStatus, self).setVisible(value) def set_interval(self, interval): """Set timer interval (ms).""" self._interval = interval if self.timer is not None: self.timer.setInterval(interval) def import_test(self): """Raise ImportError if feature is not supported.""" raise NotImplementedError def is_supported(self): """Return True if feature is supported.""" try: self.import_test() return True except ImportError: return False def get_value(self): """Return formatted text value.""" raise NotImplementedError def update_status(self): """Update status label widget, if widget is visible.""" if self.isVisible(): self.label_value.setText(self.get_value())
def embed(aQObject): tag = "__eventletEmbededTimer__" timer = QTimer() timer.setSingleShot(True) timer.setInterval(0.1) timer.timeout.connect(functools.partial(_timerOnTimeout, timer)) timer.start() aQObject.setProperty(tag, timer)
class ConnectionInspector(QWidget): def __init__(self, parent=None): super(ConnectionInspector, self).__init__(parent, Qt.Window) connections = self.fetch_data() self.table_view = ConnectionTableView(connections, self) self.setLayout(QVBoxLayout(self)) self.layout().addWidget(self.table_view) button_layout = QHBoxLayout() self.layout().addItem(button_layout) self.save_status_label = QLabel(self) button_layout.addWidget(self.save_status_label) button_layout.addStretch() self.save_button = QPushButton(self) self.save_button.setText("Save list to file...") self.save_button.clicked.connect(self.save_list_to_file) button_layout.addWidget(self.save_button) self.update_timer = QTimer(parent=self) self.update_timer.setInterval(1500) self.update_timer.timeout.connect(self.update_data) self.update_timer.start() def update_data(self): self.table_view.model().connections = self.fetch_data() def fetch_data(self): plugins = data_plugins.plugin_modules return [connection for p in plugins.values() for connection in p.connections.values() ] @Slot() def save_list_to_file(self): filename, filters = QFileDialog.getSaveFileName(self, "Save connection list", "", "Text Files (*.txt)") try: with open(filename, "w") as f: for conn in self.table_view.model().connections: f.write( "{p}://{a}\n".format(p=conn.protocol, a=conn.address)) self.save_status_label.setText("File saved to {}".format(filename)) except Exception as e: msgBox = QMessageBox() msgBox.setText("Couldn't save connection list to file.") msgBox.setInformativeText("Error: {}".format(str(e))) msgBox.setStandardButtons(QMessageBox.Ok) msgBox.exec_()
class BaseTimerStatus(StatusBarWidget): TITLE = None TIP = None def __init__(self, parent, statusbar): StatusBarWidget.__init__(self, parent, statusbar) self.setToolTip(self.TIP) layout = self.layout() layout.addWidget(QLabel(self.TITLE)) self.label = QLabel() self.label.setFont(self.label_font) layout.addWidget(self.label) layout.addSpacing(20) if self.is_supported(): self.timer = QTimer() self.timer.timeout.connect(self.update_label) self.timer.start(2000) else: self.timer = None self.hide() def set_interval(self, interval): """Set timer interval (ms)""" if self.timer is not None: self.timer.setInterval(interval) def import_test(self): """Raise ImportError if feature is not supported""" raise NotImplementedError def is_supported(self): """Return True if feature is supported""" try: self.import_test() return True except ImportError: return False def get_value(self): """Return value (e.g. CPU or memory usage)""" raise NotImplementedError def update_label(self): """Update status label widget, if widget is visible""" if self.isVisible(): self.label.setText('%d %%' % self.get_value())
def create_app(datafiles=[], interactive=True): app = get_qapp() if interactive: # Splash screen splash = get_splash() splash.image = QtGui.QPixmap(MOSVIZ_SPLASH_PATH) splash.show() else: splash = None # Start off by loading plugins. We need to do this before restoring # the session or loading the configuration since these may use existing # plugins. load_plugins(splash=splash) # # Show the splash screen for 2 seconds if interactive: timer = QTimer() timer.setInterval(2000) timer.setSingleShot(True) timer.timeout.connect(splash.close) timer.start() data_collection = glue.core.DataCollection() hub = data_collection.hub if interactive: splash.set_progress(100) ga = _create_glue_app(data_collection, hub) ga.run_startup_action('mosviz') # Load the data files. if datafiles: datasets = load_data_files(datafiles) ga.add_datasets(data_collection, datasets, auto_merge=False) return ga
class _DownloadAPI(QObject): """Download API based on requests.""" _sig_download_finished = Signal(str, str) _sig_download_progress = Signal(str, str, int, int) _sig_partial = Signal(object) MAX_THREADS = 20 DEFAULT_TIMEOUT = 5 # seconds def __init__(self, config=None): """Download API based on requests.""" super(QObject, self).__init__() self._conda_api = CondaAPI() self._client_api = ClientAPI() self._config = config self._queue = deque() self._queue_workers = deque() self._threads = [] self._workers = [] self._timer = QTimer() self._timer_worker_delete = QTimer() self._running_threads = 0 self._bag_collector = deque() # Keeps references to old workers self._chunk_size = 1024 self._timer.setInterval(333) self._timer.timeout.connect(self._start) self._timer_worker_delete.setInterval(5000) self._timer_worker_delete.timeout.connect(self._clean_workers) def _clean_workers(self): """Delete periodically workers in workers bag.""" while self._bag_collector: self._bag_collector.popleft() self._timer_worker_delete.stop() def _get_verify_ssl(self, verify, set_conda_ssl=True): """Get verify ssl.""" if verify is None: verify_value = self._client_api.get_ssl( set_conda_ssl=set_conda_ssl, ) else: verify_value = verify return verify_value def _is_internet_available(self): """Check initernet availability.""" if self._config: config_value = self._config.get('main', 'offline_mode') else: config_value = False if config_value: connectivity = False else: connectivity = True # is_internet_available() return connectivity @property def proxy_servers(self): """Return the proxy servers available from the conda rc config file.""" return self._conda_api.load_proxy_config() def _start(self): """Start threads and check for inactive workers.""" if self._queue_workers and self._running_threads < self.MAX_THREADS: # print('Queue: {0} Running: {1} Workers: {2} ' # 'Threads: {3}'.format(len(self._queue_workers), # self._running_threads, # len(self._workers), # len(self._threads))) self._running_threads += 1 thread = QThread() worker = self._queue_workers.popleft() worker.moveToThread(thread) worker.sig_finished.connect(thread.quit) thread.started.connect(worker.start) thread.start() self._threads.append(thread) if self._workers: for w in self._workers: if w.is_finished(): self._bag_collector.append(w) self._workers.remove(w) if self._threads: for t in self._threads: if t.isFinished(): self._threads.remove(t) self._running_threads -= 1 if len(self._threads) == 0 and len(self._workers) == 0: self._timer.stop() self._timer_worker_delete.start() def _create_worker(self, method, *args, **kwargs): """Create a new worker instance.""" worker = DownloadWorker(method, args, kwargs) self._workers.append(worker) self._queue_workers.append(worker) self._sig_download_finished.connect(worker.sig_download_finished) self._sig_download_progress.connect(worker.sig_download_progress) self._sig_partial.connect(worker._handle_partial) self._timer.start() return worker def _download( self, url, path=None, force=False, verify=None, chunked=True, ): """Callback for download.""" verify_value = self._get_verify_ssl(verify, set_conda_ssl=False) if path is None: path = url.split('/')[-1] # Make dir if non existent folder = os.path.dirname(os.path.abspath(path)) if not os.path.isdir(folder): os.makedirs(folder) # Get headers if self._is_internet_available(): try: r = requests.head( url, proxies=self.proxy_servers, verify=verify_value, timeout=self.DEFAULT_TIMEOUT, ) status_code = r.status_code except Exception as error: status_code = -1 logger.error(str(error)) logger.debug('Status code {0} - url'.format(status_code, url)) if status_code != 200: logger.error('Invalid url {0}'.format(url)) return path total_size = int(r.headers.get('Content-Length', 0)) # Check if file exists if os.path.isfile(path) and not force: file_size = os.path.getsize(path) else: file_size = -1 # print(path, total_size, file_size) # Check if existing file matches size of requested file if file_size == total_size: self._sig_download_finished.emit(url, path) return path else: try: r = requests.get( url, stream=chunked, proxies=self.proxy_servers, verify=verify_value, timeout=self.DEFAULT_TIMEOUT, ) status_code = r.status_code except Exception as error: status_code = -1 logger.error(str(error)) # File not found or file size did not match. Download file. progress_size = 0 bytes_stream = QBuffer() # BytesIO was segfaulting for big files bytes_stream.open(QBuffer.ReadWrite) # For some chunked content the app segfaults (with big files) # so now chunked is a kwarg for this method if chunked: for chunk in r.iter_content(chunk_size=self._chunk_size): # print(url, progress_size, total_size) if chunk: bytes_stream.write(chunk) progress_size += len(chunk) self._sig_download_progress.emit( url, path, progress_size, total_size, ) self._sig_partial.emit({ 'url': url, 'path': path, 'progress_size': progress_size, 'total_size': total_size, }) else: bytes_stream.write(r.content) bytes_stream.seek(0) data = bytes_stream.data() with open(path, 'wb') as f: f.write(data) bytes_stream.close() self._sig_download_finished.emit(url, path) return path def _is_valid_url(self, url, verify=None): """Callback for is_valid_url.""" verify_value = self._get_verify_ssl(verify) if self._is_internet_available(): try: r = requests.head( url, proxies=self.proxy_servers, verify=verify_value, timeout=self.DEFAULT_TIMEOUT, ) value = r.status_code in [200] except Exception as error: logger.error(str(error)) value = False return value def _is_valid_channel( self, channel, conda_url='https://conda.anaconda.org', verify=None, ): """Callback for is_valid_channel.""" verify_value = self._get_verify_ssl(verify) if channel.startswith('https://') or channel.startswith('http://'): url = channel else: url = "{0}/{1}".format(conda_url, channel) if url[-1] == '/': url = url[:-1] plat = self._conda_api.get_platform() repodata_url = "{0}/{1}/{2}".format(url, plat, 'repodata.json') if self._is_internet_available(): try: r = requests.head( repodata_url, proxies=self.proxy_servers, verify=verify_value, timeout=self.DEFAULT_TIMEOUT, ) value = r.status_code in [200] except Exception as error: logger.error(str(error)) value = False return value def _is_valid_api_url(self, url, verify=None): """Callback for is_valid_api_url.""" verify_value = self._get_verify_ssl(verify) # Check response is a JSON with ok: 1 data = {} if verify is None: verify_value = self._client_api.get_ssl() else: verify_value = verify if self._is_internet_available(): try: r = requests.get( url, proxies=self.proxy_servers, verify=verify_value, timeout=self.DEFAULT_TIMEOUT, ) content = to_text_string(r.content, encoding='utf-8') data = json.loads(content) except Exception as error: logger.error(str(error)) return data.get('ok', 0) == 1 def _get_url(self, url, as_json=False, verify=None): """Callback for url checking.""" data = {} verify_value = self._get_verify_ssl(verify) if self._is_internet_available(): try: # See: https://github.com/ContinuumIO/navigator/issues/1485 session = requests.Session() retry = Retry(connect=3, backoff_factor=0.5) adapter = HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) r = session.get( url, proxies=self.proxy_servers, verify=verify_value, timeout=self.DEFAULT_TIMEOUT, ) data = to_text_string(r.content, encoding='utf-8') if as_json: data = json.loads(data) except Exception as error: logger.error(str(error)) return data def _get_api_info(self, url, verify=None): """Callback.""" verify_value = self._get_verify_ssl(verify) data = { "api_url": url, "api_docs_url": "https://api.anaconda.org/docs", "conda_url": "https://conda.anaconda.org/", "main_url": "https://anaconda.org/", "pypi_url": "https://pypi.anaconda.org/", "swagger_url": "https://api.anaconda.org/swagger.json", } if self._is_internet_available(): try: r = requests.get( url, proxies=self.proxy_servers, verify=verify_value, timeout=self.DEFAULT_TIMEOUT, ) content = to_text_string(r.content, encoding='utf-8') new_data = json.loads(content) data['conda_url'] = new_data.get('conda_url', data['conda_url']) except Exception as error: logger.error(str(error)) return data # --- Public API # ------------------------------------------------------------------------- def download(self, url, path=None, force=False, verify=None, chunked=True): """Download file given by url and save it to path.""" logger.debug(str((url, path, force))) method = self._download return self._create_worker( method, url, path=path, force=force, verify=verify, chunked=chunked, ) def terminate(self): """Terminate all workers and threads.""" for t in self._threads: t.quit() self._thread = [] self._workers = [] def is_valid_url(self, url, non_blocking=True): """Check if url is valid.""" logger.debug(str((url))) if non_blocking: method = self._is_valid_url return self._create_worker(method, url) else: return self._is_valid_url(url) def is_valid_api_url(self, url, non_blocking=True, verify=None): """Check if anaconda api url is valid.""" logger.debug(str((url))) if non_blocking: method = self._is_valid_api_url return self._create_worker(method, url, verify=verify) else: return self._is_valid_api_url(url=url, verify=verify) def is_valid_channel( self, channel, conda_url='https://conda.anaconda.org', non_blocking=True, ): """Check if a conda channel is valid.""" logger.debug(str((channel, conda_url))) if non_blocking: method = self._is_valid_channel return self._create_worker(method, channel, conda_url) else: return self._is_valid_channel(channel, conda_url=conda_url) def get_url(self, url, as_json=False, verify=None, non_blocking=True): """Get url content.""" logger.debug(str(url)) if non_blocking: method = self._get_url return self._create_worker(method, url, as_json=as_json, verify=verify) else: return self._get_url(url, as_json=as_json, verify=verify) def get_api_info(self, url, non_blocking=True): """Query anaconda api info.""" logger.debug(str((url, non_blocking))) if non_blocking: method = self._get_api_info return self._create_worker(method, url) else: return self._get_api_info(url)
class FindReplace(QWidget): """Find widget""" STYLE = { False: "background-color:rgb(255, 175, 90);", True: "", None: "", 'regexp_error': "background-color:rgb(255, 80, 80);", } TOOLTIP = { False: _("No matches"), True: _("Search string"), None: _("Search string"), 'regexp_error': _("Regular expression error") } visibility_changed = Signal(bool) return_shift_pressed = Signal() return_pressed = Signal() def __init__(self, parent, enable_replace=False): QWidget.__init__(self, parent) self.enable_replace = enable_replace self.editor = None self.is_code_editor = None glayout = QGridLayout() glayout.setContentsMargins(0, 0, 0, 0) self.setLayout(glayout) self.close_button = create_toolbutton( self, triggered=self.hide, icon=ima.icon('DialogCloseButton')) glayout.addWidget(self.close_button, 0, 0) # Find layout self.search_text = PatternComboBox(self, tip=_("Search string"), adjust_to_minimum=False) self.return_shift_pressed.connect( lambda: self.find(changed=False, forward=False, rehighlight=False, multiline_replace_check=False)) self.return_pressed.connect( lambda: self.find(changed=False, forward=True, rehighlight=False, multiline_replace_check=False)) self.search_text.lineEdit().textEdited.connect( self.text_has_been_edited) self.number_matches_text = QLabel(self) self.previous_button = create_toolbutton(self, triggered=self.find_previous, icon=ima.icon('ArrowUp')) self.next_button = create_toolbutton(self, triggered=self.find_next, icon=ima.icon('ArrowDown')) self.next_button.clicked.connect(self.update_search_combo) self.previous_button.clicked.connect(self.update_search_combo) self.re_button = create_toolbutton(self, icon=ima.icon('advanced'), tip=_("Regular expression")) self.re_button.setCheckable(True) self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton(self, icon=get_icon("upper_lower.png"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) self.case_button.toggled.connect(lambda state: self.find()) self.words_button = create_toolbutton(self, icon=get_icon("whole_words.png"), tip=_("Whole words")) self.words_button.setCheckable(True) self.words_button.toggled.connect(lambda state: self.find()) self.highlight_button = create_toolbutton( self, icon=get_icon("highlight.png"), tip=_("Highlight matches")) self.highlight_button.setCheckable(True) self.highlight_button.toggled.connect(self.toggle_highlighting) hlayout = QHBoxLayout() self.widgets = [ self.close_button, self.search_text, self.number_matches_text, self.previous_button, self.next_button, self.re_button, self.case_button, self.words_button, self.highlight_button ] for widget in self.widgets[1:]: hlayout.addWidget(widget) glayout.addLayout(hlayout, 0, 1) # Replace layout replace_with = QLabel(_("Replace with:")) self.replace_text = PatternComboBox(self, adjust_to_minimum=False, tip=_('Replace string')) self.replace_text.valid.connect( lambda _: self.replace_find(focus_replace_text=True)) self.replace_button = create_toolbutton( self, text=_('Replace/find next'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find, text_beside_icon=True) self.replace_sel_button = create_toolbutton( self, text=_('Replace selection'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find_selection, text_beside_icon=True) self.replace_sel_button.clicked.connect(self.update_replace_combo) self.replace_sel_button.clicked.connect(self.update_search_combo) self.replace_all_button = create_toolbutton( self, text=_('Replace all'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find_all, text_beside_icon=True) self.replace_all_button.clicked.connect(self.update_replace_combo) self.replace_all_button.clicked.connect(self.update_search_combo) self.replace_layout = QHBoxLayout() widgets = [ replace_with, self.replace_text, self.replace_button, self.replace_sel_button, self.replace_all_button ] for widget in widgets: self.replace_layout.addWidget(widget) glayout.addLayout(self.replace_layout, 1, 1) self.widgets.extend(widgets) self.replace_widgets = widgets self.hide_replace() self.search_text.setTabOrder(self.search_text, self.replace_text) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.shortcuts = self.create_shortcuts(parent) self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(1000) self.highlight_timer.timeout.connect(self.highlight_matches) self.search_text.installEventFilter(self) def eventFilter(self, widget, event): """Event filter for search_text widget. Emits signals when presing Enter and Shift+Enter. This signals are used for search forward and backward. Also, a crude hack to get tab working in the Find/Replace boxes. """ if event.type() == QEvent.KeyPress: key = event.key() shift = event.modifiers() & Qt.ShiftModifier if key == Qt.Key_Return: if shift: self.return_shift_pressed.emit() else: self.return_pressed.emit() if key == Qt.Key_Tab: self.focusNextChild() return super(FindReplace, self).eventFilter(widget, event) def create_shortcuts(self, parent): """Create shortcuts for this widget""" # Configurable findnext = config_shortcut(self.find_next, context='_', name='Find next', parent=parent) findprev = config_shortcut(self.find_previous, context='_', name='Find previous', parent=parent) togglefind = config_shortcut(self.show, context='_', name='Find text', parent=parent) togglereplace = config_shortcut(self.show_replace, context='_', name='Replace text', parent=parent) hide = config_shortcut(self.hide, context='_', name='hide find and replace', parent=self) return [findnext, findprev, togglefind, togglereplace, hide] def get_shortcut_data(self): """ Returns shortcut data, a list of tuples (shortcut, text, default) shortcut (QShortcut or QAction instance) text (string): action/shortcut description default (string): default key sequence """ return [sc.data for sc in self.shortcuts] def update_search_combo(self): self.search_text.lineEdit().returnPressed.emit() def update_replace_combo(self): self.replace_text.lineEdit().returnPressed.emit() def toggle_replace_widgets(self): if self.enable_replace: # Toggle replace widgets if self.replace_widgets[0].isVisible(): self.hide_replace() self.hide() else: self.show_replace() if len(to_text_string(self.search_text.currentText())) > 0: self.replace_text.setFocus() @Slot(bool) def toggle_highlighting(self, state): """Toggle the 'highlight all results' feature""" if self.editor is not None: if state: self.highlight_matches() else: self.clear_matches() def show(self, hide_replace=True): """Overrides Qt Method""" QWidget.show(self) self.visibility_changed.emit(True) self.change_number_matches() if self.editor is not None: if hide_replace: if self.replace_widgets[0].isVisible(): self.hide_replace() text = self.editor.get_selected_text() # When selecting several lines, and replace box is activated the # text won't be replaced for the selection if hide_replace or len(text.splitlines()) <= 1: highlighted = True # If no text is highlighted for search, use whatever word is # under the cursor if not text: highlighted = False try: cursor = self.editor.textCursor() cursor.select(QTextCursor.WordUnderCursor) text = to_text_string(cursor.selectedText()) except AttributeError: # We can't do this for all widgets, e.g. WebView's pass # Now that text value is sorted out, use it for the search if text and not self.search_text.currentText() or highlighted: self.search_text.setEditText(text) self.search_text.lineEdit().selectAll() self.refresh() else: self.search_text.lineEdit().selectAll() self.search_text.setFocus() @Slot() def hide(self): """Overrides Qt Method""" for widget in self.replace_widgets: widget.hide() QWidget.hide(self) self.visibility_changed.emit(False) if self.editor is not None: self.editor.setFocus() self.clear_matches() def show_replace(self): """Show replace widgets""" self.show(hide_replace=False) for widget in self.replace_widgets: widget.show() def hide_replace(self): """Hide replace widgets""" for widget in self.replace_widgets: widget.hide() def refresh(self): """Refresh widget""" if self.isHidden(): if self.editor is not None: self.clear_matches() return state = self.editor is not None for widget in self.widgets: widget.setEnabled(state) if state: self.find() def set_editor(self, editor, refresh=True): """ Set associated editor/web page: codeeditor.base.TextEditBaseWidget browser.WebView """ self.editor = editor # Note: This is necessary to test widgets/editor.py # in Qt builds that don't have web widgets try: from qtpy.QtWebEngineWidgets import QWebEngineView except ImportError: QWebEngineView = type(None) self.words_button.setVisible(not isinstance(editor, QWebEngineView)) self.re_button.setVisible(not isinstance(editor, QWebEngineView)) from spyder.widgets.sourcecode.codeeditor import CodeEditor self.is_code_editor = isinstance(editor, CodeEditor) self.highlight_button.setVisible(self.is_code_editor) if refresh: self.refresh() if self.isHidden() and editor is not None: self.clear_matches() @Slot() def find_next(self): """Find next occurrence""" state = self.find(changed=False, forward=True, rehighlight=False, multiline_replace_check=False) self.editor.setFocus() self.search_text.add_current_text() return state @Slot() def find_previous(self): """Find previous occurrence""" state = self.find(changed=False, forward=False, rehighlight=False, multiline_replace_check=False) self.editor.setFocus() return state def text_has_been_edited(self, text): """Find text has been edited (this slot won't be triggered when setting the search pattern combo box text programmatically)""" self.find(changed=True, forward=True, start_highlight_timer=True) def highlight_matches(self): """Highlight found results""" if self.is_code_editor and self.highlight_button.isChecked(): text = self.search_text.currentText() words = self.words_button.isChecked() regexp = self.re_button.isChecked() self.editor.highlight_found_results(text, words=words, regexp=regexp) def clear_matches(self): """Clear all highlighted matches""" if self.is_code_editor: self.editor.clear_found_results() def find(self, changed=True, forward=True, rehighlight=True, start_highlight_timer=False, multiline_replace_check=True): """Call the find function""" # When several lines are selected in the editor and replace box is activated, # dynamic search is deactivated to prevent changing the selection. Otherwise # we show matching items. def regexp_error_msg(pattern): """Returns None if the pattern is a valid regular expression or a string describing why the pattern is invalid. """ try: re.compile(pattern) except re.error as e: return str(e) return None if multiline_replace_check and self.replace_widgets[0].isVisible() and \ len(to_text_string(self.editor.get_selected_text()).splitlines())>1: return None text = self.search_text.currentText() if len(text) == 0: self.search_text.lineEdit().setStyleSheet("") if not self.is_code_editor: # Clears the selection for WebEngine self.editor.find_text('') self.change_number_matches() return None else: case = self.case_button.isChecked() words = self.words_button.isChecked() regexp = self.re_button.isChecked() found = self.editor.find_text(text, changed, forward, case=case, words=words, regexp=regexp) stylesheet = self.STYLE[found] tooltip = self.TOOLTIP[found] if not found and regexp: error_msg = regexp_error_msg(text) if error_msg: # special styling for regexp errors stylesheet = self.STYLE['regexp_error'] tooltip = self.TOOLTIP['regexp_error'] + ': ' + error_msg self.search_text.lineEdit().setStyleSheet(stylesheet) self.search_text.setToolTip(tooltip) if self.is_code_editor and found: if rehighlight or not self.editor.found_results: self.highlight_timer.stop() if start_highlight_timer: self.highlight_timer.start() else: self.highlight_matches() else: self.clear_matches() number_matches = self.editor.get_number_matches(text, case=case) if hasattr(self.editor, 'get_match_number'): match_number = self.editor.get_match_number(text, case=case) else: match_number = 0 self.change_number_matches(current_match=match_number, total_matches=number_matches) return found
class MicroViewWidget(mvBase): __clsName = "MicroViewWidget" def tr(self, string): return QCoreApplication.translate(self.__clsName, string) def __init__(self, parent=None): super().__init__(parent) # Go before setupUi for QMetaObject.connectSlotsByName to work self._scene = MicroViewScene(self) self._scene.setObjectName("scene") self._ui = mvClass() self._ui.setupUi(self) self._ui.view.setScene(self._scene) # Apparently this is necessary with Qt5, as otherwise updating fails # on image change; there are white rectangles on the updated area # until the mouse is moved in or out of the view self._ui.view.setViewportUpdateMode( QGraphicsView.BoundingRectViewportUpdate) self._ui.view.setRenderHints(QPainter.Antialiasing) self._scene.imageItem.signals.mouseMoved.connect( self._updateCurrentPixelInfo) self._imageData = np.array([]) self._intensityMin = None self._intensityMax = None self._sliderFactor = 1 self._ui.autoButton.pressed.connect(self.autoIntensity) self._playing = False self._playTimer = QTimer() if not (qtpy.PYQT4 or qtpy.PYSIDE): self._playTimer.setTimerType(Qt.PreciseTimer) self._playTimer.setSingleShot(False) # set up preview button self._locEnabledStr = "Localizations are shown" self._locDisabledStr = "Localizations are not shown" self._ui.locButton.setToolTip(self.tr(self._locEnabledStr)) self._ui.locButton.toggled.connect(self.showLocalizationsChanged) # connect signals and slots self._ui.framenoBox.valueChanged.connect(self.selectFrame) self._ui.playButton.pressed.connect( lambda: self.setPlaying(not self._playing)) self._playTimer.timeout.connect(self.nextFrame) self._ui.zoomInButton.pressed.connect(self.zoomIn) self._ui.zoomOriginalButton.pressed.connect(self.zoomOriginal) self._ui.zoomOutButton.pressed.connect(self.zoomOut) self._ui.zoomFitButton.pressed.connect(self.zoomFit) self._scene.roiChanged.connect(self.roiChanged) # set button icons self._ui.locButton.setIcon(QIcon.fromTheme("view-preview")) self._ui.zoomOutButton.setIcon(QIcon.fromTheme("zoom-out")) self._ui.zoomOriginalButton.setIcon(QIcon.fromTheme("zoom-original")) self._ui.zoomFitButton.setIcon(QIcon.fromTheme("zoom-fit-best")) self._ui.zoomInButton.setIcon(QIcon.fromTheme("zoom-in")) self._ui.roiButton.setIcon(QIcon.fromTheme("draw-polygon")) self._playIcon = QIcon.fromTheme("media-playback-start") self._pauseIcon = QIcon.fromTheme("media-playback-pause") self._ui.playButton.setIcon(self._playIcon) # these are to be setEnable(False)'ed if there is no image sequence self._noImsDisable = [ self._ui.zoomOutButton, self._ui.zoomOriginalButton, self._ui.zoomFitButton, self._ui.zoomInButton, self._ui.roiButton, self._ui.view, self._ui.pixelInfo, self._ui.frameSelector, self._ui.contrastGroup ] # initialize image data self._locDataGood = None self._locDataBad = None self._locMarkers = None self.setImageSequence(None) def setRoi(self, roi): self._scene.roi = roi roiChanged = Signal(QPolygonF) @Property(QPolygonF, fset=setRoi, doc="Polygon describing the region of interest (ROI)") def roi(self): return self._scene.roi def setImageSequence(self, ims): self._locDataGood = None self._locDataBad = None if ims is None: self._ims = None self._imageData = None for w in self._noImsDisable: w.setEnabled(False) self.drawImage() self.drawLocalizations() return for w in self._noImsDisable: w.setEnabled(True) self._ui.framenoBox.setMaximum(len(ims)) self._ui.framenoSlider.setMaximum(len(ims)) self._ims = ims try: self._imageData = self._ims[self._ui.framenoBox.value() - 1] except Exception: self.frameReadError.emit(self._ui.framenoBox.value() - 1) return if np.issubdtype(self._imageData.dtype, np.floating): # ugly hack; get min and max corresponding to integer types based # on the range of values in the first image min = self._imageData.min() if min < 0: types = (np.int8, np.int16, np.int32, np.int64) else: types = (np.uint8, np.uint16, np.uint32, np.uint64) max = self._imageData.max() if min >= 0. and max <= 1.: min = 0 max = 1 else: for t in types: ii = np.iinfo(t) if min >= ii.min and max <= ii.max: min = ii.min max = ii.max break else: min = np.iinfo(ims.pixel_type).min max = np.iinfo(ims.pixel_type).max if min == 0. and max == 1.: self._ui.minSlider.setRange(0, 1000) self._ui.maxSlider.setRange(0, 1000) self._ui.minSpinBox.setDecimals(3) self._ui.minSpinBox.setRange(0, 1) self._ui.maxSpinBox.setDecimals(3) self._ui.maxSpinBox.setRange(0, 1) self._sliderFactor = 1000 else: self._ui.minSlider.setRange(min, max) self._ui.maxSlider.setRange(min, max) self._ui.minSpinBox.setDecimals(0) self._ui.minSpinBox.setRange(min, max) self._ui.maxSpinBox.setDecimals(0) self._ui.maxSpinBox.setRange(min, max) self._sliderFactor = 1 if (self._intensityMin is None) or (self._intensityMax is None): self.autoIntensity() else: self.drawImage() self._scene.setSceneRect(self._scene.itemsBoundingRect()) self.currentFrameChanged.emit() @Slot(int) def on_minSlider_valueChanged(self, val): self._ui.minSpinBox.setValue(float(val) / self._sliderFactor) @Slot(int) def on_maxSlider_valueChanged(self, val): self._ui.maxSpinBox.setValue(float(val) / self._sliderFactor) @Slot(float) def on_minSpinBox_valueChanged(self, val): self._ui.minSlider.setValue(round(val * self._sliderFactor)) self.setMinIntensity(val) @Slot(float) def on_maxSpinBox_valueChanged(self, val): self._ui.maxSlider.setValue(round(val * self._sliderFactor)) self.setMaxIntensity(val) @Slot(pd.DataFrame) def setLocalizationData(self, good, bad): self._locDataGood = good self._locDataBad = bad self.drawLocalizations() def setPlaying(self, play): if self._ims is None: return if play == self._playing: return if play: self._playTimer.setInterval(1000 / self._ui.fpsBox.value()) self._playTimer.start() else: self._playTimer.stop() self._ui.fpsBox.setEnabled(not play) self._ui.framenoBox.setEnabled(not play) self._ui.framenoSlider.setEnabled(not play) self._ui.framenoLabel.setEnabled(not play) self._ui.playButton.setIcon( self._pauseIcon if play else self._playIcon) self._playing = play def drawImage(self): if self._imageData is None: self._scene.setImage(QPixmap()) return img_buf = self._imageData.astype(np.float) if (self._intensityMin is None) or (self._intensityMax is None): self._intensityMin = np.min(img_buf) self._intensityMax = np.max(img_buf) img_buf -= float(self._intensityMin) img_buf *= 255. / float(self._intensityMax - self._intensityMin) np.clip(img_buf, 0., 255., img_buf) # convert grayscale to RGB 32bit # far faster than calling img_buf.astype(np.uint8).repeat(4) qi = np.empty((img_buf.shape[0], img_buf.shape[1], 4), dtype=np.uint8) qi[:, :, 0] = qi[:, :, 1] = qi[:, :, 2] = qi[:, :, 3] = img_buf # prevent QImage from being garbage collected self._qImg = QImage(qi, self._imageData.shape[1], self._imageData.shape[0], QImage.Format_RGB32) self._scene.setImage(self._qImg) def drawLocalizations(self): if isinstance(self._locMarkers, QGraphicsItem): self._scene.removeItem(self._locMarkers) self._locMarkers = None if not self.showLocalizations: return try: sel = self._locDataGood["frame"] == self._ui.framenoBox.value() - 1 dGood = self._locDataGood[sel] except Exception: return try: sel = self._locDataBad["frame"] == self._ui.framenoBox.value() - 1 dBad = self._locDataBad[sel] except Exception: pass markerList = [] for n, d in dBad.iterrows(): markerList.append(LocalizationMarker(d, Qt.red)) for n, d in dGood.iterrows(): markerList.append(LocalizationMarker(d, Qt.green)) self._locMarkers = self._scene.createItemGroup(markerList) @Slot() def autoIntensity(self): if self._imageData is None: return self._intensityMin = np.min(self._imageData) self._intensityMax = np.max(self._imageData) if self._intensityMin == self._intensityMax: if self._intensityMax == 0: self._intensityMax = 1 else: self._intensityMin = self._intensityMax - 1 self._ui.minSlider.setValue(self._intensityMin) self._ui.maxSlider.setValue(self._intensityMax) self.drawImage() @Slot(int) def setMinIntensity(self, v): self._intensityMin = min(v, self._intensityMax - 1) self._ui.minSlider.setValue(self._intensityMin) self.drawImage() @Slot(int) def setMaxIntensity(self, v): self._intensityMax = max(v, self._intensityMin + 1) self._ui.maxSlider.setValue(self._intensityMax) self.drawImage() currentFrameChanged = Signal() @Slot(int) def selectFrame(self, frameno): if self._ims is None: return try: self._imageData = self._ims[frameno - 1] except Exception: self.frameReadError.emit(frameno - 1) self.currentFrameChanged.emit() self.drawImage() self.drawLocalizations() frameReadError = Signal(int) @Slot() def nextFrame(self): if self._ims is None: return next = self._ui.framenoBox.value() + 1 if next > self._ui.framenoBox.maximum(): next = 1 self._ui.framenoBox.setValue(next) @Slot() def zoomIn(self): self._ui.view.scale(1.5, 1.5) @Slot() def zoomOut(self): self._ui.view.scale(2. / 3., 2. / 3.) @Slot() def zoomOriginal(self): self._ui.view.setTransform(QTransform()) @Slot() def zoomFit(self): self._ui.view.fitInView(self._scene.imageItem, Qt.KeepAspectRatio) def getCurrentFrame(self): return self._imageData @Property(int, doc="Number of the currently displayed frame") def currentFrameNumber(self): return self._ui.framenoBox.value() - 1 def _updateCurrentPixelInfo(self, x, y): if x >= self._imageData.shape[1] or y >= self._imageData.shape[0]: # Sometimes, when hitting the border of the image, the coordinates # are out of range return self._ui.posLabel.setText("({x}, {y})".format(x=x, y=y)) self._ui.intLabel.setText(locale.str(self._imageData[y, x])) @Slot(bool) def on_roiButton_toggled(self, checked): self._scene.roiMode = checked @Slot(bool) def on_scene_roiModeChanged(self, enabled): self._ui.roiButton.setChecked(enabled) def setShowLocalizations(self, show): self._ui.locButton.setChecked(show) showLocalizationsChanged = Signal(bool) @Property(bool, fset=setShowLocalizations, notify=showLocalizationsChanged) def showLocalizations(self): return self._ui.locButton.isChecked() def on_locButton_toggled(self, checked): tooltip = (self.tr(self._locEnabledStr) if checked else self.tr(self._locDisabledStr)) self._ui.locButton.setToolTip(tooltip) self.drawLocalizations()
def start_glue(gluefile=None, config=None, datafiles=None, maximized=True): """Run a glue session and exit Parameters ---------- gluefile : str An optional ``.glu`` file to restore. config : str An optional configuration file to use. datafiles : str An optional list of data files to load. maximized : bool Maximize screen on startup. Otherwise, use default size. """ import glue from glue.utils.qt import get_qapp app = get_qapp() splash = get_splash() splash.show() # Start off by loading plugins. We need to do this before restoring # the session or loading the configuration since these may use existing # plugins. load_plugins(splash=splash) from glue.app.qt import GlueApplication datafiles = datafiles or [] hub = None if gluefile is not None: app = restore_session(gluefile) return app.start() if config is not None: glue.env = glue.config.load_configuration(search_path=[config]) data_collection = glue.core.DataCollection() hub = data_collection.hub splash.set_progress(100) session = glue.core.Session(data_collection=data_collection, hub=hub) ga = GlueApplication(session=session) from qtpy.QtCore import QTimer timer = QTimer() timer.setInterval(1000) timer.setSingleShot(True) timer.timeout.connect(splash.close) timer.start() if datafiles: datasets = load_data_files(datafiles) ga.add_datasets(data_collection, datasets) return ga.start(maximized=maximized)
class DesignerHooks(object): """ Class that handles the integration with PyDM and the Qt Designer by hooking up slots to signals provided by FormEditor and other classes. """ __instance = None def __init__(self): if self.__initialized: return self.__form_editor = None self.__initialized = True self.__timer = None def __new__(cls, *args, **kwargs): if cls.__instance is None: cls.__instance = object.__new__(DesignerHooks) cls.__instance.__initialized = False return cls.__instance @property def form_editor(self): return self.__form_editor @form_editor.setter def form_editor(self, editor): if self.form_editor is not None: return if not editor: return self.__form_editor = editor self.setup_hooks() def setup_hooks(self): # Set PyDM to be read-only data_plugins.set_read_only(True) if self.form_editor: fwman = self.form_editor.formWindowManager() if fwman: fwman.formWindowAdded.connect( self.__new_form_added ) def __new_form_added(self, form_window_interface): style_data = stylesheet._get_style_data(None) widget = form_window_interface.formContainer() widget.setStyleSheet(style_data) if not self.__timer: self.__start_kicker() def __kick(self): fwman = self.form_editor.formWindowManager() if fwman: widget = fwman.activeFormWindow() if widget: widget.update() def __start_kicker(self): self.__timer = QTimer() self.__timer.setInterval(100) self.__timer.timeout.connect(self.__kick) self.__timer.start()
class ClientWidget(QWidget, SaveHistoryMixin, SpyderWidgetMixin): """ Client widget for the IPython Console This widget is necessary to handle the interaction between the plugin and each shell widget. """ sig_append_to_history_requested = Signal(str, str) sig_execution_state_changed = Signal() CONF_SECTION = 'ipython_console' SEPARATOR = '{0}## ---({1})---'.format(os.linesep*2, time.ctime()) INITHISTORY = ['# -*- coding: utf-8 -*-', '# *** Spyder Python Console History Log ***', ] def __init__(self, parent, id_, history_filename, config_options, additional_options, interpreter_versions, connection_file=None, hostname=None, context_menu_actions=(), menu_actions=None, is_external_kernel=False, is_spyder_kernel=True, given_name=None, give_focus=True, options_button=None, time_label=None, show_elapsed_time=False, reset_warning=True, ask_before_restart=True, ask_before_closing=False, css_path=None, handlers={}, stderr_obj=None, stdout_obj=None, fault_obj=None): super(ClientWidget, self).__init__(parent) SaveHistoryMixin.__init__(self, history_filename) # --- Init attrs self.container = parent self.id_ = id_ self.connection_file = connection_file self.hostname = hostname self.menu_actions = menu_actions self.is_external_kernel = is_external_kernel self.given_name = given_name self.show_elapsed_time = show_elapsed_time self.reset_warning = reset_warning self.ask_before_restart = ask_before_restart self.ask_before_closing = ask_before_closing # --- Other attrs self.context_menu_actions = context_menu_actions self.time_label = time_label self.options_button = options_button self.history = [] self.allow_rename = True self.is_error_shown = False self.error_text = None self.restart_thread = None self.give_focus = give_focus if css_path is None: self.css_path = CSS_PATH else: self.css_path = css_path # --- Widgets self.shellwidget = ShellWidget( config=config_options, ipyclient=self, additional_options=additional_options, interpreter_versions=interpreter_versions, is_external_kernel=is_external_kernel, is_spyder_kernel=is_spyder_kernel, handlers=handlers, local_kernel=True ) self.infowidget = self.container.infowidget self.blank_page = self._create_blank_page() self.loading_page = self._create_loading_page() # To keep a reference to the page to be displayed # in infowidget self.info_page = None self._before_prompt_is_ready() # Elapsed time self.t0 = time.monotonic() self.timer = QTimer(self) # --- Layout self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.shellwidget) if self.infowidget is not None: self.layout.addWidget(self.infowidget) self.setLayout(self.layout) # --- Exit function self.exit_callback = lambda: self.container.close_client(client=self) # --- Dialog manager self.dialog_manager = DialogManager() # --- Standard files handling self.stderr_obj = stderr_obj self.stdout_obj = stdout_obj self.fault_obj = fault_obj self.std_poll_timer = None if self.stderr_obj is not None or self.stdout_obj is not None: self.std_poll_timer = QTimer(self) self.std_poll_timer.timeout.connect(self.poll_std_file_change) self.std_poll_timer.setInterval(1000) self.std_poll_timer.start() self.shellwidget.executed.connect(self.poll_std_file_change) self.start_successful = False def __del__(self): """Close threads to avoid segfault.""" if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.quit() self.restart_thread.wait() # ----- Private methods --------------------------------------------------- def _before_prompt_is_ready(self, show_loading_page=True): """Configuration before kernel is connected.""" if show_loading_page: self._show_loading_page() self.shellwidget.sig_prompt_ready.connect( self._when_prompt_is_ready) # If remote execution, the loading page should be hidden as well self.shellwidget.sig_remote_execute.connect( self._when_prompt_is_ready) def _when_prompt_is_ready(self): """Configuration after the prompt is shown.""" self.start_successful = True # To hide the loading page self._hide_loading_page() # Show possible errors when setting Matplotlib backend self._show_mpl_backend_errors() # To show if special console is valid self._check_special_console_error() # Set the initial current working directory self._set_initial_cwd() self.shellwidget.sig_prompt_ready.disconnect( self._when_prompt_is_ready) self.shellwidget.sig_remote_execute.disconnect( self._when_prompt_is_ready) # It's necessary to do this at this point to avoid giving # focus to _control at startup. self._connect_control_signals() if self.give_focus: self.shellwidget._control.setFocus() def _create_loading_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(LOADING) loading_img = get_image_path('loading_sprites') if os.name == 'nt': loading_img = loading_img.replace('\\', '/') message = _("Connecting to kernel...") page = loading_template.substitute(css_path=self.css_path, loading_img=loading_img, message=message) return page def _create_blank_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(BLANK) page = loading_template.substitute(css_path=self.css_path) return page def _show_loading_page(self): """Show animation while the kernel is loading.""" if self.infowidget is not None: self.shellwidget.hide() self.infowidget.show() self.info_page = self.loading_page self.set_info_page() def _hide_loading_page(self): """Hide animation shown while the kernel is loading.""" if self.infowidget is not None: self.infowidget.hide() self.info_page = self.blank_page self.set_info_page() self.shellwidget.show() def _read_stderr(self): """Read the stderr file of the kernel.""" # We need to read stderr_file as bytes to be able to # detect its encoding with chardet f = open(self.stderr_file, 'rb') try: stderr_text = f.read() # This is needed to avoid showing an empty error message # when the kernel takes too much time to start. # See spyder-ide/spyder#8581. if not stderr_text: return '' # This is needed since the stderr file could be encoded # in something different to utf-8. # See spyder-ide/spyder#4191. encoding = get_coding(stderr_text) stderr_text = to_text_string(stderr_text, encoding) return stderr_text finally: f.close() def _show_mpl_backend_errors(self): """ Show possible errors when setting the selected Matplotlib backend. """ if self.shellwidget.is_spyder_kernel: self.shellwidget.call_kernel().show_mpl_backend_errors() def _check_special_console_error(self): """Check if the dependecies for special consoles are available.""" self.shellwidget.call_kernel( callback=self._show_special_console_error ).is_special_kernel_valid() def _show_special_console_error(self, missing_dependency): if missing_dependency is not None: error_message = _( "Your Python environment or installation doesn't have the " "<tt>{missing_dependency}</tt> module installed or it " "occurred a problem importing it. Due to that, it is not " "possible for Spyder to create this special console for " "you." ).format(missing_dependency=missing_dependency) self.show_kernel_error(error_message) def _abort_kernel_restart(self): """ Abort kernel restart if there are errors while starting it. We also ignore errors about comms, which are irrelevant. """ if self.start_successful: return False stderr = self.stderr_obj.get_contents() if not stderr: return False # There is an error. If it is benign, ignore. for line in stderr.splitlines(): if line and not self.is_benign_error(line): return True return False def _connect_control_signals(self): """Connect signals of control widgets.""" control = self.shellwidget._control page_control = self.shellwidget._page_control control.sig_focus_changed.connect( self.container.sig_focus_changed) page_control.sig_focus_changed.connect( self.container.sig_focus_changed) control.sig_visibility_changed.connect( self.container.refresh_container) page_control.sig_visibility_changed.connect( self.container.refresh_container) page_control.sig_show_find_widget_requested.connect( self.container.find_widget.show) def _set_initial_cwd(self): """Set initial cwd according to preferences.""" logger.debug("Setting initial working directory") cwd_path = get_home_dir() project_path = self.container.get_active_project_path() # This is for the first client if self.id_['int_id'] == '1': if self.get_conf( 'startup/use_project_or_home_directory', section='workingdir' ): cwd_path = get_home_dir() if project_path is not None: cwd_path = project_path elif self.get_conf( 'startup/use_fixed_directory', section='workingdir' ): cwd_path = self.get_conf( 'startup/fixed_directory', default=get_home_dir(), section='workingdir' ) else: # For new clients if self.get_conf( 'console/use_project_or_home_directory', section='workingdir' ): cwd_path = get_home_dir() if project_path is not None: cwd_path = project_path elif self.get_conf('console/use_cwd', section='workingdir'): cwd_path = self.container.get_working_directory() elif self.get_conf( 'console/use_fixed_directory', section='workingdir' ): cwd_path = self.get_conf( 'console/fixed_directory', default=get_home_dir(), section='workingdir' ) if osp.isdir(cwd_path): self.shellwidget.set_cwd(cwd_path) # ----- Public API -------------------------------------------------------- @property def kernel_id(self): """Get kernel id.""" if self.connection_file is not None: json_file = osp.basename(self.connection_file) return json_file.split('.json')[0] def remove_std_files(self, is_last_client=True): """Remove stderr_file associated with the client.""" try: self.shellwidget.executed.disconnect(self.poll_std_file_change) except TypeError: pass if self.std_poll_timer is not None: self.std_poll_timer.stop() if is_last_client: if self.stderr_obj is not None: self.stderr_obj.remove() if self.stdout_obj is not None: self.stdout_obj.remove() if self.fault_obj is not None: self.fault_obj.remove() @Slot() def poll_std_file_change(self): """Check if the stderr or stdout file just changed.""" self.shellwidget.call_kernel().flush_std() starting = self.shellwidget._starting if self.stderr_obj is not None: stderr = self.stderr_obj.poll_file_change() if stderr: if self.is_benign_error(stderr): return if self.shellwidget.isHidden(): # Avoid printing the same thing again if self.error_text != '<tt>%s</tt>' % stderr: full_stderr = self.stderr_obj.get_contents() self.show_kernel_error('<tt>%s</tt>' % full_stderr) if starting: self.shellwidget.banner = ( stderr + '\n' + self.shellwidget.banner) else: self.shellwidget._append_plain_text( '\n' + stderr, before_prompt=True) if self.stdout_obj is not None: stdout = self.stdout_obj.poll_file_change() if stdout: if starting: self.shellwidget.banner = ( stdout + '\n' + self.shellwidget.banner) else: self.shellwidget._append_plain_text( '\n' + stdout, before_prompt=True) def configure_shellwidget(self, give_focus=True): """Configure shellwidget after kernel is connected.""" self.give_focus = give_focus # Make sure the kernel sends the comm config over self.shellwidget.call_kernel()._send_comm_config() # Set exit callback self.shellwidget.set_exit_callback() # To save history self.shellwidget.executing.connect(self.add_to_history) # For Mayavi to run correctly self.shellwidget.executing.connect( self.shellwidget.set_backend_for_mayavi) # To update history after execution self.shellwidget.executed.connect(self.update_history) # To update the Variable Explorer after execution self.shellwidget.executed.connect( self.shellwidget.refresh_namespacebrowser) # To enable the stop button when executing a process self.shellwidget.executing.connect( self.sig_execution_state_changed) # To disable the stop button after execution stopped self.shellwidget.executed.connect( self.sig_execution_state_changed) # To show kernel restarted/died messages self.shellwidget.sig_kernel_restarted_message.connect( self.kernel_restarted_message) self.shellwidget.sig_kernel_restarted.connect( self._finalise_restart) # To correctly change Matplotlib backend interactively self.shellwidget.executing.connect( self.shellwidget.change_mpl_backend) # To show env and sys.path contents self.shellwidget.sig_show_syspath.connect(self.show_syspath) self.shellwidget.sig_show_env.connect(self.show_env) # To sync with working directory toolbar self.shellwidget.executed.connect(self.shellwidget.update_cwd) # To apply style self.set_color_scheme(self.shellwidget.syntax_style, reset=False) if self.fault_obj is not None: # To display faulthandler self.shellwidget.call_kernel().enable_faulthandler( self.fault_obj.filename) def add_to_history(self, command): """Add command to history""" if self.shellwidget.is_debugging(): return return super(ClientWidget, self).add_to_history(command) def is_client_executing(self): return (self.shellwidget._executing or self.shellwidget.is_waiting_pdb_input()) @Slot() def stop_button_click_handler(self): """Method to handle what to do when the stop button is pressed""" # Interrupt computations or stop debugging if not self.shellwidget.is_waiting_pdb_input(): self.interrupt_kernel() else: self.shellwidget.pdb_execute_command('exit') def show_kernel_error(self, error): """Show kernel initialization errors in infowidget.""" self.error_text = error if self.is_benign_error(error): return InstallerIPythonKernelError(error) # Replace end of line chars with <br> eol = sourcecode.get_eol_chars(error) if eol: error = error.replace(eol, '<br>') # Don't break lines in hyphens # From https://stackoverflow.com/q/7691569/438386 error = error.replace('-', '‑') # Create error page message = _("An error ocurred while starting the kernel") kernel_error_template = Template(KERNEL_ERROR) self.info_page = kernel_error_template.substitute( css_path=self.css_path, message=message, error=error) # Show error if self.infowidget is not None: self.set_info_page() self.shellwidget.hide() self.infowidget.show() # Tell the client we're in error mode self.is_error_shown = True # Stop shellwidget self.shellwidget.shutdown() self.remove_std_files(is_last_client=False) def is_benign_error(self, error): """Decide if an error is benign in order to filter it.""" benign_errors = [ # Error when switching from the Qt5 backend to the Tk one. # See spyder-ide/spyder#17488 "KeyboardInterrupt caught in kernel", "QSocketNotifier: Multiple socket notifiers for same socket", # Error when switching from the Tk backend to the Qt5 one. # See spyder-ide/spyder#17488 "Tcl_AsyncDelete async handler deleted by the wrong thread", "error in background error handler:", " while executing", '"::tcl::Bgerror', # Avoid showing this warning because it was up to the user to # disable secure writes. "WARNING: Insecure writes have been enabled via environment", # Old error "No such comm" ] return any([err in error for err in benign_errors]) def get_name(self): """Return client name""" if self.given_name is None: # Name according to host if self.hostname is None: name = _("Console") else: name = self.hostname # Adding id to name client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = name + u' ' + client_id elif self.given_name in ["Pylab", "SymPy", "Cython"]: client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = self.given_name + u' ' + client_id else: name = self.given_name + u'/' + self.id_['str_id'] return name def get_control(self): """Return the text widget (or similar) to give focus to""" # page_control is the widget used for paging page_control = self.shellwidget._page_control if page_control and page_control.isVisible(): return page_control else: return self.shellwidget._control def get_kernel(self): """Get kernel associated with this client""" return self.shellwidget.kernel_manager def add_actions_to_context_menu(self, menu): """Add actions to IPython widget context menu""" add_actions(menu, self.context_menu_actions) return menu def set_font(self, font): """Set IPython widget's font""" self.shellwidget._control.setFont(font) self.shellwidget.font = font def set_color_scheme(self, color_scheme, reset=True): """Set IPython color scheme.""" # Needed to handle not initialized kernel_client # See spyder-ide/spyder#6996. try: self.shellwidget.set_color_scheme(color_scheme, reset) except AttributeError: pass def shutdown(self, is_last_client): """Shutdown connection and kernel if needed.""" self.dialog_manager.close_all() if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.finished.disconnect() self.restart_thread.quit() self.restart_thread.wait() shutdown_kernel = ( is_last_client and not self.is_external_kernel and not self.is_error_shown) self.shellwidget.shutdown(shutdown_kernel) self.remove_std_files(shutdown_kernel) def interrupt_kernel(self): """Interrupt the associanted Spyder kernel if it's running""" # Needed to prevent a crash when a kernel is not running. # See spyder-ide/spyder#6299. try: self.shellwidget.request_interrupt_kernel() except RuntimeError: pass @Slot() def restart_kernel(self): """ Restart the associated kernel. Took this code from the qtconsole project Licensed under the BSD license """ sw = self.shellwidget if not running_under_pytest() and self.ask_before_restart: message = _('Are you sure you want to restart the kernel?') buttons = QMessageBox.Yes | QMessageBox.No result = QMessageBox.question(self, _('Restart kernel?'), message, buttons) else: result = None if (result == QMessageBox.Yes or running_under_pytest() or not self.ask_before_restart): if sw.kernel_manager: if self.infowidget is not None: if self.infowidget.isVisible(): self.infowidget.hide() if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return self._show_loading_page() # Close comm sw.spyder_kernel_comm.close() # Stop autorestart mechanism sw.kernel_manager.stop_restarter() sw.kernel_manager.autorestart = False # Reconfigure client before the new kernel is connected again. self._before_prompt_is_ready(show_loading_page=False) # Create and run restarting thread if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.finished.disconnect() self.restart_thread.quit() self.restart_thread.wait() self.restart_thread = QThread(None) self.restart_thread.run = self._restart_thread_main self.restart_thread.error = None self.restart_thread.finished.connect( lambda: self._finalise_restart(True)) self.restart_thread.start() else: sw._append_plain_text( _('Cannot restart a kernel not started by Spyder\n'), before_prompt=True ) self._hide_loading_page() def _restart_thread_main(self): """Restart the kernel in a thread.""" try: self.shellwidget.kernel_manager.restart_kernel( stderr=self.stderr_obj.handle, stdout=self.stdout_obj.handle) except RuntimeError as e: self.restart_thread.error = e def _finalise_restart(self, reset=False): """Finishes the restarting of the kernel.""" sw = self.shellwidget if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return if self.restart_thread and self.restart_thread.error is not None: sw._append_plain_text( _('Error restarting kernel: %s\n') % self.restart_thread.error, before_prompt=True ) else: if self.fault_obj is not None: fault = self.fault_obj.get_contents() if fault: fault = self.filter_fault(fault) self.shellwidget._append_plain_text( '\n' + fault, before_prompt=True) # Reset Pdb state and reopen comm sw._pdb_in_loop = False sw.spyder_kernel_comm.remove() try: sw.spyder_kernel_comm.open_comm(sw.kernel_client) except AttributeError: # An error occurred while opening our comm channel. # Aborting! return # Start autorestart mechanism sw.kernel_manager.autorestart = True sw.kernel_manager.start_restarter() # For spyder-ide/spyder#6235, IPython was changing the # setting of %colors on windows by assuming it was using a # dark background. This corrects it based on the scheme. self.set_color_scheme(sw.syntax_style, reset=reset) sw._append_html(_("<br>Restarting kernel...<br>"), before_prompt=True) sw.insert_horizontal_ruler() if self.fault_obj is not None: self.shellwidget.call_kernel().enable_faulthandler( self.fault_obj.filename) self._hide_loading_page() self.restart_thread = None self.sig_execution_state_changed.emit() def filter_fault(self, fault): """Get a fault from a previous session.""" thread_regex = ( r"(Current thread|Thread) " r"(0x[\da-f]+) \(most recent call first\):" r"(?:.|\r\n|\r|\n)+?(?=Current thread|Thread|\Z)") # Keep line for future improvments # files_regex = r"File \"([^\"]+)\", line (\d+) in (\S+)" main_re = "Main thread id:(?:\r\n|\r|\n)(0x[0-9a-f]+)" main_id = 0 for match in re.finditer(main_re, fault): main_id = int(match.group(1), base=16) system_re = ("System threads ids:" "(?:\r\n|\r|\n)(0x[0-9a-f]+(?: 0x[0-9a-f]+)+)") ignore_ids = [] start_idx = 0 for match in re.finditer(system_re, fault): ignore_ids = [int(i, base=16) for i in match.group(1).split()] start_idx = match.span()[1] text = "" for idx, match in enumerate(re.finditer(thread_regex, fault)): if idx == 0: text += fault[start_idx:match.span()[0]] thread_id = int(match.group(2), base=16) if thread_id != main_id: if thread_id in ignore_ids: continue if "wurlitzer.py" in match.group(0): # Wurlitzer threads are launched later continue text += "\n" + match.group(0) + "\n" else: try: pattern = (r".*(?:/IPython/core/interactiveshell\.py|" r"\\IPython\\core\\interactiveshell\.py).*") match_internal = next(re.finditer(pattern, match.group(0))) end_idx = match_internal.span()[0] except StopIteration: end_idx = None text += "\nMain thread:\n" + match.group(0)[:end_idx] + "\n" return text @Slot(str) def kernel_restarted_message(self, msg): """Show kernel restarted/died messages.""" if self.stderr_obj is not None: # If there are kernel creation errors, jupyter_client will # try to restart the kernel and qtconsole prints a # message about it. # So we read the kernel's stderr_file and display its # contents in the client instead of the usual message shown # by qtconsole. self.poll_std_file_change() else: self.shellwidget._append_html("<br>%s<hr><br>" % msg, before_prompt=False) @Slot() def enter_array_inline(self): """Enter and show the array builder on inline mode.""" self.shellwidget._control.enter_array_inline() @Slot() def enter_array_table(self): """Enter and show the array builder on table.""" self.shellwidget._control.enter_array_table() @Slot() def inspect_object(self): """Show how to inspect an object with our Help plugin""" self.shellwidget._control.inspect_current_object() @Slot() def clear_line(self): """Clear a console line""" self.shellwidget._keyboard_quit() @Slot() def clear_console(self): """Clear the whole console""" self.shellwidget.clear_console() @Slot() def reset_namespace(self): """Resets the namespace by removing all names defined by the user""" self.shellwidget.reset_namespace(warning=self.reset_warning, message=True) def update_history(self): self.history = self.shellwidget._history @Slot(object) def show_syspath(self, syspath): """Show sys.path contents.""" if syspath is not None: editor = CollectionsEditor(self) editor.setup(syspath, title="sys.path contents", readonly=True, icon=ima.icon('syspath')) self.dialog_manager.show(editor) else: return @Slot(object) def show_env(self, env): """Show environment variables.""" self.dialog_manager.show(RemoteEnvDialog(env, parent=self)) def show_time(self, end=False): """Text to show in time_label.""" if self.time_label is None: return elapsed_time = time.monotonic() - self.t0 # System time changed to past date, so reset start. if elapsed_time < 0: self.t0 = time.monotonic() elapsed_time = 0 if elapsed_time > 24 * 3600: # More than a day...! fmt = "%d %H:%M:%S" else: fmt = "%H:%M:%S" if end: color = QStylePalette.COLOR_TEXT_3 else: color = QStylePalette.COLOR_ACCENT_4 text = "<span style=\'color: %s\'><b>%s" \ "</b></span>" % (color, time.strftime(fmt, time.gmtime(elapsed_time))) if self.show_elapsed_time: self.time_label.setText(text) else: self.time_label.setText("") @Slot(bool) def set_show_elapsed_time(self, state): """Slot to show/hide elapsed time label.""" self.show_elapsed_time = state def set_info_page(self): """Set current info_page.""" if self.infowidget is not None and self.info_page is not None: self.infowidget.setHtml( self.info_page, QUrl.fromLocalFile(self.css_path) )
class _CondaAPI(QObject): """ """ ROOT_PREFIX = None ENCODING = 'ascii' UTF8 = 'utf-8' DEFAULT_CHANNELS = ['https://repo.continuum.io/pkgs/pro', 'https://repo.continuum.io/pkgs/free'] def __init__(self, parent=None): super(_CondaAPI, self).__init__() self._parent = parent self._queue = deque() self._timer = QTimer() self._current_worker = None self._workers = [] self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) self.set_root_prefix() def _clean(self): """ Periodically check for inactive workers and remove their references. """ if self._workers: for w in self._workers: if w.is_finished(): self._workers.remove(w) else: self._current_worker = None self._timer.stop() def _start(self): """ """ if len(self._queue) == 1: self._current_worker = self._queue.popleft() self._workers.append(self._current_worker) self._current_worker.start() self._timer.start() def is_active(self): """ Check if a worker is still active. """ return len(self._workers) == 0 def terminate_all_processes(self): """ Kill all working processes. """ for worker in self._workers: worker.close() # --- Conda api # ------------------------------------------------------------------------- def _call_conda(self, extra_args, abspath=True, parse=False, callback=None): """ Call conda with the list of extra arguments, and return the worker. The result can be force by calling worker.communicate(), which returns the tuple (stdout, stderr). """ if abspath: if sys.platform == 'win32': python = join(self.ROOT_PREFIX, 'python.exe') conda = join(self.ROOT_PREFIX, 'Scripts', 'conda-script.py') else: python = join(self.ROOT_PREFIX, 'bin/python') conda = join(self.ROOT_PREFIX, 'bin/conda') cmd_list = [python, conda] else: # Just use whatever conda is on the path cmd_list = ['conda'] cmd_list.extend(extra_args) process_worker = ProcessWorker(cmd_list, parse=parse, callback=callback) process_worker.sig_finished.connect(self._start) self._queue.append(process_worker) self._start() return process_worker def _call_and_parse(self, extra_args, abspath=True, callback=None): """ """ return self._call_conda(extra_args, abspath=abspath, parse=True, callback=callback) def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()): cmd_list = [] if kwargs.get('override_channels', False) and 'channel' not in kwargs: raise TypeError('conda search: override_channels requires channel') if 'env' in kwargs: cmd_list.extend(['--name', kwargs.pop('env')]) if 'prefix' in kwargs: cmd_list.extend(['--prefix', kwargs.pop('prefix')]) if 'channel' in kwargs: channel = kwargs.pop('channel') if isinstance(channel, str): cmd_list.extend(['--channel', channel]) else: cmd_list.append('--channel') cmd_list.extend(channel) for key in keys: if key in kwargs and kwargs[key]: cmd_list.append('--' + key.replace('_', '-')) return cmd_list def set_root_prefix(self, prefix=None): """ Set the prefix to the root environment (default is /opt/anaconda). This function should only be called once (right after importing conda_api). """ if prefix: self.ROOT_PREFIX = prefix else: # Find some conda instance, and then use info to get 'root_prefix' worker = self._call_and_parse(['info', '--json'], abspath=False) info = worker.communicate()[0] self.ROOT_PREFIX = info['root_prefix'] def get_conda_version(self): """ Return the version of conda being used (invoked) as a string. """ return self._call_conda(['--version'], callback=self._get_conda_version) def _get_conda_version(self, stdout, stderr): # argparse outputs version to stderr in Python < 3.4. # http://bugs.python.org/issue18920 pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)') m = pat.match(stderr.decode().strip()) if m is None: m = pat.match(stdout.decode().strip()) if m is None: raise Exception('output did not match: {0}'.format(stderr)) return m.group(1) def get_envs(self): """ Return all of the (named) environment (this does not include the root environment), as a list of absolute path to their prefixes. """ logger.debug('') # return self._call_and_parse(['info', '--json'], # callback=lambda o, e: o['envs']) envs = os.listdir(os.sep.join([self.ROOT_PREFIX, 'envs'])) envs = [os.sep.join([self.ROOT_PREFIX, 'envs', i]) for i in envs] valid_envs = [e for e in envs if os.path.isdir(e) and self.environment_exists(prefix=e)] return valid_envs def get_prefix_envname(self, name): """ Given the name of an environment return its full prefix path, or None if it cannot be found. """ prefix = None if name == 'root': prefix = self.ROOT_PREFIX # envs, error = self.get_envs().communicate() envs = self.get_envs() for p in envs: if basename(p) == name: prefix = p return prefix def linked(self, prefix): """ Return the (set of canonical names) of linked packages in `prefix`. """ logger.debug(str(prefix)) if not isdir(prefix): raise Exception('no such directory: {0}'.format(prefix)) meta_dir = join(prefix, 'conda-meta') if not isdir(meta_dir): # We might have nothing in linked (and no conda-meta directory) return set() return set(fn[:-5] for fn in os.listdir(meta_dir) if fn.endswith('.json')) def split_canonical_name(self, cname): """ Split a canonical package name into (name, version, build) strings. """ return tuple(cname.rsplit('-', 2)) def info(self, abspath=True): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ logger.debug(str('')) return self._call_and_parse(['info', '--json'], abspath=abspath) def package_info(self, package, abspath=True): """ Return a dictionary with package information. """ return self._call_and_parse(['info', package, '--json'], abspath=abspath) def search(self, regex=None, spec=None, **kwargs): """ Search for packages. """ cmd_list = ['search', '--json'] if regex and spec: raise TypeError('conda search: only one of regex or spec allowed') if regex: cmd_list.append(regex) if spec: cmd_list.extend(['--spec', spec]) if 'platform' in kwargs: cmd_list.extend(['--platform', kwargs.pop('platform')]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('canonical', 'unknown', 'use_index_cache', 'outdated', 'override_channels'))) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def create(self, name=None, prefix=None, pkgs=None, channels=None): """ Create an environment either by name or path with a specified set of packages. """ logger.debug(str((prefix, pkgs, channels))) # TODO: Fix temporal hack if not pkgs or not isinstance(pkgs, (list, tuple, str)): raise TypeError('must specify a list of one or more packages to ' 'install into new environment') cmd_list = ['create', '--yes', '--quiet', '--json', '--mkdir'] if name: ref = name search = [os.path.join(d, name) for d in self.info().communicate()[0]['envs_dirs']] cmd_list.extend(['--name', name]) elif prefix: ref = prefix search = [prefix] cmd_list.extend(['--prefix', prefix]) else: raise TypeError('must specify either an environment name or a ' 'path for new environment') if any(os.path.exists(prefix) for prefix in search): raise CondaEnvExistsError('Conda environment {0} already ' 'exists'.format(ref)) # TODO: Fix temporal hack if isinstance(pkgs, (list, tuple)): cmd_list.extend(pkgs) elif isinstance(pkgs, str): cmd_list.extend(['--file', pkgs]) # TODO: Check if correct if channels: cmd_list.extend(['--override-channels']) for channel in channels: cmd_list.extend(['--channel']) cmd_list.extend([channel]) return self._call_and_parse(cmd_list) def parse_token_channel(self, channel, token): """ Adapt a channel to include the authentication token of the logged user. Ignore default channels """ if token and channel not in self.DEFAULT_CHANNELS: url_parts = channel.split('/') start = url_parts[:-1] middle = 't/{0}'.format(token) end = url_parts[-1] token_channel = '{0}/{1}/{2}'.format('/'.join(start), middle, end) return token_channel else: return channel def install(self, name=None, prefix=None, pkgs=None, dep=True, channels=None, token=None): """ Install packages into an environment either by name or path with a specified set of packages. If token is specified, the channels different from the defaults will get the token appended. """ logger.debug(str((prefix, pkgs, channels))) # TODO: Fix temporal hack if not pkgs or not isinstance(pkgs, (list, tuple, str)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--yes', '--json', '--force-pscheck'] if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: # Just install into the current environment, whatever that is pass # TODO: Check if correct if channels: cmd_list.extend(['--override-channels']) for channel in channels: cmd_list.extend(['--channel']) channel = self.parse_token_channel(channel, token) cmd_list.extend([channel]) # TODO: Fix temporal hack if isinstance(pkgs, (list, tuple)): cmd_list.extend(pkgs) elif isinstance(pkgs, str): cmd_list.extend(['--file', pkgs]) if not dep: cmd_list.extend(['--no-deps']) return self._call_and_parse(cmd_list) def update(self, *pkgs, **kwargs): """ Update package(s) (in an environment) by name. """ cmd_list = ['update', '--json', '--quiet', '--yes'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to update, or " "all=True.") cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'no_deps', 'override_channels', 'no_pin', 'force', 'all', 'use_index_cache', 'use_local', 'alt_hint'))) cmd_list.extend(pkgs) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove(self, name=None, prefix=None, pkgs=None, all_=False): """ Remove a package (from an environment) by name. Returns { success: bool, (this is always true), (other information) } """ logger.debug(str((prefix, pkgs))) cmd_list = ['remove', '--json', '--quiet', '--yes'] if not pkgs and not all_: raise TypeError("Must specify at least one package to remove, or " "all=True.") if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: raise TypeError('must specify either an environment name or a ' 'path for package removal') if all_: cmd_list.extend(['--all']) else: cmd_list.extend(pkgs) return self._call_and_parse(cmd_list) def remove_environment(self, name=None, path=None, **kwargs): """ Remove an environment entirely. See ``remove``. """ return self.remove(name=name, path=path, all=True, **kwargs) def clone_environment(self, clone, name=None, prefix=None, **kwargs): """ Clone the environment `clone` into `name` or `prefix`. """ cmd_list = ['create', '--json', '--quiet'] if (name and prefix) or not (name or prefix): raise TypeError("conda clone_environment: exactly one of `name` " "or `path` required") if name: cmd_list.extend(['--name', name]) if prefix: cmd_list.extend(['--prefix', prefix]) cmd_list.extend(['--clone', clone]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'unknown', 'use_index_cache', 'use_local', 'no_pin', 'force', 'all', 'channel', 'override_channels', 'no_default_packages'))) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) # FIXME: def process(self, name=None, prefix=None, cmd=None): """ Create a Popen process for cmd using the specified args but in the conda environment specified by name or prefix. The returned object will need to be invoked with p.communicate() or similar. """ if bool(name) == bool(prefix): raise TypeError('exactly one of name or prefix must be specified') if not cmd: raise TypeError('cmd to execute must be specified') if not args: args = [] if name: prefix = self.get_prefix_envname(name) conda_env = dict(os.environ) sep = os.pathsep if sys.platform == 'win32': conda_env['PATH'] = join(prefix, 'Scripts') + sep + conda_env['PATH'] else: # Unix conda_env['PATH'] = join(prefix, 'bin') + sep + conda_env['PATH'] conda_env['PATH'] = prefix + os.pathsep + conda_env['PATH'] cmd_list = [cmd] cmd_list.extend(args) # = self.subprocess.process(cmd_list, env=conda_env, stdin=stdin, # stdout=stdout, stderr=stderr) def _setup_config_from_kwargs(self, kwargs): cmd_list = ['--json', '--force'] if 'file' in kwargs: cmd_list.extend(['--file', kwargs['file']]) if 'system' in kwargs: cmd_list.append('--system') return cmd_list def config_path(self, **kwargs): """ Get the path to the config file. """ cmd_list = ['config', '--get'] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o['rc_path']) def config_get(self, *keys, **kwargs): """ Get the values of configuration keys. Returns a dictionary of values. Note, the key may not be in the dictionary if the key wasn't set in the configuration file. """ cmd_list = ['config', '--get'] cmd_list.extend(keys) cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o['get']) def config_set(self, key, value, **kwargs): """ Set a key to a (bool) value. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--set', key, str(value)] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse( cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o.get('warnings', [])) def config_add(self, key, value, **kwargs): """ Add a value to a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--add', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse( cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o.get('warnings', [])) def config_remove(self, key, value, **kwargs): """ Remove a value from a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse( cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o.get('warnings', [])) def config_delete(self, key, **kwargs): """ Remove a key entirely. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove-key', key] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) return self._call_and_parse( cmd_list, abspath=kwargs.get('abspath', True), callback=lambda o, e: o.get('warnings', [])) def run(self, command, abspath=True): """ Launch the specified app by name or full package name. Returns a dictionary containing the key "fn", whose value is the full package (ending in ``.tar.bz2``) of the app. """ cmd_list = ['run', '--json', command] return self._call_and_parse(cmd_list, abspath=abspath) # --- Additional methods # ----------------------------------------------------------------------------- def dependencies(self, name=None, prefix=None, pkgs=None, channels=None, dep=True): """ Get dependenciy list for packages to be installed into an environment defined either by 'name' or 'prefix'. """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--dry-run', '--json', '--force-pscheck'] if not dep: cmd_list.extend(['--no-deps']) if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: pass cmd_list.extend(pkgs) # TODO: Check if correct if channels: cmd_list.extend(['--override-channels']) for channel in channels: cmd_list.extend(['--channel']) cmd_list.extend([channel]) return self._call_and_parse(cmd_list) def environment_exists(self, name=None, prefix=None, abspath=True): """ Check if an environment exists by 'name' or by 'prefix'. If query is by 'name' only the default conda environments directory is searched. """ logger.debug(str((name, prefix))) if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if name: prefix = self.get_prefix_envname(name) if prefix is None: prefix = self.ROOT_PREFIX return os.path.isdir(os.path.join(prefix, 'conda-meta')) def clear_lock(self, abspath=True): """ Clean any conda lock in the system. """ cmd_list = ['clean', '--lock', '--json'] return self._call_and_parse(cmd_list, abspath=abspath) def package_version(self, prefix=None, name=None, pkg=None): """ """ package_versions = {} if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if name: prefix = self.get_prefix_envname(name) if self.environment_exists(prefix=prefix): for package in self.linked(prefix): if pkg in package: n, v, b = self.split_canonical_name(package) package_versions[n] = v return package_versions.get(pkg, None) def get_platform(self): """ Get platform of current system (system and bitness). """ _sys_map = {'linux2': 'linux', 'linux': 'linux', 'darwin': 'osx', 'win32': 'win', 'openbsd5': 'openbsd'} non_x86_linux_machines = {'armv6l', 'armv7l', 'ppc64le'} sys_platform = _sys_map.get(sys.platform, 'unknown') bits = 8 * tuple.__itemsize__ if (sys_platform == 'linux' and platform.machine() in non_x86_linux_machines): arch_name = platform.machine() subdir = 'linux-{0}'.format(arch_name) else: arch_name = {64: 'x86_64', 32: 'x86'}[bits] subdir = '{0}-{1}'.format(sys_platform, bits) return subdir def get_condarc_channels(self): """ Returns all the channel urls defined in .condarc using the defined `channel_alias`. If no condarc file is found, use the default channels. """ # First get the location of condarc file and parse it to get # the channel alias and the channels. default_channel_alias = 'https://conda.anaconda.org' default_urls = ['https://repo.continuum.io/pkgs/free', 'https://repo.continuum.io/pkgs/pro'] condarc_path = os.path.abspath(os.path.expanduser('~/.condarc')) channels = default_urls[:] if not os.path.isfile(condarc_path): condarc = None channel_alias = default_channel_alias else: with open(condarc_path, 'r') as f: data = f.read() condarc = yaml.load(data) channels += condarc.get('channels', []) channel_alias = condarc.get('channel_alias', default_channel_alias) if channel_alias[-1] == '/': template = "{0}{1}" else: template = "{0}/{1}" if 'defaults' in channels: channels.remove('defaults') channel_urls = [] for channel in channels: if not channel.startswith('http'): channel_url = template.format(channel_alias, channel) else: channel_url = channel channel_urls.append(channel_url) return channel_urls # --- Pip commands # ------------------------------------------------------------------------- def _call_pip(self, name=None, prefix=None, extra_args=None, callback=None): """ """ cmd_list = self._pip_cmd(name=name, prefix=prefix) cmd_list.extend(extra_args) process_worker = ProcessWorker(cmd_list, pip=True, callback=callback) process_worker.sig_finished.connect(self._start) self._queue.append(process_worker) self._start() return process_worker def _pip_cmd(self, name=None, prefix=None): """ Get pip location based on environment `name` or `prefix`. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' " "required.") if name and self.environment_exists(name=name): prefix = self.get_prefix_envname(name) if sys.platform == 'win32': python = join(prefix, 'python.exe') # FIXME: pip = join(prefix, 'pip.exe') # FIXME: else: python = join(prefix, 'bin/python') pip = join(prefix, 'bin/pip') cmd_list = [python, pip] return cmd_list def pip_list(self, name=None, prefix=None, abspath=True): """ Get list of pip installed packages. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' " "required.") if name: prefix = self.get_prefix_envname(name) pip_command = os.sep.join([prefix, 'bin', 'python']) cmd_list = [pip_command, PIP_LIST_SCRIPT] process_worker = ProcessWorker(cmd_list, pip=True, parse=True, callback=self._pip_list, extra_kwargs={'prefix': prefix}) process_worker.sig_finished.connect(self._start) self._queue.append(process_worker) self._start() return process_worker # if name: # cmd_list = ['list', '--name', name] # if prefix: # cmd_list = ['list', '--prefix', prefix] # return self._call_conda(cmd_list, abspath=abspath, # callback=self._pip_list) def _pip_list(self, stdout, stderr, prefix=None): """ """ result = stdout # A dict linked = self.linked(prefix) pip_only = [] linked_names = [self.split_canonical_name(l)[0] for l in linked] for pkg in result: name = self.split_canonical_name(pkg)[0] if name not in linked_names: pip_only.append(pkg) # FIXME: NEED A MORE ROBUST WAY! # if '<pip>' in line and '#' not in line: # temp = line.split()[:-1] + ['pip'] # temp = '-'.join(temp) # if '-(' in temp: # start = temp.find('-(') # end = temp.find(')') # substring = temp[start:end+1] # temp = temp.replace(substring, '') # result.append(temp) return pip_only def pip_remove(self, name=None, prefix=None, pkgs=None): """ Remove a pip package in given environment by `name` or `prefix`. """ logger.debug(str((prefix, pkgs))) if isinstance(pkgs, list) or isinstance(pkgs, tuple): pkg = ' '.join(pkgs) else: pkg = pkgs extra_args = ['uninstall', '--yes', pkg] return self._call_pip(name=name, prefix=prefix, extra_args=extra_args) def pip_search(self, search_string=None): """ Search for pip installable python packages in PyPI matching `search_string`. """ extra_args = ['search', search_string] return self._call_pip(name='root', extra_args=extra_args, callback=self._pip_search) # if stderr: # raise PipError(stderr) # You are using pip version 7.1.2, however version 8.0.2 is available. # You should consider upgrading via the 'pip install --upgrade pip' # command. def _pip_search(self, stdout, stderr): result = {} lines = to_text_string(stdout).split('\n') while '' in lines: lines.remove('') for line in lines: if ' - ' in line: parts = line.split(' - ') name = parts[0].strip() description = parts[1].strip() result[name] = description return result
class BaseTimerStatus(StatusBarWidget): """Status bar widget base for widgets that update based on timers.""" TITLE = None TIP = None def __init__(self, parent, statusbar): """Status bar widget base for widgets that update based on timers.""" super(BaseTimerStatus, self).__init__(parent, statusbar) # Widgets self.label = QLabel(self.TITLE) self.value = QLabel() # Widget setup self.setToolTip(self.TIP) self.value.setAlignment(Qt.AlignRight) self.value.setFont(self.label_font) fm = self.value.fontMetrics() self.value.setMinimumWidth(fm.width('000%')) # Layout layout = self.layout() layout.addWidget(self.label) layout.addWidget(self.value) layout.addSpacing(20) # Setup if self.is_supported(): self.timer = QTimer() self.timer.timeout.connect(self.update_label) self.timer.start(2000) else: self.timer = None self.hide() def set_interval(self, interval): """Set timer interval (ms).""" if self.timer is not None: self.timer.setInterval(interval) def import_test(self): """Raise ImportError if feature is not supported.""" raise NotImplementedError def is_supported(self): """Return True if feature is supported.""" try: self.import_test() return True except ImportError: return False def get_value(self): """Return value (e.g. CPU or memory usage).""" raise NotImplementedError def update_label(self): """Update status label widget, if widget is visible.""" if self.isVisible(): self.value.setText('%d %%' % self.get_value())
class FindReplace(QWidget): """Find widget""" STYLE = {False: "background-color:rgb(255, 175, 90);", True: "", None: "", 'regexp_error': "background-color:rgb(255, 80, 80);", } TOOLTIP = {False: _("No matches"), True: _("Search string"), None: _("Search string"), 'regexp_error': _("Regular expression error") } visibility_changed = Signal(bool) return_shift_pressed = Signal() return_pressed = Signal() def __init__(self, parent, enable_replace=False): QWidget.__init__(self, parent) self.enable_replace = enable_replace self.editor = None self.is_code_editor = None glayout = QGridLayout() glayout.setContentsMargins(0, 0, 0, 0) self.setLayout(glayout) self.close_button = create_toolbutton(self, triggered=self.hide, icon=ima.icon('DialogCloseButton')) glayout.addWidget(self.close_button, 0, 0) # Find layout self.search_text = PatternComboBox(self, tip=_("Search string"), adjust_to_minimum=False) self.return_shift_pressed.connect( lambda: self.find(changed=False, forward=False, rehighlight=False, multiline_replace_check = False)) self.return_pressed.connect( lambda: self.find(changed=False, forward=True, rehighlight=False, multiline_replace_check = False)) self.search_text.lineEdit().textEdited.connect( self.text_has_been_edited) self.number_matches_text = QLabel(self) self.previous_button = create_toolbutton(self, triggered=self.find_previous, icon=ima.icon('ArrowUp')) self.next_button = create_toolbutton(self, triggered=self.find_next, icon=ima.icon('ArrowDown')) self.next_button.clicked.connect(self.update_search_combo) self.previous_button.clicked.connect(self.update_search_combo) self.re_button = create_toolbutton(self, icon=get_icon('regexp.svg'), tip=_("Regular expression")) self.re_button.setCheckable(True) self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton(self, icon=get_icon("upper_lower.png"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) self.case_button.toggled.connect(lambda state: self.find()) self.words_button = create_toolbutton(self, icon=get_icon("whole_words.png"), tip=_("Whole words")) self.words_button.setCheckable(True) self.words_button.toggled.connect(lambda state: self.find()) self.highlight_button = create_toolbutton(self, icon=get_icon("highlight.png"), tip=_("Highlight matches")) self.highlight_button.setCheckable(True) self.highlight_button.toggled.connect(self.toggle_highlighting) hlayout = QHBoxLayout() self.widgets = [self.close_button, self.search_text, self.number_matches_text, self.previous_button, self.next_button, self.re_button, self.case_button, self.words_button, self.highlight_button] for widget in self.widgets[1:]: hlayout.addWidget(widget) glayout.addLayout(hlayout, 0, 1) # Replace layout replace_with = QLabel(_("Replace with:")) self.replace_text = PatternComboBox(self, adjust_to_minimum=False, tip=_('Replace string')) self.replace_text.valid.connect( lambda _: self.replace_find(focus_replace_text=True)) self.replace_button = create_toolbutton(self, text=_('Replace/find next'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find, text_beside_icon=True) self.replace_sel_button = create_toolbutton(self, text=_('Replace selection'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find_selection, text_beside_icon=True) self.replace_sel_button.clicked.connect(self.update_replace_combo) self.replace_sel_button.clicked.connect(self.update_search_combo) self.replace_all_button = create_toolbutton(self, text=_('Replace all'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find_all, text_beside_icon=True) self.replace_all_button.clicked.connect(self.update_replace_combo) self.replace_all_button.clicked.connect(self.update_search_combo) self.replace_layout = QHBoxLayout() widgets = [replace_with, self.replace_text, self.replace_button, self.replace_sel_button, self.replace_all_button] for widget in widgets: self.replace_layout.addWidget(widget) glayout.addLayout(self.replace_layout, 1, 1) self.widgets.extend(widgets) self.replace_widgets = widgets self.hide_replace() self.search_text.setTabOrder(self.search_text, self.replace_text) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.shortcuts = self.create_shortcuts(parent) self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(1000) self.highlight_timer.timeout.connect(self.highlight_matches) self.search_text.installEventFilter(self) def eventFilter(self, widget, event): """Event filter for search_text widget. Emits signals when presing Enter and Shift+Enter. This signals are used for search forward and backward. Also, a crude hack to get tab working in the Find/Replace boxes. """ if event.type() == QEvent.KeyPress: key = event.key() shift = event.modifiers() & Qt.ShiftModifier if key == Qt.Key_Return: if shift: self.return_shift_pressed.emit() else: self.return_pressed.emit() if key == Qt.Key_Tab: if self.search_text.hasFocus(): self.replace_text.set_current_text( self.search_text.currentText()) self.focusNextChild() return super(FindReplace, self).eventFilter(widget, event) def create_shortcuts(self, parent): """Create shortcuts for this widget""" # Configurable findnext = config_shortcut(self.find_next, context='_', name='Find next', parent=parent) findprev = config_shortcut(self.find_previous, context='_', name='Find previous', parent=parent) togglefind = config_shortcut(self.show, context='_', name='Find text', parent=parent) togglereplace = config_shortcut(self.show_replace, context='_', name='Replace text', parent=parent) hide = config_shortcut(self.hide, context='_', name='hide find and replace', parent=self) return [findnext, findprev, togglefind, togglereplace, hide] def get_shortcut_data(self): """ Returns shortcut data, a list of tuples (shortcut, text, default) shortcut (QShortcut or QAction instance) text (string): action/shortcut description default (string): default key sequence """ return [sc.data for sc in self.shortcuts] def update_search_combo(self): self.search_text.lineEdit().returnPressed.emit() def update_replace_combo(self): self.replace_text.lineEdit().returnPressed.emit() def toggle_replace_widgets(self): if self.enable_replace: # Toggle replace widgets if self.replace_widgets[0].isVisible(): self.hide_replace() self.hide() else: self.show_replace() if len(to_text_string(self.search_text.currentText()))>0: self.replace_text.setFocus() @Slot(bool) def toggle_highlighting(self, state): """Toggle the 'highlight all results' feature""" if self.editor is not None: if state: self.highlight_matches() else: self.clear_matches() def show(self, hide_replace=True): """Overrides Qt Method""" QWidget.show(self) self.visibility_changed.emit(True) self.change_number_matches() if self.editor is not None: if hide_replace: if self.replace_widgets[0].isVisible(): self.hide_replace() text = self.editor.get_selected_text() # When selecting several lines, and replace box is activated the # text won't be replaced for the selection if hide_replace or len(text.splitlines())<=1: highlighted = True # If no text is highlighted for search, use whatever word is # under the cursor if not text: highlighted = False try: cursor = self.editor.textCursor() cursor.select(QTextCursor.WordUnderCursor) text = to_text_string(cursor.selectedText()) except AttributeError: # We can't do this for all widgets, e.g. WebView's pass # Now that text value is sorted out, use it for the search if text and not self.search_text.currentText() or highlighted: self.search_text.setEditText(text) self.search_text.lineEdit().selectAll() self.refresh() else: self.search_text.lineEdit().selectAll() self.search_text.setFocus() @Slot() def hide(self): """Overrides Qt Method""" for widget in self.replace_widgets: widget.hide() QWidget.hide(self) self.visibility_changed.emit(False) if self.editor is not None: self.editor.setFocus() self.clear_matches() def show_replace(self): """Show replace widgets""" self.show(hide_replace=False) for widget in self.replace_widgets: widget.show() def hide_replace(self): """Hide replace widgets""" for widget in self.replace_widgets: widget.hide() def refresh(self): """Refresh widget""" if self.isHidden(): if self.editor is not None: self.clear_matches() return state = self.editor is not None for widget in self.widgets: widget.setEnabled(state) if state: self.find() def set_editor(self, editor, refresh=True): """ Set associated editor/web page: codeeditor.base.TextEditBaseWidget browser.WebView """ self.editor = editor # Note: This is necessary to test widgets/editor.py # in Qt builds that don't have web widgets try: from qtpy.QtWebEngineWidgets import QWebEngineView except ImportError: QWebEngineView = type(None) self.words_button.setVisible(not isinstance(editor, QWebEngineView)) self.re_button.setVisible(not isinstance(editor, QWebEngineView)) from spyder.plugins.editor.widgets.codeeditor import CodeEditor self.is_code_editor = isinstance(editor, CodeEditor) self.highlight_button.setVisible(self.is_code_editor) if refresh: self.refresh() if self.isHidden() and editor is not None: self.clear_matches() @Slot() def find_next(self): """Find next occurrence""" state = self.find(changed=False, forward=True, rehighlight=False, multiline_replace_check=False) self.editor.setFocus() self.search_text.add_current_text() return state @Slot() def find_previous(self): """Find previous occurrence""" state = self.find(changed=False, forward=False, rehighlight=False, multiline_replace_check=False) self.editor.setFocus() return state def text_has_been_edited(self, text): """Find text has been edited (this slot won't be triggered when setting the search pattern combo box text programmatically)""" self.find(changed=True, forward=True, start_highlight_timer=True) def highlight_matches(self): """Highlight found results""" if self.is_code_editor and self.highlight_button.isChecked(): text = self.search_text.currentText() words = self.words_button.isChecked() regexp = self.re_button.isChecked() self.editor.highlight_found_results(text, words=words, regexp=regexp) def clear_matches(self): """Clear all highlighted matches""" if self.is_code_editor: self.editor.clear_found_results() def find(self, changed=True, forward=True, rehighlight=True, start_highlight_timer=False, multiline_replace_check=True): """Call the find function""" # When several lines are selected in the editor and replace box is activated, # dynamic search is deactivated to prevent changing the selection. Otherwise # we show matching items. if multiline_replace_check and self.replace_widgets[0].isVisible() and \ len(to_text_string(self.editor.get_selected_text()).splitlines())>1: return None text = self.search_text.currentText() if len(text) == 0: self.search_text.lineEdit().setStyleSheet("") if not self.is_code_editor: # Clears the selection for WebEngine self.editor.find_text('') self.change_number_matches() return None else: case = self.case_button.isChecked() words = self.words_button.isChecked() regexp = self.re_button.isChecked() found = self.editor.find_text(text, changed, forward, case=case, words=words, regexp=regexp) stylesheet = self.STYLE[found] tooltip = self.TOOLTIP[found] if not found and regexp: error_msg = regexp_error_msg(text) if error_msg: # special styling for regexp errors stylesheet = self.STYLE['regexp_error'] tooltip = self.TOOLTIP['regexp_error'] + ': ' + error_msg self.search_text.lineEdit().setStyleSheet(stylesheet) self.search_text.setToolTip(tooltip) if self.is_code_editor and found: block = self.editor.textCursor().block() TextHelper(self.editor).unfold_if_colapsed(block) if rehighlight or not self.editor.found_results: self.highlight_timer.stop() if start_highlight_timer: self.highlight_timer.start() else: self.highlight_matches() else: self.clear_matches() number_matches = self.editor.get_number_matches(text, case=case, regexp=regexp) if hasattr(self.editor, 'get_match_number'): match_number = self.editor.get_match_number(text, case=case, regexp=regexp) else: match_number = 0 self.change_number_matches(current_match=match_number, total_matches=number_matches) return found @Slot() def replace_find(self, focus_replace_text=False, replace_all=False): """Replace and find""" if (self.editor is not None): replace_text = to_text_string(self.replace_text.currentText()) search_text = to_text_string(self.search_text.currentText()) re_pattern = None # Check regexp before proceeding if self.re_button.isChecked(): try: re_pattern = re.compile(search_text) # Check if replace_text can be substituted in re_pattern # Fixes issue #7177 re_pattern.sub(replace_text, '') except re.error: # Do nothing with an invalid regexp return case = self.case_button.isChecked() first = True cursor = None while True: if first: # First found seltxt = to_text_string(self.editor.get_selected_text()) cmptxt1 = search_text if case else search_text.lower() cmptxt2 = seltxt if case else seltxt.lower() if re_pattern is None: has_selected = self.editor.has_selected_text() if has_selected and cmptxt1 == cmptxt2: # Text was already found, do nothing pass else: if not self.find(changed=False, forward=True, rehighlight=False): break else: if len(re_pattern.findall(cmptxt2)) > 0: pass else: if not self.find(changed=False, forward=True, rehighlight=False): break first = False wrapped = False position = self.editor.get_position('cursor') position0 = position cursor = self.editor.textCursor() cursor.beginEditBlock() else: position1 = self.editor.get_position('cursor') if is_position_inf(position1, position0 + len(replace_text) - len(search_text) + 1): # Identify wrapping even when the replace string # includes part of the search string wrapped = True if wrapped: if position1 == position or \ is_position_sup(position1, position): # Avoid infinite loop: replace string includes # part of the search string break if position1 == position0: # Avoid infinite loop: single found occurrence break position0 = position1 if re_pattern is None: cursor.removeSelectedText() cursor.insertText(replace_text) else: seltxt = to_text_string(cursor.selectedText()) cursor.removeSelectedText() cursor.insertText(re_pattern.sub(replace_text, seltxt)) if self.find_next(): found_cursor = self.editor.textCursor() cursor.setPosition(found_cursor.selectionStart(), QTextCursor.MoveAnchor) cursor.setPosition(found_cursor.selectionEnd(), QTextCursor.KeepAnchor) else: break if not replace_all: break if cursor is not None: cursor.endEditBlock() if focus_replace_text: self.replace_text.setFocus() @Slot() def replace_find_all(self, focus_replace_text=False): """Replace and find all matching occurrences""" self.replace_find(focus_replace_text, replace_all=True) @Slot() def replace_find_selection(self, focus_replace_text=False): """Replace and find in the current selection""" if self.editor is not None: replace_text = to_text_string(self.replace_text.currentText()) search_text = to_text_string(self.search_text.currentText()) case = self.case_button.isChecked() words = self.words_button.isChecked() re_flags = re.MULTILINE if case else re.IGNORECASE|re.MULTILINE re_pattern = None if self.re_button.isChecked(): pattern = search_text else: pattern = re.escape(search_text) replace_text = re.escape(replace_text) if words: # match whole words only pattern = r'\b{pattern}\b'.format(pattern=pattern) # Check regexp before proceeding try: re_pattern = re.compile(pattern, flags=re_flags) # Check if replace_text can be substituted in re_pattern # Fixes issue #7177 re_pattern.sub(replace_text, '') except re.error as e: # Do nothing with an invalid regexp return selected_text = to_text_string(self.editor.get_selected_text()) replacement = re_pattern.sub(replace_text, selected_text) if replacement != selected_text: cursor = self.editor.textCursor() cursor.beginEditBlock() cursor.removeSelectedText() if not self.re_button.isChecked(): replacement = re.sub(r'\\(?![nrtf])(.)', r'\1', replacement) cursor.insertText(replacement) cursor.endEditBlock() if focus_replace_text: self.replace_text.setFocus() else: self.editor.setFocus() def change_number_matches(self, current_match=0, total_matches=0): """Change number of match and total matches.""" if current_match and total_matches: matches_string = u"{} {} {}".format(current_match, _(u"of"), total_matches) self.number_matches_text.setText(matches_string) elif total_matches: matches_string = u"{} {}".format(total_matches, _(u"matches")) self.number_matches_text.setText(matches_string) else: self.number_matches_text.setText(_(u"no matches"))
class PyDMApplication(QApplication): """ PyDMApplication handles loading PyDM display files, opening new windows, and most importantly, establishing and managing connections to channels via data plugins. Parameters ---------- ui_file : str, optional The file path to a PyDM display file (.ui or .py). command_line_args : list, optional A list of strings representing arguments supplied at the command line. All arguments in this list are handled by QApplication, in addition to PyDMApplication. display_args : list, optional A list of command line arguments that should be forwarded to the Display class. This is only useful if a Related Display Button is opening up a .py file with extra arguments specified, and probably isn't something you will ever need to use when writing code that instantiates PyDMApplication. perfmon : bool, optional Whether or not to enable performance monitoring using 'psutil'. When enabled, CPU load information on a per-thread basis is periodically printed to the terminal. hide_nav_bar : bool, optional Whether or not to display the navigation bar (forward/back/home buttons) when the main window is first displayed. hide_menu_bar: bool, optional Whether or not to display the menu bar (File, View) when the main window is first displayed. hide_status_bar: bool, optional Whether or not to display the status bar (general messages and errors) when the main window is first displayed. read_only: bool, optional Whether or not to launch PyDM in a read-only state. macros : dict, optional A dictionary of macro variables to be forwarded to the display class being loaded. use_main_window : bool, optional If ui_file is note given, this parameter controls whether or not to create a PyDMMainWindow in the initialization (Default is True). fullscreen : bool, optional Whether or not to launch PyDM in a full screen mode. """ # Instantiate our plugins. plugins = data_plugins.plugin_modules def __init__(self, ui_file=None, command_line_args=[], display_args=[], perfmon=False, hide_nav_bar=False, hide_menu_bar=False, hide_status_bar=False, read_only=False, macros=None, use_main_window=True, stylesheet_path=None, fullscreen=False): super(PyDMApplication, self).__init__(command_line_args) # Enable High DPI display, if available. if hasattr(Qt, 'AA_UseHighDpiPixmaps'): self.setAttribute(Qt.AA_UseHighDpiPixmaps) # The macro and directory stacks are needed for nested displays (usually PyDMEmbeddedDisplays). # During the process of loading a display (whether from a .ui file, or a .py file), the application's # 'open_file' method will be called recursively. Inside open_file, the last item on the stack represents # the parent widget's file path and macro variables. Any file paths are joined to the end of the parent's # file path, and any macros are merged with the parent's macros. This system depends on open_file always # being called hierarchially (i.e., parent calls it first, then on down the ancestor tree, with no unrelated # calls in between). If something crazy happens and PyDM somehow gains the ability to open files in a # multi-threaded way, for example, this system will fail. data_plugins.set_read_only(read_only) self.main_window = None self.directory_stack = [''] self.macro_stack = [{}] self.windows = {} self.display_args = display_args self.hide_nav_bar = hide_nav_bar self.hide_menu_bar = hide_menu_bar self.hide_status_bar = hide_status_bar self.fullscreen = fullscreen # Open a window if required. if ui_file is not None: self.make_main_window(stylesheet_path=stylesheet_path) self.make_window(ui_file, macros, command_line_args) elif use_main_window: self.make_main_window(stylesheet_path=stylesheet_path) self.had_file = ui_file is not None # Re-enable sigint (usually blocked by pyqt) signal.signal(signal.SIGINT, signal.SIG_DFL) # Performance monitoring if perfmon: import psutil self.perf = psutil.Process() self.perf_timer = QTimer() self.perf_timer.setInterval(2000) self.perf_timer.timeout.connect(self.get_CPU_usage) self.perf_timer.start() def get_string_encoding(self): return os.getenv("PYDM_STRING_ENCODING", "utf_8") def exec_(self): """ Execute the QApplication. """ return super(PyDMApplication, self).exec_() def is_read_only(self): warnings.warn("'PyDMApplication.is_read_only' is deprecated, " "use 'pydm.data_plugins.is_read_only' instead.") return data_plugins.is_read_only() @Slot() def get_CPU_usage(self): """ Prints total CPU usage (in percent), as well as per-thread usage, to the terminal. """ with self.perf.oneshot(): total_percent = self.perf.cpu_percent(interval=None) total_time = sum(self.perf.cpu_times()) usage = [total_percent * ((t.system_time + t.user_time) / total_time) for t in self.perf.threads()] print("Total: {tot}, Per Thread: {percpu}".format(tot=total_percent, percpu=usage)) def new_pydm_process(self, ui_file, macros=None, command_line_args=None): """ Spawn a new PyDM process and open the supplied file. Commands to open new windows in PyDM typically actually spawn an entirely new PyDM process. This keeps each window isolated, so that one window cannot slow down or crash another. Parameters ---------- ui_file : str The path to a .ui or .py file to open in the new process. macros : dict, optional A dictionary of macro variables to supply to the display file to be opened. command_line_args : list, optional A list of command line arguments to pass to the new process. Typically, this argument is used by related display buttons to pass in extra arguments. It is probably rare that code you write needs to use this argument. """ # Expand user (~ or ~user) and environment variables. ui_file = os.path.expanduser(os.path.expandvars(ui_file)) base_dir, fname, args = path_info(str(ui_file)) filepath = os.path.join(base_dir, fname) filepath_args = args pydm_display_app_path = which("pydm") if pydm_display_app_path is None: if os.environ.get("PYDM_PATH") is not None: pydm_display_app_path = os.path.join(os.environ["PYDM_PATH"], "pydm") else: # Not in the PATH and no ENV VAR pointing to it... # Let's try the script folder... pydm_display_app_path = os.path.join(os.path.split(os.path.realpath(__file__))[0], "..", "scripts", "pydm") args = [pydm_display_app_path] if self.hide_nav_bar: args.extend(["--hide-nav-bar"]) if self.hide_menu_bar: args.extend(["--hide-menu-bar"]) if self.hide_status_bar: args.extend(["--hide-status-bar"]) if self.fullscreen: args.extend(["--fullscreen"]) if macros is not None: args.extend(["-m", json.dumps(macros)]) args.append(filepath) args.extend(self.display_args) args.extend(filepath_args) if command_line_args is not None: args.extend(command_line_args) subprocess.Popen(args, shell=False) def new_window(self, ui_file, macros=None, command_line_args=None): """ Make a new window and open the supplied file. Currently, this method just calls `new_pydm_process`. This is an internal method that typically will not be needed by users. Parameters ---------- ui_file : str The path to a .ui or .py file to open in the new process. macros : dict, optional A dictionary of macro variables to supply to the display file to be opened. command_line_args : list, optional A list of command line arguments to pass to the new process. Typically, this argument is used by related display buttons to pass in extra arguments. It is probably rare that code you write needs to use this argument. """ # All new windows are spawned as new processes. self.new_pydm_process(ui_file, macros, command_line_args) def make_main_window(self, stylesheet_path=None): """ Instantiate a new PyDMMainWindow, add it to the application's list of windows. Typically, this function is only called as part of starting up a new process, because PyDMApplications only have one window per process. """ main_window = PyDMMainWindow(hide_nav_bar=self.hide_nav_bar, hide_menu_bar=self.hide_menu_bar, hide_status_bar=self.hide_status_bar) self.main_window = main_window apply_stylesheet(stylesheet_path, widget=self.main_window) self.main_window.update_tools_menu() if self.fullscreen: main_window.enter_fullscreen() else: main_window.show() # If we are launching a new window, we don't want it to sit right on top of an existing window. if len(self.windows) > 1: main_window.move(main_window.x() + 10, main_window.y() + 10) def make_window(self, ui_file, macros=None, command_line_args=None): """ Open the ui_file in the window. Parameters ---------- ui_file : str The path to a .ui or .py file to open in the new process. macros : dict, optional A dictionary of macro variables to supply to the display file to be opened. command_line_args : list, optional A list of command line arguments to pass to the new process. Typically, this argument is used by related display buttons to pass in extra arguments. It is probably rare that code you write needs to use this argument. """ if ui_file is not None: self.main_window.open_file(ui_file, macros, command_line_args) self.windows[self.main_window] = path_info(ui_file)[0] def close_window(self, window): try: del self.windows[window] except KeyError: # If window is no longer at self.windows # it means that we already closed it. pass def load_ui_file(self, uifile, macros=None): """ Load a .ui file, perform macro substitution, then return the resulting QWidget. This is an internal method, users will usually want to use `open_file` instead. Parameters ---------- uifile : str The path to a .ui file to load. macros : dict, optional A dictionary of macro variables to supply to the file to be opened. Returns ------- QWidget """ if macros is not None and len(macros) > 0: f = macro.substitute_in_file(uifile, macros) else: f = uifile return uic.loadUi(f) def load_py_file(self, pyfile, args=None, macros=None): """ Load a .py file, performs some sanity checks to try and determine if the file actually contains a valid PyDM Display subclass, and if the checks pass, create and return an instance. This is an internal method, users will usually want to use `open_file` instead. Parameters ---------- pyfile : str The path to a .ui file to load. args : list, optional A list of command-line arguments to pass to the loaded display subclass. macros : dict, optional A dictionary of macro variables to supply to the loaded display subclass. Returns ------- pydm.Display """ # Add the intelligence module directory to the python path, so that submodules can be loaded. Eventually, this should go away, and intelligence modules should behave as real python modules. module_dir = os.path.dirname(os.path.abspath(pyfile)) sys.path.append(module_dir) temp_name = str(uuid.uuid4()) # Now load the intelligence module. module = imp.load_source(temp_name, pyfile) if hasattr(module, 'intelclass'): cls = module.intelclass if not issubclass(cls, Display): raise ValueError("Invalid class definition at file {}. {} does not inherit from Display. Nothing to open at this time.".format(pyfile, cls.__name__)) else: classes = [obj for name, obj in inspect.getmembers(module) if inspect.isclass(obj) and issubclass(obj, Display) and obj != Display] if len(classes) == 0: raise ValueError("Invalid File Format. {} has no class inheriting from Display. Nothing to open at this time.".format(pyfile)) if len(classes) > 1: warnings.warn("More than one Display class in file {}. The first occurence (in alphabetical order) will be opened: {}".format(pyfile, classes[0].__name__), RuntimeWarning, stacklevel=2) cls = classes[0] try: # This only works in python 3 and up. module_params = inspect.signature(cls).parameters except AttributeError: # Works in python 2, deprecated in 3.0 and up. module_params = inspect.getargspec(cls.__init__).args # Because older versions of Display may not have the args parameter or the macros parameter, we check # to see if it does before trying to use them. kwargs = {} if 'args' in module_params: kwargs['args'] = args if 'macros' in module_params: kwargs['macros'] = macros return cls(**kwargs) def open_file(self, ui_file, macros=None, command_line_args=None, **kwargs): """ Open a .ui or .py file, and return a widget from the loaded file. This method is the entry point for all opening of new displays, and manages handling macros and relative file paths when opening nested displays. Parameters ---------- ui_file : str The path to a .ui or .py file to open in the new process. macros : dict, optional A dictionary of macro variables to supply to the display file to be opened. command_line_args : list, optional A list of command line arguments to pass to the new process. Typically, this argument is used by related display buttons to pass in extra arguments. It is probably rare that code you write needs to use this argument. Returns ------- QWidget """ if 'establish_connection' in kwargs: logger.warning("Ignoring 'establish_connection' parameter at " "open_relative. The connection is now handled by the" " widgets.") # First split the ui_file string into a filepath and arguments args = command_line_args if command_line_args is not None else [] dir_name, file_name, extra_args = path_info(ui_file) args.extend(extra_args) filepath = os.path.join(dir_name, file_name) self.directory_stack.append(dir_name) (filename, extension) = os.path.splitext(file_name) if macros is None: macros = {} merged_macros = self.macro_stack[-1].copy() merged_macros.update(macros) self.macro_stack.append(merged_macros) with data_plugins.connection_queue(): if extension == '.ui': widget = self.load_ui_file(filepath, merged_macros) elif extension == '.py': widget = self.load_py_file(filepath, args, merged_macros) else: self.directory_stack.pop() self.macro_stack.pop() raise ValueError("Invalid file type: {}".format(extension)) # Add on the macros to the widget after initialization. This is # done for both ui files and python files. widget.base_macros = merged_macros self.directory_stack.pop() self.macro_stack.pop() return widget # get_path gives you the path to ui_file relative to where you are running pydm from. # Many widgets handle file paths (related display, embedded display, and drawing image come to mind) # and the standard is that they expect paths to be given relative to the .ui or .py file in which the # widget lives. But, python and Qt want the file path relative to the directory you are running # pydm from. This function does that translation. def get_path(self, ui_file): """ Gives you the path to ui_file relative to where you are running pydm from. Many widgets handle file paths (related display, embedded display, and drawing image come to mind) and the standard is that they expect paths to be given relative to the .ui or .py file in which the widget lives. But, python and Qt want the file path relative to the directory you are running pydm from. This function does that translation. Parameters ---------- ui_file : str Returns ------- str """ dirname = self.directory_stack[-1] full_path = os.path.join(dirname, str(ui_file)) return full_path def open_relative(self, ui_file, widget, macros=None, command_line_args=[], **kwargs): """ open_relative opens a ui file with a relative path. This is really only used by embedded displays. """ if 'establish_connection' in kwargs: logger.warning("Ignoring 'establish_connection' parameter at " "open_relative. The connection is now handled by the" " widgets.") full_path = self.get_path(ui_file) if not os.path.exists(full_path): new_fname = find_display_in_path(ui_file) if new_fname is not None and new_fname != "": full_path = new_fname return self.open_file(full_path, macros=macros, command_line_args=command_line_args) def plugin_for_channel(self, channel): """ Given a PyDMChannel object, determine the appropriate plugin to use. Parameters ---------- channel : PyDMChannel Returns ------- PyDMPlugin """ warnings.warn("'PyDMApplication.plugin_for_channel' is deprecated, " "use 'pydm.data_plugins.plugin_for_address' instead.") if channel.address is None or channel.address == "": return None return data_plugins.plugin_for_address(channel.address) def add_connection(self, channel): """ Add a new connection to a channel. Parameters ---------- channel : PyDMChannel """ warnings.warn("'PyDMApplication.add_connection' is deprecated, " "use PyDMConnection.connect()") channel.connect() def remove_connection(self, channel): """ Remove a connection to a channel. Parameters ---------- channel : PyDMChannel """ warnings.warn("'PyDMApplication.remove_connection' is deprecated, " "use PyDMConnection.disconnect()") channel.disconnect() def eventFilter(self, obj, event): warnings.warn("'PyDMApplication.eventFilter' is deprecated, " " this function is now found on PyDMWidget") obj.eventFilter(obj, event) def show_address_tooltip(self, obj, event): warnings.warn("'PyDMApplication.show_address_tooltip' is deprecated, " " this function is now found on PyDMWidget") obj.show_address_tooltip(event) def establish_widget_connections(self, widget): """ Given a widget to start from, traverse the tree of child widgets, and try to establish connections to any widgets with channels. Display subclasses which dynamically create widgets may need to use this method. Parameters ---------- widget : QWidget """ warnings.warn("'PyDMApplication.establish_widget_connections' is deprecated, " "this function is now found on `utilities.establish_widget_connections`.") connection.establish_widget_connections(widget) def close_widget_connections(self, widget): """ Given a widget to start from, traverse the tree of child widgets, and try to close connections to any widgets with channels. Parameters ---------- widget : QWidget """ warnings.warn( "'PyDMApplication.close_widget_connections' is deprecated, " "this function is now found on `utilities.close_widget_connections`.") connection.close_widget_connections(widget) def open_template(self, template_filename): fname = os.path.expanduser(os.path.expandvars(template_filename)) if os.path.isabs(fname): full_path=fname else: full_path = self.get_path(template_filename) if not os.path.exists(full_path): new_fname = find_display_in_path(template_filename) if new_fname is not None and new_fname != "": full_path = new_fname template = macro.template_for_file(full_path) return template def widget_from_template(self, template, macros): if macros is None: macros = {} merged_macros = self.macro_stack[-1].copy() merged_macros.update(macros) f = macro.replace_macros_in_template(template, merged_macros) w = self.load_ui_file(f) w.base_macros = merged_macros return w
class MatplotlibDataViewer(DataViewer): _state_cls = MatplotlibDataViewerState tools = ['mpl:home', 'mpl:pan', 'mpl:zoom'] subtools = {'save': ['mpl:save']} def __init__(self, session, parent=None, wcs=None, state=None): super(MatplotlibDataViewer, self).__init__(session, parent=parent, state=state) # Use MplWidget to set up a Matplotlib canvas inside the Qt window self.mpl_widget = MplWidget() self.setCentralWidget(self.mpl_widget) # TODO: shouldn't have to do this self.central_widget = self.mpl_widget self.figure, self._axes = init_mpl(self.mpl_widget.canvas.fig, wcs=wcs) for spine in self._axes.spines.values(): spine.set_zorder(ZORDER_MAX) self.loading_rectangle = Rectangle((0, 0), 1, 1, color='0.9', alpha=0.9, zorder=ZORDER_MAX - 1, transform=self.axes.transAxes) self.loading_rectangle.set_visible(False) self.axes.add_patch(self.loading_rectangle) self.loading_text = self.axes.text(0.4, 0.5, 'Computing', color='k', zorder=self.loading_rectangle.get_zorder() + 1, ha='left', va='center', transform=self.axes.transAxes) self.loading_text.set_visible(False) self.state.add_callback('aspect', self.update_aspect) self.update_aspect() self.state.add_callback('x_min', self.limits_to_mpl) self.state.add_callback('x_max', self.limits_to_mpl) self.state.add_callback('y_min', self.limits_to_mpl) self.state.add_callback('y_max', self.limits_to_mpl) self.limits_to_mpl() self.state.add_callback('x_log', self.update_x_log, priority=1000) self.state.add_callback('y_log', self.update_y_log, priority=1000) self.update_x_log() self.axes.callbacks.connect('xlim_changed', self.limits_from_mpl) self.axes.callbacks.connect('ylim_changed', self.limits_from_mpl) self.axes.set_autoscale_on(False) self.state.add_callback('x_axislabel', self.update_x_axislabel) self.state.add_callback('x_axislabel_weight', self.update_x_axislabel) self.state.add_callback('x_axislabel_size', self.update_x_axislabel) self.state.add_callback('y_axislabel', self.update_y_axislabel) self.state.add_callback('y_axislabel_weight', self.update_y_axislabel) self.state.add_callback('y_axislabel_size', self.update_y_axislabel) self.state.add_callback('x_ticklabel_size', self.update_x_ticklabel) self.state.add_callback('y_ticklabel_size', self.update_y_ticklabel) self.update_x_axislabel() self.update_y_axislabel() self.update_x_ticklabel() self.update_y_ticklabel() self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self._monitor_computation = QTimer() self._monitor_computation.setInterval(500) self._monitor_computation.timeout.connect(self._update_computation) def _update_computation(self, message=None): # If we get a ComputationStartedMessage and the timer isn't currently # active, then we start the timer but we then return straight away. # This is to avoid showing the 'Computing' message straight away in the # case of reasonably fast operations. if isinstance(message, ComputationStartedMessage): if not self._monitor_computation.isActive(): self._monitor_computation.start() return for layer_artist in self.layers: if layer_artist.is_computing: self.loading_rectangle.set_visible(True) text = self.loading_text.get_text() if text.count('.') > 2: text = 'Computing' else: text += '.' self.loading_text.set_text(text) self.loading_text.set_visible(True) self.redraw() return self.loading_rectangle.set_visible(False) self.loading_text.set_visible(False) self.redraw() # If we get here, the computation has stopped so we can stop the timer self._monitor_computation.stop() def add_data(self, *args, **kwargs): return super(MatplotlibDataViewer, self).add_data(*args, **kwargs) def add_subset(self, *args, **kwargs): return super(MatplotlibDataViewer, self).add_subset(*args, **kwargs) def update_x_axislabel(self, *event): self.axes.set_xlabel(self.state.x_axislabel, weight=self.state.x_axislabel_weight, size=self.state.x_axislabel_size) self.redraw() def update_y_axislabel(self, *event): self.axes.set_ylabel(self.state.y_axislabel, weight=self.state.y_axislabel_weight, size=self.state.y_axislabel_size) self.redraw() def update_x_ticklabel(self, *event): self.axes.tick_params(axis='x', labelsize=self.state.x_ticklabel_size) self.axes.xaxis.get_offset_text().set_fontsize(self.state.x_ticklabel_size) self.redraw() def update_y_ticklabel(self, *event): self.axes.tick_params(axis='y', labelsize=self.state.y_ticklabel_size) self.axes.yaxis.get_offset_text().set_fontsize(self.state.y_ticklabel_size) self.redraw() def redraw(self): self.figure.canvas.draw() def update_x_log(self, *args): self.axes.set_xscale('log' if self.state.x_log else 'linear') self.redraw() def update_y_log(self, *args): self.axes.set_yscale('log' if self.state.y_log else 'linear') self.redraw() def update_aspect(self, aspect=None): self.axes.set_aspect(self.state.aspect, adjustable='datalim') @avoid_circular def limits_from_mpl(self, *args): with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'): if isinstance(self.state.x_min, np.datetime64): x_min, x_max = [mpl_to_datetime64(x) for x in self.axes.get_xlim()] else: x_min, x_max = self.axes.get_xlim() self.state.x_min, self.state.x_max = x_min, x_max if isinstance(self.state.y_min, np.datetime64): y_min, y_max = [mpl_to_datetime64(y) for y in self.axes.get_ylim()] else: y_min, y_max = self.axes.get_ylim() self.state.y_min, self.state.y_max = y_min, y_max @avoid_circular def limits_to_mpl(self, *args): if self.state.x_min is not None and self.state.x_max is not None: x_min, x_max = self.state.x_min, self.state.x_max if self.state.x_log: if self.state.x_max <= 0: x_min, x_max = 0.1, 1 elif self.state.x_min <= 0: x_min = x_max / 10 self.axes.set_xlim(x_min, x_max) if self.state.y_min is not None and self.state.y_max is not None: y_min, y_max = self.state.y_min, self.state.y_max if self.state.y_log: if self.state.y_max <= 0: y_min, y_max = 0.1, 1 elif self.state.y_min <= 0: y_min = y_max / 10 self.axes.set_ylim(y_min, y_max) if self.state.aspect == 'equal': # FIXME: for a reason I don't quite understand, dataLim doesn't # get updated immediately here, which means that there are then # issues in the first draw of the image (the limits are such that # only part of the image is shown). We just set dataLim manually # to avoid this issue. self.axes.dataLim.intervalx = self.axes.get_xlim() self.axes.dataLim.intervaly = self.axes.get_ylim() # We then force the aspect to be computed straight away self.axes.apply_aspect() # And propagate any changes back to the state since we have the # @avoid_circular decorator with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'): # TODO: fix case with datetime64 here self.state.x_min, self.state.x_max = self.axes.get_xlim() self.state.y_min, self.state.y_max = self.axes.get_ylim() self.axes.figure.canvas.draw() # TODO: shouldn't need this! @property def axes(self): return self._axes def _update_appearance_from_settings(self, message=None): update_appearance_from_settings(self.axes) self.redraw() def get_layer_artist(self, cls, layer=None, layer_state=None): return cls(self.axes, self.state, layer=layer, layer_state=layer_state) def apply_roi(self, roi, use_current=False): """ This method must be implemented by subclasses """ raise NotImplementedError def _script_header(self): state_dict = self.state.as_dict() return ['import matplotlib.pyplot as plt'], SCRIPT_HEADER.format(**state_dict) def _script_footer(self): state_dict = self.state.as_dict() state_dict['x_log_str'] = 'log' if self.state.x_log else 'linear' state_dict['y_log_str'] = 'log' if self.state.y_log else 'linear' return [], SCRIPT_FOOTER.format(**state_dict)
def start_glue(gluefile=None, config=None, datafiles=None, maximized=True, startup_actions=None, auto_merge=False): """Run a glue session and exit Parameters ---------- gluefile : str An optional ``.glu`` file to restore. config : str An optional configuration file to use. datafiles : str An optional list of data files to load. maximized : bool Maximize screen on startup. Otherwise, use default size. auto_merge : bool, optional Whether to automatically merge data passed in `datafiles` (default is `False`) """ import glue from glue.utils.qt import get_qapp app = get_qapp() splash = get_splash() splash.show() # Start off by loading plugins. We need to do this before restoring # the session or loading the configuration since these may use existing # plugins. load_plugins(splash=splash) from glue.app.qt import GlueApplication datafiles = datafiles or [] hub = None from qtpy.QtCore import QTimer timer = QTimer() timer.setInterval(1000) timer.setSingleShot(True) timer.timeout.connect(splash.close) timer.start() if gluefile is not None: app = restore_session(gluefile) return app.start() if config is not None: glue.env = glue.config.load_configuration(search_path=[config]) data_collection = glue.core.DataCollection() hub = data_collection.hub splash.set_progress(100) session = glue.core.Session(data_collection=data_collection, hub=hub) ga = GlueApplication(session=session) if datafiles: datasets = load_data_files(datafiles) ga.add_datasets(data_collection, datasets, auto_merge=auto_merge) if startup_actions is not None: for name in startup_actions: ga.run_startup_action(name) return ga.start(maximized=maximized)
class InterpreterStatus(BaseTimerStatus): """Status bar widget for displaying the current conda environment.""" def __init__(self, parent, statusbar, icon=None, interpreter=None): """Status bar widget for displaying the current conda environment.""" self._interpreter = interpreter super(InterpreterStatus, self).__init__(parent, statusbar, icon=icon) self.main = parent self.env_actions = [] self.path_to_env = {} self.envs = {} self.value = '' self.menu = QMenu(self) self.sig_clicked.connect(self.show_menu) # Worker to compute envs in a thread self._worker_manager = WorkerManager(max_threads=1) # Timer to get envs every minute self._get_envs_timer = QTimer(self) self._get_envs_timer.setInterval(60000) self._get_envs_timer.timeout.connect(self.get_envs) self._get_envs_timer.start() # Update the list of envs at startup self.get_envs() def import_test(self): pass def get_value(self): """ Switch to default interpreter if current env was removed or update Python version of current one. """ env_dir = self._get_env_dir(self._interpreter) if not osp.isdir(env_dir): # Env was removed on Mac or Linux CONF.set('main_interpreter', 'custom', False) CONF.set('main_interpreter', 'default', True) self.update_interpreter(sys.executable) elif not osp.isfile(self._interpreter): # This can happen on Windows because the interpreter was # renamed to .conda_trash if not osp.isdir(osp.join(env_dir, 'conda-meta')): # If conda-meta is missing, it means the env was removed CONF.set('main_interpreter', 'custom', False) CONF.set('main_interpreter', 'default', True) self.update_interpreter(sys.executable) else: # If not, it means the interpreter is being updated so # we need to update its version self.get_envs() else: # We need to do this in case the Python version was # changed in the env if self._interpreter in self.path_to_env: self.update_interpreter() return self.value def _get_env_dir(self, interpreter): """Get env directory from interpreter executable.""" if os.name == 'nt': return osp.dirname(interpreter) else: return osp.dirname(osp.dirname(interpreter)) def _get_envs(self): """Get the list of environments in the system.""" # Compute info of default interpreter to have it available in # case we need to switch to it. This will avoid lags when # doing that in get_value. if sys.executable not in self.path_to_env: self._get_env_info(sys.executable) # Get envs conda_env = get_list_conda_envs() pyenv_env = get_list_pyenv_envs() return {**conda_env, **pyenv_env} def get_envs(self): """ Get the list of environments in a thread to keep them up to date. """ self._worker_manager.terminate_all() worker = self._worker_manager.create_python_worker(self._get_envs) worker.sig_finished.connect(self.update_envs) worker.start() def update_envs(self, worker, output, error): """Update the list of environments in the system.""" self.envs.update(**output) for env in list(self.envs.keys()): path, version = self.envs[env] # Save paths in lowercase on Windows to avoid issues with # capitalization. path = path.lower() if os.name == 'nt' else path self.path_to_env[path] = env self.update_interpreter() def show_menu(self): """Display a menu when clicking on the widget.""" menu = self.menu menu.clear() text = _("Change default environment in Preferences...") change_action = create_action( self, text=text, triggered=self.open_interpreter_preferences, ) add_actions(menu, [change_action]) rect = self.contentsRect() os_height = 7 if os.name == 'nt' else 12 pos = self.mapToGlobal(rect.topLeft() + QPoint(-40, -rect.height() - os_height)) menu.popup(pos) def open_interpreter_preferences(self): """Open the Preferences dialog in the Python interpreter section.""" self.main.show_preferences() dlg = self.main.prefs_dialog_instance index = dlg.get_index_by_name("main_interpreter") dlg.set_current_index(index) def _get_env_info(self, path): """Get environment information.""" path = path.lower() if os.name == 'nt' else path try: name = self.path_to_env[path] except KeyError: win_app_path = osp.join('AppData', 'Local', 'Programs', 'spyder') if 'Spyder.app' in path or win_app_path in path: name = 'internal' elif 'conda' in path: name = 'conda' elif 'pyenv' in path: name = 'pyenv' else: name = 'custom' version = get_interpreter_info(path) self.path_to_env[path] = name self.envs[name] = (path, version) __, version = self.envs[name] return f'{name} ({version})' def get_tooltip(self): """Override api method.""" return self._interpreter if self._interpreter else '' def update_interpreter(self, interpreter=None): """Set main interpreter and update information.""" if interpreter: self._interpreter = interpreter self.value = self._get_env_info(self._interpreter) self.set_value(self.value) self.update_tooltip()
class Connection(PyDMConnection): """ Class that manages channel access connections using pyca through psp. See :class:`PyDMConnection` class. """ def __init__(self, channel, pv, protocol=None, parent=None): """ Instantiate Pv object and set up the channel access connections. :param channel: :class:`PyDMChannel` object as the first listener. :type channel: :class:`PyDMChannel` :param pv: Name of the pv to connect to. :type pv: str :param parent: PyQt widget that this widget is inside of. :type parent: QWidget """ super(Connection, self).__init__(channel, pv, protocol, parent) self.python_type = None self.pv = setup_pv(pv, con_cb=self.connected_cb, mon_cb=self.monitor_cb, rwaccess_cb=self.rwaccess_cb) self.enums = None self.sevr = None self.ctrl_llim = None self.ctrl_hlim = None self.units = None self.prec = None self.count = None self.epics_type = None # Auxilliary info to help with throttling self.scan_pv = setup_pv(pv + ".SCAN", mon_cb=self.scan_pv_cb, mon_cb_once=True) self.throttle = QTimer(self) self.throttle.timeout.connect(self.throttle_cb) self.add_listener(channel) def connected_cb(self, isconnected): """ Callback to run whenever the connection state of our pv changes. :param isconnected: True if we are connected, False otherwise. :type isconnected: bool """ self.connected = isconnected self.send_connection_state(isconnected) if isconnected: self.epics_type = self.pv.type() self.count = self.pv.count or 1 # Get the control info for the PV. self.pv.get_data(True, -1.0, self.count) pyca.flush_io() if self.epics_type == "DBF_ENUM": self.pv.get_enum_strings(-1.0) if not self.pv.ismonitored: self.pv.monitor() self.python_type = type_map.get(self.epics_type) if self.python_type is None: raise Exception("Unsupported EPICS type {0} for pv {1}".format( self.epics_type, self.pv.name)) def monitor_cb(self, e=None): """ Callback to run whenever the value of our pv changes. :param e: Error state. Should be None under normal circumstances. """ if e is None: self.send_new_value(self.pv.value) def rwaccess_cb(self, read_access, write_access): """ Callback to run when the access state of our pv changes. :param read_access: Whether or not the PV is readable. :param write_access: Whether or not the PV is writeable. """ self.send_access_state(read_access, write_access) def throttle_cb(self): """ Callback to run when the throttle timer times out. """ self.send_new_value(self.pv.get()) def timestamp(self): try: secs, nanos = self.pv.timestamp() except KeyError: return None return float(secs + nanos / 1.0e9) def send_new_value(self, value=None): """ Send a value to every channel listening for our Pv. :param value: Value to emit to our listeners. :type value: int, float, str, or np.ndarray, depending on our record type. """ if self.python_type is None: return if self.enums is None: try: self.update_enums() except KeyError: self.pv.get_enum_strings(-1.0) if self.pv.severity is not None and self.pv.severity != self.sevr: self.sevr = self.pv.severity self.new_severity_signal.emit(self.sevr) try: prec = self.pv.data['precision'] if self.prec != prec: self.prec = prec self.prec_signal.emit(int(self.prec)) except KeyError: pass try: units = self.pv.data['units'] if self.units != units: self.units = units self.unit_signal.emit(self.units.decode(encoding='ascii')) except KeyError: pass try: ctrl_llim = self.pv.data['ctrl_llim'] if self.ctrl_llim != ctrl_llim: self.ctrl_llim = ctrl_llim self.lower_ctrl_limit_signal.emit(self.ctrl_llim) except KeyError: pass try: ctrl_hlim = self.pv.data['ctrl_hlim'] if self.ctrl_hlim != ctrl_hlim: self.ctrl_hlim = ctrl_hlim self.upper_ctrl_limit_signal.emit(self.ctrl_hlim) except KeyError: pass if self.count > 1: self.new_value_signal[np.ndarray].emit(value) else: self.new_value_signal[self.python_type].emit(self.python_type(value)) def send_ctrl_vars(self): if self.enums is None: try: self.update_enums() except KeyError: self.pv.get_enum_strings(-1.0) else: self.enum_strings_signal.emit(self.enums) if self.pv.severity != self.sevr: self.sevr = self.pv.severity self.new_severity_signal.emit(self.sevr) if self.prec is None: try: self.prec = self.pv.data['precision'] except KeyError: pass if self.prec: self.prec_signal.emit(int(self.prec)) if self.units is None: try: self.units = self.pv.data['units'] except KeyError: pass if self.units: self.unit_signal.emit(self.units.decode(encoding='ascii')) if self.ctrl_llim is None: try: self.ctrl_llim = self.pv.data['ctrl_llim'] except KeyError: pass if self.ctrl_llim: self.lower_ctrl_limit_signal.emit(self.ctrl_llim) if self.ctrl_hlim is None: try: self.ctrl_hlim = self.pv.data['ctrl_hlim'] except KeyError: pass if self.ctrl_hlim: self.upper_ctrl_limit_signal.emit(self.ctrl_hlim) def send_connection_state(self, conn=None): """ Send an update on our connection state to every listener. :param conn: True if we are connected, False if we are disconnected. :type conn: bool """ self.connection_state_signal.emit(conn) def send_access_state(self, read_access, write_access): if data_plugins.is_read_only(): self.write_access_signal.emit(False) return self.write_access_signal.emit(write_access) def update_enums(self): """ Send an update on our enum strings to every listener, if this is an enum record. """ if self.epics_type == "DBF_ENUM": if self.enums is None: self.enums = tuple(b.decode(encoding='ascii') for b in self.pv.data["enum_set"]) self.enum_strings_signal.emit(self.enums) @Slot(int) @Slot(float) @Slot(str) @Slot(np.ndarray) def put_value(self, value): """ Set our PV's value in EPICS. :param value: The value we'd like to put to our PV. :type value: int or float or str or np.ndarray, depending on our record type. """ if self.count == 1: value = self.python_type(value) try: self.pv.put(value) except pyca.caexc as e: print("pyca error: {}".format(e)) @Slot(np.ndarray) def put_waveform(self, value): """ Set a PV's waveform value in EPICS. This is a deprecated function kept temporarily for compatibility with old code. :param value: The waveform value we'd like to put to our PV. :type value: np.ndarray """ self.put_value(value) def scan_pv_cb(self, e=None): """ Call set_throttle once we have a value from the scan_pv. We need this value inside set_throttle to decide if we can ignore the throttle request (i.e. our pv updates more slowly than our throttle) :param e: Error state. Should be None under normal circumstances. """ if e is None: self.pv.wait_ready() count = self.pv.count or 1 if count > 1: max_data_rate = 1000000. # bytes/s bytes = self.pv.value.itemsize # bytes throttle = max_data_rate / (bytes * count) # Hz if throttle < 120: self.set_throttle(throttle) @Slot(int) @Slot(float) def set_throttle(self, refresh_rate): """ Throttle our update rate. This is useful when the data is large (e.g. image waveforms). Set to zero to disable throttling. :param delay: frequency of pv updates :type delay: float or int """ try: scan = scan_list[self.scan_pv.value] except: scan = float("inf") if 0 < refresh_rate < 1 / scan: self.pv.monitor_stop() self.throttle.setInterval(1000.0 / refresh_rate) self.throttle.start() else: self.throttle.stop() if not self.pv.ismonitored: self.pv.monitor() def add_listener(self, channel): """ Connect a channel's signals and slots with this object's signals and slots. :param channel: The channel to connect. :type channel: :class:`PyDMChannel` """ super(Connection, self).add_listener(channel) # If we are adding a listener to an already existing PV, we need to # manually send the signals indicating that the PV is connected, what # the latest value is, etc. if self.pv.isconnected and self.pv.isinitialized: self.send_connection_state(conn=True) self.monitor_cb() self.update_enums() self.send_ctrl_vars() if channel.value_signal is not None: try: channel.value_signal[str].connect(self.put_value, Qt.QueuedConnection) except KeyError: pass try: channel.value_signal[int].connect(self.put_value, Qt.QueuedConnection) except KeyError: pass try: channel.value_signal[float].connect(self.put_value, Qt.QueuedConnection) except KeyError: pass try: channel.value_signal[np.ndarray].connect(self.put_value, Qt.QueuedConnection) except KeyError: pass def close(self): """ Clean up. """ self.throttle.stop() self.pv.monitor_stop() self.pv.disconnect() self.scan_pv.monitor_stop() self.scan_pv.disconnect()
class QtPoll(QObject): """Polls anything once per frame via an event. QtPoll was first created for VispyTiledImageLayer. It polls the visual when the camera moves. However, we also want visuals to keep loading chunks even when the camera stops. We want the visual to finish up anything that was in progress. Before it goes fully idle. QtPoll will poll those visuals using a timer. If the visual says the event was "handled" it means the visual has more work to do. If that happens, QtPoll will continue to poll and draw the visual it until the visual is done with the in-progress work. An analogy is a snow globe. The user moving the camera shakes up the snow globe. We need to keep polling/drawing things until all the snow settles down. Then everything will stay completely still until the camera is moved again, shaking up the globe once more. Parameters ---------- parent : QObject Parent Qt object. camera : Camera The viewer's main camera. """ def __init__(self, parent: QObject, camera: Camera): super().__init__(parent) self.events = EmitterGroup(source=self, auto_connect=True, poll=None) camera.events.connect(self._on_camera) self.timer = QTimer() self.timer.setInterval(POLL_INTERVAL_MS) self.timer.timeout.connect(self._on_timer) def _on_camera(self, _event) -> None: """Called when camera view changes at all.""" # Poll right away. If the timer is running, it's generally starved # out by the mouse button being down. Why? If we end up "double # polling" it *should* be harmless. But if we don't poll then # everything is frozen. So better to poll. self._poll() # Start the timer so that we will keep polling even if the camera # doesn't move again. Although the mouse movement is starving out # the timer right now, we need the timer going so we keep polling # even if the mouse stops. self.timer.start() def _on_timer(self) -> None: """Called when the timer is running.""" # The timer is running which means someone we are polling still has # work to do. self._poll() def _poll(self) -> None: """Called on camera move or with the timer.""" # Poll everyone listening to our even. event = self.events.poll() # Listeners will "handle" the event if they need more polling. If # no one needs polling, then we can stop the timer. if not event.handled: self.timer.stop() return # Someone handled the event, so they want to be polled even if # the mouse doesn't move. So start the timer if needed. if not self.timer.isActive(): self.timer.start() def closeEvent(self, _event: QEvent) -> None: """Cleanup and close. Parameters ---------- event : QEvent The close event. """ self.timer.stop() self.deleteLater()
class VispyVolumeViewer(BaseVispyViewer): LABEL = "3D Volume Rendering" _state_cls = Vispy3DVolumeViewerState _layer_style_widget_cls = { VolumeLayerArtist: VolumeLayerStyleWidget, ScatterLayerArtist: ScatterLayerStyleWidget } tools = BaseVispyViewer.tools + [ 'vispy:lasso', 'vispy:rectangle', 'vispy:circle', 'volume3d:floodfill' ] def __init__(self, *args, **kwargs): super(VispyVolumeViewer, self).__init__(*args, **kwargs) # We now make it so that is the user clicks to drag or uses the # mouse wheel (or scroll on a trackpad), we downsample the volume # rendering temporarily. canvas = self._vispy_widget.canvas canvas.events.mouse_press.connect(self.mouse_press) canvas.events.mouse_wheel.connect(self.mouse_wheel) canvas.events.mouse_release.connect(self.mouse_release) viewbox = self._vispy_widget.view.camera.viewbox viewbox.events.mouse_wheel.connect(self.camera_mouse_wheel) viewbox.events.mouse_move.connect(self.camera_mouse_move) viewbox.events.mouse_press.connect(self.camera_mouse_press) viewbox.events.mouse_release.connect(self.camera_mouse_release) self._downsampled = False # For the mouse wheel, we receive discrete events so we need to have # a buffer (for now 250ms) before which we consider the mouse wheel # event to have stopped. self._downsample_timer = QTimer() self._downsample_timer.setInterval(250) self._downsample_timer.setSingleShot(True) self._downsample_timer.timeout.connect(self.mouse_release) # We need to use MultiVolume instance to store volumes, but we should # only have one per canvas. Therefore, we store the MultiVolume # instance in the vispy viewer instance. # Set whether we are emulating a 3D texture. This needs to be # enabled as a workaround on Windows otherwise VisPy crashes. emulate_texture = (sys.platform == 'win32' and sys.version_info[0] < 3) multivol = MultiVolume(emulate_texture=emulate_texture, bgcolor=settings.BACKGROUND_COLOR) self._vispy_widget.add_data_visual(multivol) self._vispy_widget._multivol = multivol self.state.add_callback('resolution', self._update_resolution) self._update_resolution() # We do this here in addition to in the volume viewer itself as for # some situations e.g. reloading from session files, a clip_data event # isn't emitted. # FIXME: needs to be done after first layer added # self._update_clip(force=True) def mouse_press(self, event=None): if self.state.downsample: if hasattr(self._vispy_widget, '_multivol') and not self._downsampled: self._vispy_widget._multivol.downsample() self._downsampled = True def mouse_release(self, event=None): if self.state.downsample: if hasattr(self._vispy_widget, '_multivol') and self._downsampled: self._vispy_widget._multivol.upsample() self._downsampled = False self._vispy_widget.canvas.render() self._update_slice_transform() self._update_clip() def _update_clip(self, force=False): if hasattr(self._vispy_widget, '_multivol'): if (self.state.clip_data or force): dx = self.state.x_stretch * self.state.aspect[0] dy = self.state.y_stretch * self.state.aspect[1] dz = self.state.z_stretch * self.state.aspect[2] coords = np.array([[-dx, -dy, -dz], [dx, dy, dz]]) coords = ( self._vispy_widget._multivol.transform.imap(coords)[:, :3] / self._vispy_widget._multivol.resolution) self._vispy_widget._multivol.set_clip(self.state.clip_data, coords.ravel()) else: self._vispy_widget._multivol.set_clip(False, [0, 0, 0, 1, 1, 1]) def _update_slice_transform(self): self._vispy_widget._multivol._update_slice_transform( self.state.x_min, self.state.x_max, self.state.y_min, self.state.y_max, self.state.z_min, self.state.z_max) def _update_resolution(self, *event): self._vispy_widget._multivol.set_resolution(self.state.resolution) self._update_slice_transform() self._update_clip() def mouse_wheel(self, event=None): if self.state.downsample: if hasattr(self._vispy_widget, '_multivol'): if not self._downsampled: self.mouse_press() self._downsample_timer.start() if event is not None: event.handled = True def resizeEvent(self, event=None): self.mouse_wheel() super(VispyVolumeViewer, self).resizeEvent(event) def get_data_layer_artist(self, layer=None, layer_state=None): if layer.ndim == 1: cls = ScatterLayerArtist else: cls = VolumeLayerArtist return self.get_layer_artist(cls, layer=layer, layer_state=layer_state) def get_subset_layer_artist(self, layer=None, layer_state=None): if layer.ndim == 1: cls = ScatterLayerArtist else: cls = VolumeLayerArtist return self.get_layer_artist(cls, layer=layer, layer_state=layer_state) def add_data(self, data): first_layer_artist = len(self._layer_artist_container) == 0 if data.ndim == 1: if first_layer_artist: QMessageBox.critical( self, "Error", "Can only add a scatter plot overlay once " "a volume is present", buttons=QMessageBox.Ok) return False elif data.ndim == 3: if not self._has_free_volume_layers: self._warn_no_free_volume_layers() return False else: QMessageBox.critical( self, "Error", "Data should be 1- or 3-dimensional ({0} dimensions " "found)".format(data.ndim), buttons=QMessageBox.Ok) return False added = super(VispyVolumeViewer, self).add_data(data) if added: if data.ndim == 1: self._vispy_widget._update_limits() if first_layer_artist: # The above call to add_data may have added subset layers, some # of which may be incompatible with the data, so we need to now # explicitly use the layer for the actual data object. layer = self._layer_artist_container[data][0] self.state.set_limits(*layer.bbox) self._ready_draw = True self._update_slice_transform() self._show_free_layer_warning = True return added def add_subset(self, subset): if not self._has_free_volume_layers: self._warn_no_free_volume_layers() return False added = super(VispyVolumeViewer, self).add_subset(subset) if added: self._show_free_layer_warning = True return added @property def _has_free_volume_layers(self): return (not hasattr(self._vispy_widget, '_multivol') or self._vispy_widget._multivol.has_free_slots) def _warn_no_free_volume_layers(self): if getattr(self, '_show_free_layer_warning', True): QMessageBox.critical( self, "Error", "The volume viewer has reached the maximum number " "of volume layers. To show more volume layers, remove " "existing layers and try again. This error will not " "be shown again unless the limit is reached again in " "the future.", buttons=QMessageBox.Ok) self._show_free_layer_warning = False def _update_appearance_from_settings(self, message): super(VispyVolumeViewer, self)._update_appearance_from_settings(message) if hasattr(self._vispy_widget, '_multivol'): self._vispy_widget._multivol.set_background( settings.BACKGROUND_COLOR) def _toggle_clip(self, *args): if hasattr(self._vispy_widget, '_multivol'): self._update_clip() @classmethod def __setgluestate__(cls, rec, context): viewer = super(VispyVolumeViewer, cls).__setgluestate__(rec, context) if rec.get('_protocol', 0) < 2: # Find all data objects in layers (not subsets) layer_data = [ layer.layer for layer in viewer.state.layers if (isinstance(layer, VolumeLayerState) and isinstance(layer.layer, BaseData)) ] if len(layer_data) > 1: reference = layer_data[0] for data in layer_data[1:]: if data not in reference.pixel_aligned_data: break else: return viewer buttons = QMessageBox.Yes | QMessageBox.No message = ( "The 3D volume rendering viewer now requires datasets to " "be linked in order to be shown at the same time. Are you " "happy for glue to automatically link your datasets by " "pixel coordinates?") answer = QMessageBox.question(None, "Link data?", message, buttons=buttons, defaultButton=QMessageBox.Yes) if answer == QMessageBox.Yes: for data in layer_data[1:]: if data not in reference.pixel_aligned_data: for i in range(3): link = LinkSame(reference.pixel_component_ids[i], data.pixel_component_ids[i]) viewer.session.data_collection.add_link(link) return viewer def __gluestate__(self, context): state = super(VispyVolumeViewer, self).__gluestate__(context) state['_protocol'] = 2 return state
class WorkerManager(QObject): """Spyder Worker Manager for Generic Workers.""" def __init__(self, max_threads=10): """Spyder Worker Manager for Generic Workers.""" super(QObject, self).__init__() self._queue = deque() self._queue_workers = deque() self._threads = [] self._workers = [] self._timer = QTimer() self._timer_worker_delete = QTimer() self._running_threads = 0 self._max_threads = max_threads # Keeps references to old workers # Needed to avoud C++/python object errors self._bag_collector = deque() self._timer.setInterval(333) self._timer.timeout.connect(self._start) self._timer_worker_delete.setInterval(5000) self._timer_worker_delete.timeout.connect(self._clean_workers) def _clean_workers(self): """Delete periodically workers in workers bag.""" while self._bag_collector: self._bag_collector.popleft() self._timer_worker_delete.stop() def _start(self, worker=None): """Start threads and check for inactive workers.""" if worker: self._queue_workers.append(worker) if self._queue_workers and self._running_threads < self._max_threads: #print('Queue: {0} Running: {1} Workers: {2} ' # 'Threads: {3}'.format(len(self._queue_workers), # self._running_threads, # len(self._workers), # len(self._threads))) self._running_threads += 1 worker = self._queue_workers.popleft() thread = QThread() if isinstance(worker, PythonWorker): worker.moveToThread(thread) worker.sig_finished.connect(thread.quit) thread.started.connect(worker._start) thread.start() elif isinstance(worker, ProcessWorker): thread.quit() worker._start() self._threads.append(thread) else: self._timer.start() if self._workers: for w in self._workers: if w.is_finished(): self._bag_collector.append(w) self._workers.remove(w) if self._threads: for t in self._threads: if t.isFinished(): self._threads.remove(t) self._running_threads -= 1 if len(self._threads) == 0 and len(self._workers) == 0: self._timer.stop() self._timer_worker_delete.start() def create_python_worker(self, func, *args, **kwargs): """Create a new python worker instance.""" worker = PythonWorker(func, args, kwargs) self._create_worker(worker) return worker def create_process_worker(self, cmd_list, environ=None): """Create a new process worker instance.""" worker = ProcessWorker(cmd_list, environ=environ) self._create_worker(worker) return worker def terminate_all(self): """Terminate all worker processes.""" for worker in self._workers: worker.terminate() # for thread in self._threads: # try: # thread.terminate() # thread.wait() # except Exception: # pass self._queue_workers = deque() def _create_worker(self, worker): """Common worker setup.""" worker.sig_started.connect(self._start) self._workers.append(worker)
class _DownloadAPI(QObject): """Download API based on QNetworkAccessManager.""" def __init__(self, chunk_size=1024, load_rc_func=None): """Download API based on QNetworkAccessManager.""" super(_DownloadAPI, self).__init__() self._chunk_size = chunk_size self._head_requests = {} self._get_requests = {} self._paths = {} self._workers = {} self._load_rc_func = load_rc_func self._manager = QNetworkAccessManager(self) self._proxy_factory = NetworkProxyFactory(load_rc_func=load_rc_func) self._timer = QTimer() # Setup self._manager.setProxyFactory(self._proxy_factory) self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) # Signals self._manager.finished.connect(self._request_finished) self._manager.sslErrors.connect(self._handle_ssl_errors) self._manager.proxyAuthenticationRequired.connect( self._handle_proxy_auth) @staticmethod def _handle_ssl_errors(reply, errors): """Callback for ssl_errors.""" logger.error(str(('SSL Errors', errors, reply))) @staticmethod def _handle_proxy_auth(proxy, authenticator): """Callback for ssl_errors.""" # authenticator.setUser('1')` # authenticator.setPassword('1') logger.error(str(('Proxy authentication Error. ' 'Enter credentials in condarc', proxy, authenticator))) def _clean(self): """Check for inactive workers and remove their references.""" if self._workers: for url in self._workers.copy(): w = self._workers[url] if w.is_finished(): self._workers.pop(url) self._paths.pop(url) if url in self._get_requests: self._get_requests.pop(url) else: self._timer.stop() def _request_finished(self, reply): """Callback for download once the request has finished.""" url = to_text_string(reply.url().toEncoded(), encoding='utf-8') if url in self._paths: path = self._paths[url] if url in self._workers: worker = self._workers[url] if url in self._head_requests: error = reply.error() # print(url, error) if error: logger.error(str(('Head Reply Error:', error))) worker.sig_download_finished.emit(url, path) worker.sig_finished.emit(worker, path, error) return self._head_requests.pop(url) start_download = not bool(error) header_pairs = reply.rawHeaderPairs() headers = {} for hp in header_pairs: headers[to_text_string(hp[0]).lower()] = to_text_string(hp[1]) total_size = int(headers.get('content-length', 0)) # Check if file exists if os.path.isfile(path): file_size = os.path.getsize(path) # Check if existing file matches size of requested file start_download = file_size != total_size if start_download: # File sizes dont match, hence download file qurl = QUrl(url) request = QNetworkRequest(qurl) self._get_requests[url] = request reply = self._manager.get(request) error = reply.error() if error: logger.error(str(('Reply Error:', error))) reply.downloadProgress.connect( lambda r, t, w=worker: self._progress(r, t, w)) else: # File sizes match, dont download file or error? worker.finished = True worker.sig_download_finished.emit(url, path) worker.sig_finished.emit(worker, path, None) elif url in self._get_requests: data = reply.readAll() self._save(url, path, data) def _save(self, url, path, data): """Save `data` of downloaded `url` in `path`.""" worker = self._workers[url] path = self._paths[url] if len(data): try: with open(path, 'wb') as f: f.write(data) except Exception: logger.error((url, path)) # Clean up worker.finished = True worker.sig_download_finished.emit(url, path) worker.sig_finished.emit(worker, path, None) self._get_requests.pop(url) self._workers.pop(url) self._paths.pop(url) @staticmethod def _progress(bytes_received, bytes_total, worker): """Return download progress.""" worker.sig_download_progress.emit( worker.url, worker.path, bytes_received, bytes_total) def download(self, url, path): """Download url and save data to path.""" # original_url = url # print(url) qurl = QUrl(url) url = to_text_string(qurl.toEncoded(), encoding='utf-8') logger.debug(str((url, path))) if url in self._workers: while not self._workers[url].finished: return self._workers[url] worker = DownloadWorker(url, path) # Check download folder exists folder = os.path.dirname(os.path.abspath(path)) if not os.path.isdir(folder): os.makedirs(folder) request = QNetworkRequest(qurl) self._head_requests[url] = request self._paths[url] = path self._workers[url] = worker self._manager.head(request) self._timer.start() return worker def terminate(self): """Terminate all download workers and threads.""" pass
class NapariNotification(QDialog): """Notification dialog frame, appears at the bottom right of the canvas. By default, only the first line of the notification is shown, and the text is elided. Double-clicking on the text (or clicking the chevron icon) will expand to show the full notification. The dialog will autmatically disappear in ``DISMISS_AFTER`` milliseconds, unless hovered or clicked. Parameters ---------- message : str The message that will appear in the notification severity : str or NotificationSeverity, optional Severity level {'error', 'warning', 'info', 'none'}. Will determine the icon associated with the message. by default NotificationSeverity.WARNING. source : str, optional A source string for the notifcation (intended to show the module and or package responsible for the notification), by default None actions : list of tuple, optional A sequence of 2-tuples, where each tuple is a string and a callable. Each tuple will be used to create button in the dialog, where the text on the button is determine by the first item in the tuple, and a callback function to call when the button is pressed is the second item in the tuple. by default () """ MAX_OPACITY = 0.9 FADE_IN_RATE = 220 FADE_OUT_RATE = 120 DISMISS_AFTER = 4000 MIN_WIDTH = 400 def __init__( self, message: str, severity: Union[ str, NotificationSeverity] = NotificationSeverity.WARNING, source: Optional[str] = None, actions: ActionSequence = (), ): """[summary] """ super().__init__(None) # FIXME: this does not work with multiple viewers. # we need a way to detect the viewer in which the error occured. for wdg in QApplication.topLevelWidgets(): if isinstance(wdg, QMainWindow): try: # TODO: making the canvas the parent makes it easier to # move/resize, but also means that the notification can get # clipped on the left if the canvas is too small. canvas = wdg.centralWidget().children()[1].canvas.native self.setParent(canvas) canvas.resized.connect(self.move_to_bottom_right) break except Exception: pass self.setupUi() self.setAttribute(Qt.WA_DeleteOnClose) self.setup_buttons(actions) self.setMouseTracking(True) self.severity_icon.setText(NotificationSeverity(severity).as_icon()) self.message.setText(message) if source: self.source_label.setText(f'Source: {source}') self.close_button.clicked.connect(self.close) self.expand_button.clicked.connect(self.toggle_expansion) self.timer = None self.opacity = QGraphicsOpacityEffect() self.setGraphicsEffect(self.opacity) self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity", self) self.geom_anim = QPropertyAnimation(self, b"geometry", self) self.move_to_bottom_right() def move_to_bottom_right(self, offset=(8, 8)): """Position widget at the bottom right edge of the parent.""" if not self.parent(): return sz = self.parent().size() - self.size() - QSize(*offset) self.move(QPoint(sz.width(), sz.height())) def slide_in(self): """Run animation that fades in the dialog with a slight slide up.""" geom = self.geometry() self.geom_anim.setDuration(self.FADE_IN_RATE) self.geom_anim.setStartValue(geom.translated(0, 20)) self.geom_anim.setEndValue(geom) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) # fade in self.opacity_anim.setDuration(self.FADE_IN_RATE) self.opacity_anim.setStartValue(0) self.opacity_anim.setEndValue(self.MAX_OPACITY) self.geom_anim.start() self.opacity_anim.start() def show(self): """Show the message with a fade and slight slide in from the bottom.""" super().show() self.slide_in() self.timer = QTimer() self.timer.setInterval(self.DISMISS_AFTER) self.timer.setSingleShot(True) self.timer.timeout.connect(self.close) self.timer.start() def mouseMoveEvent(self, event): """On hover, stop the self-destruct timer""" self.timer.stop() def mouseDoubleClickEvent(self, event): """Expand the notification on double click.""" self.toggle_expansion() def close(self): """Fade out then close.""" self.opacity_anim.setDuration(self.FADE_OUT_RATE) self.opacity_anim.setStartValue(self.MAX_OPACITY) self.opacity_anim.setEndValue(0) self.opacity_anim.start() self.opacity_anim.finished.connect(super().close) def toggle_expansion(self): """Toggle the expanded state of the notification frame.""" self.contract() if self.property('expanded') else self.expand() self.timer.stop() def expand(self): """Expanded the notification so that the full message is visible.""" curr = self.geometry() self.geom_anim.setDuration(100) self.geom_anim.setStartValue(curr) new_height = self.sizeHint().height() delta = new_height - curr.height() self.geom_anim.setEndValue( QRect(curr.x(), curr.y() - delta, curr.width(), new_height)) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) self.geom_anim.start() self.setProperty('expanded', True) self.style().unpolish(self.expand_button) self.style().polish(self.expand_button) def contract(self): """Contract notification to a single elided line of the message.""" geom = self.geometry() self.geom_anim.setDuration(100) self.geom_anim.setStartValue(geom) dlt = geom.height() - self.minimumHeight() self.geom_anim.setEndValue( QRect(geom.x(), geom.y() + dlt, geom.width(), geom.height() - dlt)) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) self.geom_anim.start() self.setProperty('expanded', False) self.style().unpolish(self.expand_button) self.style().polish(self.expand_button) def setupUi(self): """Set up the UI during initialization.""" self.setWindowFlags(Qt.SubWindow) self.setMinimumWidth(self.MIN_WIDTH) self.setMaximumWidth(self.MIN_WIDTH) self.setMinimumHeight(40) self.setSizeGripEnabled(False) self.setModal(False) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setContentsMargins(2, 2, 2, 2) self.verticalLayout.setSpacing(0) self.row1_widget = QWidget(self) self.row1 = QHBoxLayout(self.row1_widget) self.row1.setContentsMargins(12, 12, 12, 8) self.row1.setSpacing(4) self.severity_icon = QLabel(self.row1_widget) self.severity_icon.setObjectName("severity_icon") self.severity_icon.setMinimumWidth(30) self.severity_icon.setMaximumWidth(30) self.row1.addWidget(self.severity_icon, alignment=Qt.AlignTop) self.message = MultilineElidedLabel(self.row1_widget) self.message.setMinimumWidth(self.MIN_WIDTH - 200) self.message.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.row1.addWidget(self.message, alignment=Qt.AlignTop) self.expand_button = QPushButton(self.row1_widget) self.expand_button.setObjectName("expand_button") self.expand_button.setCursor(Qt.PointingHandCursor) self.expand_button.setMaximumWidth(20) self.expand_button.setFlat(True) self.row1.addWidget(self.expand_button, alignment=Qt.AlignTop) self.close_button = QPushButton(self.row1_widget) self.close_button.setObjectName("close_button") self.close_button.setCursor(Qt.PointingHandCursor) self.close_button.setMaximumWidth(20) self.close_button.setFlat(True) self.row1.addWidget(self.close_button, alignment=Qt.AlignTop) self.verticalLayout.addWidget(self.row1_widget, 1) self.row2_widget = QWidget(self) self.row2_widget.hide() self.row2 = QHBoxLayout(self.row2_widget) self.source_label = QLabel(self.row2_widget) self.source_label.setObjectName("source_label") self.row2.addWidget(self.source_label, alignment=Qt.AlignBottom) self.row2.addStretch() self.row2.setContentsMargins(12, 2, 16, 12) self.row2_widget.setMaximumHeight(34) self.row2_widget.setStyleSheet('QPushButton{' 'padding: 4px 12px 4px 12px; ' 'font-size: 11px;' 'min-height: 18px; border-radius: 0;}') self.verticalLayout.addWidget(self.row2_widget, 0) self.setProperty('expanded', False) self.resize(self.MIN_WIDTH, 40) def setup_buttons(self, actions: ActionSequence = ()): """Add buttons to the dialog. Parameters ---------- actions : tuple, optional A sequence of 2-tuples, where each tuple is a string and a callable. Each tuple will be used to create button in the dialog, where the text on the button is determine by the first item in the tuple, and a callback function to call when the button is pressed is the second item in the tuple. by default () """ if isinstance(actions, dict): actions = list(actions.items()) for text, callback in actions: btn = QPushButton(text) btn.clicked.connect(callback) btn.clicked.connect(self.close) self.row2.addWidget(btn) if actions: self.row2_widget.show() self.setMinimumHeight(self.row2_widget.maximumHeight() + self.minimumHeight()) def sizeHint(self): """Return the size required to show the entire message.""" return QSize( super().sizeHint().width(), self.row2_widget.height() + self.message.sizeHint().height(), ) @classmethod def from_exception(cls, exception: BaseException) -> 'NapariNotification': """Create a NapariNotifcation dialog from an exception object.""" # TODO: this method could be used to recognize various exception # subclasses and populate the dialog accordingly. msg = getattr(exception, 'message', str(exception)) severity = getattr(exception, 'severity', 'WARNING') source = None actions = getattr(exception, 'actions', ()) return cls(msg, severity, source, actions)
def start_glue(gluefile=None, config=None, datafiles=None, maximized=True, startup_actions=None, auto_merge=False): """Run a glue session and exit Parameters ---------- gluefile : str An optional ``.glu`` file to restore. config : str An optional configuration file to use. datafiles : str An optional list of data files to load. maximized : bool Maximize screen on startup. Otherwise, use default size. auto_merge : bool, optional Whether to automatically merge data passed in `datafiles` (default is `False`) """ import glue # Some Qt modules are picky in terms of being imported before the # application is set up, so we import them here. We do it here rather # than in get_qapp since in the past, including this code in get_qapp # caused severe issues (e.g. segmentation faults) in plugin packages # during testing. try: from qtpy import QtWebEngineWidgets # noqa except ImportError: # Not all PyQt installations have this module pass from glue.utils.qt import get_qapp app = get_qapp() splash = get_splash() splash.show() # Start off by loading plugins. We need to do this before restoring # the session or loading the configuration since these may use existing # plugins. load_plugins(splash=splash) from glue.app.qt import GlueApplication datafiles = datafiles or [] hub = None from qtpy.QtCore import QTimer timer = QTimer() timer.setInterval(1000) timer.setSingleShot(True) timer.timeout.connect(splash.close) timer.start() if gluefile is not None: app = restore_session(gluefile) return app.start() if config is not None: glue.env = glue.config.load_configuration(search_path=[config]) data_collection = glue.core.DataCollection() hub = data_collection.hub splash.set_progress(100) session = glue.core.Session(data_collection=data_collection, hub=hub) ga = GlueApplication(session=session) if datafiles: datasets = load_data_files(datafiles) ga.add_datasets(data_collection, datasets, auto_merge=auto_merge) if startup_actions is not None: for name in startup_actions: ga.run_startup_action(name) return ga.start(maximized=maximized)
class BasePlot(PlotWidget, PyDMPrimitiveWidget): crosshair_position_updated = Signal(float, float) def __init__(self, parent=None, background='default', axisItems=None): PlotWidget.__init__(self, parent=parent, background=background, axisItems=axisItems) PyDMPrimitiveWidget.__init__(self) self.plotItem = self.getPlotItem() self.plotItem.hideButtons() self._auto_range_x = None self.setAutoRangeX(True) self._auto_range_y = None self.setAutoRangeY(True) self._min_x = 0.0 self._max_x = 1.0 self._min_y = 0.0 self._max_y = 1.0 self._show_x_grid = None self.setShowXGrid(False) self._show_y_grid = None self.setShowYGrid(False) self._show_right_axis = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawPlot) self._redraw_rate = 30 # Redraw at 30 Hz by default. self.maxRedrawRate = self._redraw_rate self._curves = [] self._title = None self._show_legend = False self._legend = self.addLegend() self._legend.hide() # Drawing crosshair on the ViewBox self.vertical_crosshair_line = None self.horizontal_crosshair_line = None self.crosshair_movement_proxy = None def addCurve(self, plot_item, curve_color=None): if curve_color is None: curve_color = utilities.colors.default_colors[ len(self._curves) % len(utilities.colors.default_colors)] plot_item.color_string = curve_color self._curves.append(plot_item) self.addItem(plot_item) self.redraw_timer.start() # Connect channels for chan in plot_item.channels(): if chan: chan.connect() # self._legend.addItem(plot_item, plot_item.curve_name) def removeCurve(self, plot_item): self.removeItem(plot_item) self._curves.remove(plot_item) if len(self._curves) < 1: self.redraw_timer.stop() # Disconnect channels for chan in plot_item.channels(): if chan: chan.disconnect() def removeCurveWithName(self, name): for curve in self._curves: if curve.name() == name: self.removeCurve(curve) def removeCurveAtIndex(self, index): curve_to_remove = self._curves[index] self.removeCurve(curve_to_remove) def setCurveAtIndex(self, index, new_curve): old_curve = self._curves[index] self._curves[index] = new_curve # self._legend.addItem(new_curve, new_curve.name()) self.removeCurve(old_curve) def curveAtIndex(self, index): return self._curves[index] def curves(self): return self._curves def clear(self): legend_items = [label.text for (sample, label) in self._legend.items] for item in legend_items: self._legend.removeItem(item) self.plotItem.clear() self._curves = [] @Slot() def redrawPlot(self): pass def getShowXGrid(self): return self._show_x_grid def setShowXGrid(self, value, alpha=None): self._show_x_grid = value self.showGrid(x=self._show_x_grid, alpha=alpha) def resetShowXGrid(self): self.setShowXGrid(False) showXGrid = Property("bool", getShowXGrid, setShowXGrid, resetShowXGrid) def getShowYGrid(self): return self._show_y_grid def setShowYGrid(self, value, alpha=None): self._show_y_grid = value self.showGrid(y=self._show_y_grid, alpha=alpha) def resetShowYGrid(self): self.setShowYGrid(False) showYGrid = Property("bool", getShowYGrid, setShowYGrid, resetShowYGrid) def getBackgroundColor(self): return self.backgroundBrush().color() def setBackgroundColor(self, color): if self.backgroundBrush().color() != color: self.setBackgroundBrush(QBrush(color)) backgroundColor = Property(QColor, getBackgroundColor, setBackgroundColor) def getAxisColor(self): return self.getAxis('bottom')._pen.color() def setAxisColor(self, color): if self.getAxis('bottom')._pen.color() != color: self.getAxis('bottom').setPen(color) self.getAxis('left').setPen(color) self.getAxis('top').setPen(color) self.getAxis('right').setPen(color) axisColor = Property(QColor, getAxisColor, setAxisColor) def getBottomAxisLabel(self): return self.getAxis('bottom').labelText def getShowRightAxis(self): """ Provide whether the right y-axis is being shown. Returns : bool ------- True if the graph shows the right y-axis. False if not. """ return self._show_right_axis def setShowRightAxis(self, show): """ Set whether the graph should show the right y-axis. Parameters ---------- show : bool True for showing the right axis; False is for not showing. """ if show: self.showAxis("right") else: self.hideAxis("right") self._show_right_axis = show showRightAxis = Property("bool", getShowRightAxis, setShowRightAxis) def getPlotTitle(self): if self._title is None: return "" return str(self._title) def setPlotTitle(self, value): self._title = str(value) if len(self._title) < 1: self._title = None self.setTitle(self._title) def resetPlotTitle(self): self._title = None self.setTitle(self._title) title = Property(str, getPlotTitle, setPlotTitle, resetPlotTitle) def getShowLegend(self): """ Check if the legend is being shown. Returns : bool ------- True if the legend is displayed on the graph; False if not. """ return self._show_legend def setShowLegend(self, value): """ Set to display the legend on the graph. Parameters ---------- value : bool True to display the legend; False is not. """ self._show_legend = value if self._show_legend: if self._legend is None: self._legend = self.addLegend() else: self._legend.show() else: if self._legend is not None: self._legend.hide() def resetShowLegend(self): """ Reset the legend display status to hidden. """ self.setShowLegend(False) showLegend = Property(bool, getShowLegend, setShowLegend, resetShowLegend) def getAutoRangeX(self): return self._auto_range_x def setAutoRangeX(self, value): self._auto_range_x = value if self._auto_range_x: self.plotItem.enableAutoRange(ViewBox.XAxis, enable=self._auto_range_x) def resetAutoRangeX(self): self.setAutoRangeX(True) def getAutoRangeY(self): return self._auto_range_y def setAutoRangeY(self, value): self._auto_range_y = value if self._auto_range_y: self.plotItem.enableAutoRange(ViewBox.YAxis, enable=self._auto_range_y) def resetAutoRangeY(self): self.setAutoRangeY(True) def getMinXRange(self): """ Minimum X-axis value visible on the plot. Returns ------- float """ return self.plotItem.viewRange()[0][0] def setMinXRange(self, new_min_x_range): """ Set the minimum X-axis value visible on the plot. Parameters ------- new_min_x_range : float """ if self._auto_range_x: return self._min_x = new_min_x_range self.plotItem.setXRange(self._min_x, self._max_x, padding=0) def getMaxXRange(self): """ Maximum X-axis value visible on the plot. Returns ------- float """ return self.plotItem.viewRange()[0][1] def setMaxXRange(self, new_max_x_range): """ Set the Maximum X-axis value visible on the plot. Parameters ------- new_max_x_range : float """ if self._auto_range_x: return self._max_x = new_max_x_range self.plotItem.setXRange(self._min_x, self._max_x, padding=0) def getMinYRange(self): """ Minimum Y-axis value visible on the plot. Returns ------- float """ return self.plotItem.viewRange()[1][0] def setMinYRange(self, new_min_y_range): """ Set the minimum Y-axis value visible on the plot. Parameters ------- new_min_y_range : float """ if self._auto_range_y: return self._min_y = new_min_y_range self.plotItem.setYRange(self._min_y, self._max_y, padding=0) def getMaxYRange(self): """ Maximum Y-axis value visible on the plot. Returns ------- float """ return self.plotItem.viewRange()[1][1] def setMaxYRange(self, new_max_y_range): """ Set the maximum Y-axis value visible on the plot. Parameters ------- new_max_y_range : float """ if self._auto_range_y: return self._max_y = new_max_y_range self.plotItem.setYRange(self._min_y, self._max_y, padding=0) @Property(bool) def mouseEnabledX(self): """ Whether or not mouse interactions are enabled for the X-axis. Returns ------- bool """ return self.plotItem.getViewBox().state['mouseEnabled'][0] @mouseEnabledX.setter def mouseEnabledX(self, x_enabled): """ Whether or not mouse interactions are enabled for the X-axis. Parameters ------- x_enabled : bool """ self.plotItem.setMouseEnabled(x=x_enabled) @Property(bool) def mouseEnabledY(self): """ Whether or not mouse interactions are enabled for the Y-axis. Returns ------- bool """ return self.plotItem.getViewBox().state['mouseEnabled'][1] @mouseEnabledY.setter def mouseEnabledY(self, y_enabled): """ Whether or not mouse interactions are enabled for the Y-axis. Parameters ------- y_enabled : bool """ self.plotItem.setMouseEnabled(y=y_enabled) @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0/self._redraw_rate)*1000)) def pausePlotting(self): self.redraw_timer.stop() if self.redraw_timer.isActive() else self.redraw_timer.start() return self.redraw_timer.isActive() def mouseMoved(self, evt): """ A handler for the crosshair feature. Every time the mouse move, the mouse coordinates are updated, and the horizontal and vertical hairlines will be redrawn at the new coordinate. If a PyDMDisplay object is available, that display will also have the x- and y- values to update on the UI. Parameters ------- evt: MouseEvent The mouse event type, from which the mouse coordinates are obtained. """ pos = evt[0] if self.sceneBoundingRect().contains(pos): mouse_point = self.getViewBox().mapSceneToView(pos) self.vertical_crosshair_line.setPos(mouse_point.x()) self.horizontal_crosshair_line.setPos(mouse_point.y()) self.crosshair_position_updated.emit(mouse_point.x(), mouse_point.y()) def enableCrosshair(self, is_enabled, starting_x_pos, starting_y_pos, vertical_angle=90, horizontal_angle=0, vertical_movable=False, horizontal_movable=False): """ Enable the crosshair to be drawn on the ViewBox. Parameters ---------- is_enabled : bool True is to draw the crosshair, False is to not draw. starting_x_pos : float The x coordinate where to start the vertical crosshair line. starting_y_pos : float The y coordinate where to start the horizontal crosshair line. vertical_angle : float The angle to tilt the vertical crosshair line. Default at 90 degrees. horizontal_angle The angle to tilt the horizontal crosshair line. Default at 0 degrees. vertical_movable : bool True if the vertical line can be moved by the user; False is not. horizontal_movable False if the horizontal line can be moved by the user; False is not. """ if is_enabled: self.vertical_crosshair_line = InfiniteLine(pos=starting_x_pos, angle=vertical_angle, movable=vertical_movable) self.horizontal_crosshair_line = InfiniteLine(pos=starting_y_pos, angle=horizontal_angle, movable=horizontal_movable) self.plotItem.addItem(self.vertical_crosshair_line) self.plotItem.addItem(self.horizontal_crosshair_line) self.crosshair_movement_proxy = SignalProxy(self.plotItem.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved) else: if self.vertical_crosshair_line: self.plotItem.removeItem(self.vertical_crosshair_line) if self.horizontal_crosshair_line: self.plotItem.removeItem(self.horizontal_crosshair_line) if self.crosshair_movement_proxy: self.crosshair_movement_proxy.disconnect()
class _ClientAPI(QObject): """ """ def __init__(self): super(QObject, self).__init__() self._anaconda_client_api = binstar_client.utils.get_server_api( log_level=logging.NOTSET) self._queue = deque() self._threads = [] self._workers = [] self._timer = QTimer() self._conda_api = CondaAPI() self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) def _clean(self): """ Periodically check for inactive workers and remove their references. """ if self._workers: for w in self._workers: if w.is_finished(): self._workers.remove(w) if self._threads: for t in self._threads: if t.isFinished(): self._threads.remove(t) else: self._timer.stop() def _start(self): """ """ if len(self._queue) == 1: thread = self._queue.popleft() thread.start() self._timer.start() def _create_worker(self, method, *args, **kwargs): """ Create a worker for this client to be run in a separate thread. """ # FIXME: this might be heavy... thread = QThread() worker = ClientWorker(method, args, kwargs) worker.moveToThread(thread) worker.sig_finished.connect(self._start) worker.sig_finished.connect(thread.quit) thread.started.connect(worker.start) self._queue.append(thread) self._threads.append(thread) self._workers.append(worker) self._start() return worker def _load_repodata(self, filepaths, extra_data={}, metadata={}): """ Load all the available pacakges information for downloaded repodata files (repo.continuum.io), additional data provided (anaconda cloud), and additional metadata and merge into a single set of packages and apps. """ repodata = [] for filepath in filepaths: compressed = filepath.endswith('.bz2') mode = 'rb' if filepath.endswith('.bz2') else 'r' if os.path.isfile(filepath): with open(filepath, mode) as f: raw_data = f.read() if compressed: data = bz2.decompress(raw_data) else: data = raw_data try: data = json.loads(to_text_string(data, 'UTF-8')) except Exception as error: logger.error(str(error)) data = {} repodata.append(data) all_packages = {} for data in repodata: packages = data.get('packages', {}) for canonical_name in packages: data = packages[canonical_name] name, version, b = tuple(canonical_name.rsplit('-', 2)) if name not in all_packages: all_packages[name] = {'versions': set(), 'size': {}, 'type': {}, 'app_entry': {}, 'app_type': {}, } elif name in metadata: temp_data = all_packages[name] temp_data['home'] = metadata[name].get('home', '') temp_data['license'] = metadata[name].get('license', '') temp_data['summary'] = metadata[name].get('summary', '') temp_data['latest_version'] = metadata[name].get('version') all_packages[name] = temp_data all_packages[name]['versions'].add(version) all_packages[name]['size'][version] = data.get('size', '') # Only the latest builds will have the correct metadata for # apps, so only store apps that have the app metadata if data.get('type', None): all_packages[name]['type'][version] = data.get( 'type', None) all_packages[name]['app_entry'][version] = data.get( 'app_entry', None) all_packages[name]['app_type'][version] = data.get( 'app_type', None) all_apps = {} for name in all_packages: versions = sort_versions(list(all_packages[name]['versions'])) all_packages[name]['versions'] = versions[:] for version in versions: has_type = all_packages[name].get('type', None) # Has type in this case implies being an app if has_type: all_apps[name] = all_packages[name].copy() # Remove all versions that are not apps! versions = all_apps[name]['versions'][:] types = all_apps[name]['type'] app_versions = [v for v in versions if v in types] all_apps[name]['versions'] = app_versions return all_packages, all_apps def _prepare_model_data(self, packages, linked, pip=[], private_packages={}): """ """ data = [] if private_packages is not None: for pkg in private_packages: if pkg in packages: p_data = packages.get(pkg, None) versions = p_data.get('versions', '') if p_data else [] private_versions = private_packages[pkg]['versions'] all_versions = sort_versions(list(set(versions + private_versions))) packages[pkg]['versions'] = all_versions else: private_versions = sort_versions(private_packages[pkg]['versions']) private_packages[pkg]['versions'] = private_versions packages[pkg] = private_packages[pkg] else: private_packages = {} linked_packages = {} for canonical_name in linked: name, version, b = tuple(canonical_name.rsplit('-', 2)) linked_packages[name] = {'version': version} pip_packages = {} for canonical_name in pip: name, version, b = tuple(canonical_name.rsplit('-', 2)) pip_packages[name] = {'version': version} packages_names = sorted(list(set(list(linked_packages.keys()) + list(pip_packages.keys()) + list(packages.keys()) + list(private_packages.keys()) ) ) ) for name in packages_names: p_data = packages.get(name, None) summary = p_data.get('summary', '') if p_data else '' url = p_data.get('home', '') if p_data else '' license_ = p_data.get('license', '') if p_data else '' versions = p_data.get('versions', '') if p_data else [] version = p_data.get('latest_version', '') if p_data else '' if name in pip_packages: type_ = C.PIP_PACKAGE version = pip_packages[name].get('version', '') status = C.INSTALLED elif name in linked_packages: type_ = C.CONDA_PACKAGE version = linked_packages[name].get('version', '') status = C.INSTALLED if version in versions: vers = versions upgradable = not version == vers[-1] and len(vers) != 1 downgradable = not version == vers[0] and len(vers) != 1 if upgradable and downgradable: status = C.MIXGRADABLE elif upgradable: status = C.UPGRADABLE elif downgradable: status = C.DOWNGRADABLE else: type_ = C.CONDA_PACKAGE status = C.NOT_INSTALLED if version == '' and len(versions) != 0: version = versions[-1] row = {C.COL_ACTION: C.ACTION_NONE, C.COL_PACKAGE_TYPE: type_, C.COL_NAME: name, C.COL_DESCRIPTION: summary.capitalize(), C.COL_VERSION: version, C.COL_STATUS: status, C.COL_URL: url, C.COL_LICENSE: license_, C.COL_INSTALL: False, C.COL_REMOVE: False, C.COL_UPGRADE: False, C.COL_DOWNGRADE: False, C.COL_ACTION_VERSION: None } data.append(row) return data # --- Public API # ------------------------------------------------------------------------- def login(self, username, password, application, application_url): """ Login to anaconda cloud. """ logger.debug(str((username, application, application_url))) method = self._anaconda_client_api.authenticate return self._create_worker(method, username, password, application, application_url) def logout(self): """ Logout from anaconda cloud. """ logger.debug('Logout') method = self._anaconda_client_api.remove_authentication return self._create_worker(method) def authentication(self): """ """ # logger.debug('') method = self._anaconda_client_api.user return self._create_worker(method) def load_repodata(self, filepaths, extra_data={}, metadata={}): """ Load all the available pacakges information for downloaded repodata files (repo.continuum.io), additional data provided (anaconda cloud), and additional metadata and merge into a single set of packages and apps. """ logger.debug(str((filepaths))) method = self._load_repodata return self._create_worker(method, filepaths, extra_data=extra_data, metadata=metadata) def prepare_model_data(self, packages, linked, pip=[], private_packages={}): """ """ logger.debug('') return self._prepare_model_data(packages, linked, pip=pip, private_packages=private_packages) # method = self._prepare_model_data # return self._create_worker(method, packages, linked, pip) def set_domain(self, domain='https://api.anaconda.org'): """ """ logger.debug(str((domain))) config = binstar_client.utils.get_config() config['url'] = domain binstar_client.utils.set_config(config) self._anaconda_client_api = binstar_client.utils.get_server_api( token=None, log_level=logging.NOTSET) return self.user() def store_token(self, token): """ """ class args: site = None binstar_client.utils.store_token(token, args) def remove_token(self): """ """ class args: site = None binstar_client.utils.remove_token(args) def user(self): try: user = self._anaconda_client_api.user() except Exception: user = {} return user def domain(self): return self._anaconda_client_api.domain def packages(self, login=None, platform=None, package_type=None, type_=None, access=None): """ :param type_: only find packages that have this conda `type` (i.e. 'app') :param access: only find packages that have this access level (e.g. 'private', 'authenticated', 'public') """ # data = self._anaconda_client_api.user_packages( # login=login, # platform=platform, # package_type=package_type, # type_=type_, # access=access) logger.debug('') method = self._anaconda_client_api.user_packages return self._create_worker(method, login=login, platform=platform, package_type=package_type, type_=type_, access=access) def _multi_packages(self, logins=None, platform=None, package_type=None, type_=None, access=None, new_client=True): private_packages = {} if not new_client: time.sleep(0.3) return private_packages for login in logins: data = self._anaconda_client_api.user_packages( login=login, platform=platform, package_type=package_type, type_=type_, access=access) for item in data: name = item.get('name', '') public = item.get('public', True) package_types = item.get('package_types', []) latest_version = item.get('latest_version', '') if name and not public and 'conda' in package_types: if name in private_packages: versions = private_packages.get('versions', []), new_versions = item.get('versions', []), vers = sort_versions(list(set(versions + new_versions ))) private_packages[name]['versions'] = vers private_packages[name]['latest_version'] = vers[-1] else: private_packages[name] = { 'versions': item.get('versions', []), 'app_entry': {}, 'type': {}, 'size': {}, 'latest_version': latest_version, } return private_packages def multi_packages(self, logins=None, platform=None, package_type=None, type_=None, access=None): """ Get all the private packages for a given set of usernames (logins) """ logger.debug('') method = self._multi_packages new_client = True try: # Only the newer versions have extra keywords like `access` self._anaconda_client_api.user_packages(access='private') except Exception: new_client = False return self._create_worker(method, logins=logins, platform=platform, package_type=package_type, type_=type_, access=access, new_client=new_client) def organizations(self, login=None): """ List all the organizations a user has access to. """ return self._anaconda_client_api.user(login=login) def load_token(self, url): token = binstar_client.utils.load_token(url) return token
class ProcessWorker(QObject): """Process worker based on a QProcess for non blocking UI.""" sig_started = Signal(object) sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, environ=None): """ Process worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. environ : dict Process environment, """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._fired = False self._communicate_first = False self._partial_stdout = None self._started = False self._timer = QTimer() self._process = QProcess() self._set_environment(environ) self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _get_encoding(self): """Return the encoding/codepage to use.""" enco = 'utf-8' # Currently only cp1252 is allowed? if WIN: import ctypes codepage = to_text_string(ctypes.cdll.kernel32.GetACP()) # import locale # locale.getpreferredencoding() # Differences? enco = 'cp' + codepage return enco def _set_environment(self, environ): """Set the environment on the QProcess.""" if environ: q_environ = self._process.processEnvironment() for k, v in environ.items(): q_environ.insert(k, v) self._process.setProcessEnvironment(q_environ) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, self._get_encoding()) if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() enco = self._get_encoding() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, enco) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, enco) result = [stdout.encode(enco), stderr.encode(enco)] if PY2: stderr = stderr.decode() result[-1] = '' self._result = result if not self._fired: self.sig_finished.emit(self, result[0], result[-1]) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def _start(self): """Start process.""" if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() def terminate(self): """Terminate running processes.""" if self._process.state() == QProcess.Running: try: self._process.terminate() except Exception: pass self._fired = True def start(self): """Start worker.""" if not self._started: self.sig_started.emit(self) self._started = True
class PyDMTimePlot(BasePlot): """ PyDMWaveformPlot is a widget to plot one or more waveforms. Each curve can plot either a Y-axis waveform vs. its indices, or a Y-axis waveform against an X-axis waveform. Parameters ---------- parent : optional The parent of this widget. init_y_channels : list A list of scalar channels to plot vs time. plot_by_timestamps : bool If True, the x-axis shows timestamps as ticks, and those timestamps scroll to the left as time progresses. If False, the x-axis tick marks show time relative to the current time. background: optional The background color for the plot. Accepts any arguments that pyqtgraph.mkColor will accept. """ SynchronousMode = 1 AsynchronousMode = 2 plot_redrawn_signal = Signal(TimePlotCurveItem) def __init__(self, parent=None, init_y_channels=[], plot_by_timestamps=True, background='default'): """ Parameters ---------- parent : Widget The parent widget of the chart. init_y_channels : list A list of scalar channels to plot vs time. plot_by_timestamps : bool If True, the x-axis shows timestamps as ticks, and those timestamps scroll to the left as time progresses. If False, the x-axis tick marks show time relative to the current time. background : str, optional The background color for the plot. Accepts any arguments that pyqtgraph.mkColor will accept. """ self._plot_by_timestamps = plot_by_timestamps self._left_axis = AxisItem("left") if plot_by_timestamps: self._bottom_axis = TimeAxisItem('bottom') else: self.starting_epoch_time = time.time() self._bottom_axis = AxisItem('bottom') super(PyDMTimePlot, self).__init__(parent=parent, background=background, axisItems={"bottom": self._bottom_axis, "left": self._left_axis}) # Removing the downsampling while PR 763 is not merged at pyqtgraph # Reference: https://github.com/pyqtgraph/pyqtgraph/pull/763 # self.setDownsampling(ds=True, auto=True, mode="mean") if self._plot_by_timestamps: self.plotItem.disableAutoRange(ViewBox.XAxis) self.getViewBox().setMouseEnabled(x=False) else: self.plotItem.setRange(xRange=[DEFAULT_X_MIN, 0], padding=0) self.plotItem.setLimits(xMax=0) self._bufferSize = DEFAULT_BUFFER_SIZE self._time_span = DEFAULT_TIME_SPAN # This is in seconds self._update_interval = DEFAULT_UPDATE_INTERVAL self.update_timer = QTimer(self) self.update_timer.setInterval(self._update_interval) self._update_mode = PyDMTimePlot.SynchronousMode self._needs_redraw = True self.labels = { "left": None, "right": None, "bottom": None } self.units = { "left": None, "right": None, "bottom": None } for channel in init_y_channels: self.addYChannel(channel) def initialize_for_designer(self): # If we are in Qt Designer, don't update the plot continuously. # This function gets called by PyDMTimePlot's designer plugin. self.redraw_timer.setSingleShot(True) def addYChannel(self, y_channel=None, name=None, color=None, lineStyle=None, lineWidth=None, symbol=None, symbolSize=None): """ Adds a new curve to the current plot Parameters ---------- y_channel : str The PV address name : str The name of the curve (usually made the same as the PV address) color : QColor The color for the curve lineStyle : str The line style of the curve, i.e. solid, dash, dot, etc. lineWidth : int How thick the curve line should be symbol : str The symbols as markers along the curve, i.e. circle, square, triangle, star, etc. symbolSize : int How big the symbols should be Returns ------- new_curve : TimePlotCurveItem The newly created curve. """ plot_opts = dict() plot_opts['symbol'] = symbol if symbolSize is not None: plot_opts['symbolSize'] = symbolSize if lineStyle is not None: plot_opts['lineStyle'] = lineStyle if lineWidth is not None: plot_opts['lineWidth'] = lineWidth # Add curve new_curve = TimePlotCurveItem(y_channel, plot_by_timestamps=self._plot_by_timestamps, name=name, color=color, **plot_opts) new_curve.setUpdatesAsynchronously(self.updatesAsynchronously) new_curve.setBufferSize(self._bufferSize) self.update_timer.timeout.connect(new_curve.asyncUpdate) self.addCurve(new_curve, curve_color=color) new_curve.data_changed.connect(self.set_needs_redraw) self.redraw_timer.start() return new_curve def removeYChannel(self, curve): """ Remove a curve from the graph. This also stops update the timer associated with the curve. Parameters ---------- curve : TimePlotCurveItem The curve to be removed. """ self.update_timer.timeout.disconnect(curve.asyncUpdate) self.removeCurve(curve) if len(self._curves) < 1: self.redraw_timer.stop() def removeYChannelAtIndex(self, index): """ Remove a curve from the graph, given its index in the graph's curve list. Parameters ---------- index : int The curve's index from the graph's curve list. """ curve = self._curves[index] self.removeYChannel(curve) @Slot() def set_needs_redraw(self): self._needs_redraw = True @Slot() def redrawPlot(self): """ Redraw the graph """ if not self._needs_redraw: return self.updateXAxis() for curve in self._curves: curve.redrawCurve() self.plot_redrawn_signal.emit(curve) self._needs_redraw = False def updateXAxis(self, update_immediately=False): """ Update the x-axis for every graph redraw. Parameters ---------- update_immediately : bool Update the axis range(s) immediately if True, or defer until the next rendering. """ if len(self._curves) == 0: return if self._plot_by_timestamps: if self._update_mode == PyDMTimePlot.SynchronousMode: maxrange = max([curve.max_x() for curve in self._curves]) else: maxrange = time.time() minrange = maxrange - self._time_span self.plotItem.setXRange(minrange, maxrange, padding=0.0, update=update_immediately) else: diff_time = self.starting_epoch_time - max([curve.max_x() for curve in self._curves]) if diff_time > DEFAULT_X_MIN: diff_time = DEFAULT_X_MIN self.getViewBox().setLimits(minXRange=diff_time) def clearCurves(self): """ Remove all curves from the graph. """ super(PyDMTimePlot, self).clear() def getCurves(self): """ Dump the current list of curves and each curve's settings into a list of JSON-formatted strings. Returns ------- settings : list A list of JSON-formatted strings, each containing a curve's settings """ return [json.dumps(curve.to_dict()) for curve in self._curves] def setCurves(self, new_list): """ Add a list of curves into the graph. Parameters ---------- new_list : list A list of JSON-formatted strings, each contains a curve and its settings """ try: new_list = [json.loads(str(i)) for i in new_list] except ValueError as e: logger.exception("Error parsing curve json data: {}".format(e)) return self.clearCurves() for d in new_list: color = d.get('color') if color: color = QColor(color) self.addYChannel(d['channel'], name=d.get('name'), color=color, lineStyle=d.get('lineStyle'), lineWidth=d.get('lineWidth'), symbol=d.get('symbol'), symbolSize=d.get('symbolSize')) curves = Property("QStringList", getCurves, setCurves) def findCurve(self, pv_name): """ Find a curve from a graph's curve list. Parameters ---------- pv_name : str The curve's PV address. Returns ------- curve : TimePlotCurveItem The found curve, or None. """ for curve in self._curves: if curve.address == pv_name: return curve def refreshCurve(self, curve): """ Remove a curve currently being plotted on the timeplot, then redraw that curve, which could have been updated with a new symbol, line style, line width, etc. Parameters ---------- curve : TimePlotCurveItem The curve to be re-added. """ curve = self.findCurve(curve.channel) if curve: self.removeYChannel(curve) self.addYChannel(y_channel=curve.address, color=curve.color, name=curve.address, lineStyle=curve.lineStyle, lineWidth=curve.lineWidth, symbol=curve.symbol, symbolSize=curve.symbolSize) def addLegendItem(self, item, pv_name, force_show_legend=False): """ Add an item into the graph's legend. Parameters ---------- item : TimePlotCurveItem A curve being plotted in the graph pv_name : str The PV channel force_show_legend : bool True to make the legend to be displayed; False to just add the item, but do not display the legend. """ self._legend.addItem(item, pv_name) self.setShowLegend(force_show_legend) def removeLegendItem(self, pv_name): """ Remove an item from the legend. Parameters ---------- pv_name : str The PV channel, used to search for the legend item to remove. """ self._legend.removeItem(pv_name) if len(self._legend.items) == 0: self.setShowLegend(False) def getBufferSize(self): """ Get the size of the data buffer for the entire chart. Returns ------- size : int The chart's data buffer size. """ return int(self._bufferSize) def setBufferSize(self, value): """ Set the size of the data buffer of the entire chart. This will also update the same value for each of the data buffer of each chart's curve. Parameters ---------- value : int The new buffer size for the chart. """ if self._bufferSize != int(value): # Originally, the bufferSize is the max between the user's input and 1, and 1 doesn't make sense. # So, I'm comparing the user's input with the minimum buffer size, and pick the max between the two self._bufferSize = max(int(value), MINIMUM_BUFFER_SIZE) for curve in self._curves: curve.setBufferSize(value) def resetBufferSize(self): """ Reset the data buffer size of the chart, and each of the chart's curve's data buffer, to the minimum """ if self._bufferSize != DEFAULT_BUFFER_SIZE: self._bufferSize = DEFAULT_BUFFER_SIZE for curve in self._curves: curve.resetBufferSize() bufferSize = Property("int", getBufferSize, setBufferSize, resetBufferSize) def getUpdatesAsynchronously(self): return self._update_mode == PyDMTimePlot.AsynchronousMode def setUpdatesAsynchronously(self, value): for curve in self._curves: curve.setUpdatesAsynchronously(value) if value is True: self._update_mode = PyDMTimePlot.AsynchronousMode self.update_timer.start() else: self._update_mode = PyDMTimePlot.SynchronousMode self.update_timer.stop() def resetUpdatesAsynchronously(self): self._update_mode = PyDMTimePlot.SynchronousMode self.update_timer.stop() for curve in self._curves: curve.resetUpdatesAsynchronously() updatesAsynchronously = Property("bool", getUpdatesAsynchronously, setUpdatesAsynchronously, resetUpdatesAsynchronously) def getTimeSpan(self): """ The extent of the x-axis of the chart, in seconds. In other words, how long a data point stays on the plot before falling off the left edge. Returns ------- time_span : float The extent of the x-axis of the chart, in seconds. """ return float(self._time_span) def setTimeSpan(self, value): """ Set the extent of the x-axis of the chart, in seconds. In aynchronous mode, the chart will allocate enough buffer for the new time span duration. Data arriving after each duration will be recorded into the buffer having been rotated. Parameters ---------- value : float The time span duration, in seconds, to allocate enough buffer to collect data for, before rotating the buffer. """ value = float(value) if self._time_span != value: self._time_span = value if self.getUpdatesAsynchronously(): self.setBufferSize(int((self._time_span * 1000.0) / self._update_interval)) self.updateXAxis(update_immediately=True) def resetTimeSpan(self): """ Reset the timespan to the default value. """ if self._time_span != DEFAULT_TIME_SPAN: self._time_span = DEFAULT_TIME_SPAN if self.getUpdatesAsynchronously(): self.setBufferSize(int((self._time_span * 1000.0) / self._update_interval)) self.updateXAxis(update_immediately=True) timeSpan = Property(float, getTimeSpan, setTimeSpan, resetTimeSpan) def getUpdateInterval(self): """ Get the update interval for the chart. Returns ------- interval : float The update interval of the chart. """ return float(self._update_interval) / 1000.0 def setUpdateInterval(self, value): """ Set a new update interval for the chart and update its data buffer size. Parameters ---------- value : float The new update interval value. """ value = abs(int(1000.0 * value)) if self._update_interval != value: self._update_interval = value self.update_timer.setInterval(self._update_interval) if self.getUpdatesAsynchronously(): self.setBufferSize(int((self._time_span * 1000.0) / self._update_interval)) def resetUpdateInterval(self): """ Reset the chart's update interval to the default. """ if self._update_interval != DEFAULT_UPDATE_INTERVAL: self._update_interval = DEFAULT_UPDATE_INTERVAL self.update_timer.setInterval(self._update_interval) if self.getUpdatesAsynchronously(): self.setBufferSize(int((self._time_span * 1000.0) / self._update_interval)) updateInterval = Property(float, getUpdateInterval, setUpdateInterval, resetUpdateInterval) def getAutoRangeX(self): if self._plot_by_timestamps: return False else: super(PyDMTimePlot, self).getAutoRangeX() def setAutoRangeX(self, value): if self._plot_by_timestamps: self._auto_range_x = False self.plotItem.enableAutoRange(ViewBox.XAxis, enable=self._auto_range_x) else: super(PyDMTimePlot, self).setAutoRangeX(value) def channels(self): return [curve.channel for curve in self._curves] # The methods for autoRangeY, minYRange, and maxYRange are # all defined in BasePlot, but we don't expose them as properties there, because not all plot # subclasses necessarily want them to be user-configurable in Designer. autoRangeY = Property(bool, BasePlot.getAutoRangeY, BasePlot.setAutoRangeY, BasePlot.resetAutoRangeY, doc=""" Whether or not the Y-axis automatically rescales to fit the data. If true, the values in minYRange and maxYRange are ignored. """) minYRange = Property(float, BasePlot.getMinYRange, BasePlot.setMinYRange, doc=""" Minimum Y-axis value visible on the plot.""") maxYRange = Property(float, BasePlot.getMaxYRange, BasePlot.setMaxYRange, doc=""" Maximum Y-axis value visible on the plot.""") def enableCrosshair(self, is_enabled, starting_x_pos=DEFAULT_X_MIN, starting_y_pos=DEFAULT_Y_MIN, vertical_angle=90, horizontal_angle=0, vertical_movable=False, horizontal_movable=False): """ Display a crosshair on the graph. Parameters ---------- is_enabled : bool True is to display the crosshair; False is to hide it. starting_x_pos : float The x position where the vertical line will cross starting_y_pos : float The y position where the horizontal line will cross vertical_angle : int The angle of the vertical line horizontal_angle : int The angle of the horizontal line vertical_movable : bool True if the user can move the vertical line; False if not horizontal_movable : bool True if the user can move the horizontal line; False if not """ super(PyDMTimePlot, self).enableCrosshair(is_enabled, starting_x_pos, starting_y_pos, vertical_angle, horizontal_angle, vertical_movable, horizontal_movable)
class PyDMImageView(ImageView, PyDMWidget, PyDMColorMap, ReadingOrder): """ A PyQtGraph ImageView with support for Channels and more from PyDM. If there is no :attr:`channelWidth` it is possible to define the width of the image with the :attr:`width` property. The :attr:`normalizeData` property defines if the colors of the images are relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to the minimum and maximum values of the image. Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new image is rendered in the widget. Parameters ---------- parent : QWidget The parent widget for the Label image_channel : str, optional The channel to be used by the widget for the image data. width_channel : str, optional The channel to be used by the widget to receive the image width information """ ReadingOrder = ReadingOrder Q_ENUMS(ReadingOrder) Q_ENUMS(PyDMColorMap) color_maps = cmaps def __init__(self, parent=None, image_channel=None, width_channel=None): """Initialize widget.""" # Set the default colormap. self._colormap = PyDMColorMap.Inferno self._cm_colors = None self._imagechannel = None self._widthchannel = None self.image_waveform = np.zeros(0) self._image_width = 0 self._normalize_data = False self._auto_downsample = True self._show_axes = False # Set default reading order of numpy array data to Fortranlike. self._reading_order = ReadingOrder.Fortranlike self._redraw_rate = 30 # Set color map limits. self.cm_min = 0.0 self.cm_max = 255.0 plot_item = PlotItem() ImageView.__init__(self, parent, view=plot_item) PyDMWidget.__init__(self) self._channels = [None, None] self.thread = None self.axes = dict({'t': None, "x": 0, "y": 1, "c": None}) self.showAxes = self._show_axes # Hide some itens of the widget. self.ui.histogram.hide() self.getImageItem().sigImageChanged.disconnect( self.ui.histogram.imageChanged) self.ui.roiBtn.hide() self.ui.menuBtn.hide() # Make a right-click menu for changing the color map. self.cm_group = QActionGroup(self) self.cmap_for_action = {} for cm in self.color_maps: action = self.cm_group.addAction(cmap_names[cm]) action.setCheckable(True) self.cmap_for_action[action] = cm self.colorMap = self._colormap # Setup the redraw timer. self.needs_redraw = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawImage) self.maxRedrawRate = self._redraw_rate self.newImageSignal = self.getImageItem().sigImageChanged # Set live channels if requested on initialization if image_channel: self.imageChannel = image_channel or '' if width_channel: self.widthChannel = width_channel or '' @Property(str, designable=False) def channel(self): return @channel.setter def channel(self, ch): if not ch: return logger.info("Use the imageChannel property with the ImageView widget.") return def widget_ctx_menu(self): """ Fetch the Widget specific context menu. It will be populated with additional tools by `assemble_tools_menu`. Returns ------- QMenu or None If the return of this method is None a new QMenu will be created by `assemble_tools_menu`. """ self.menu = ViewBoxMenu(self.getView().getViewBox()) cm_menu = self.menu.addMenu("Color Map") for act in self.cmap_for_action.keys(): cm_menu.addAction(act) cm_menu.triggered.connect(self._changeColorMap) return self.menu def _changeColorMap(self, action): """ Method invoked by the colormap Action Menu. Changes the current colormap used to render the image. Parameters ---------- action : QAction """ self.colorMap = self.cmap_for_action[action] @Property(float) def colorMapMin(self): """ Minimum value for the colormap. Returns ------- float """ return self.cm_min @colorMapMin.setter @Slot(float) def colorMapMin(self, new_min): """ Set the minimum value for the colormap. Parameters ---------- new_min : float """ if self.cm_min != new_min: self.cm_min = new_min if self.cm_min > self.cm_max: self.cm_max = self.cm_min @Property(float) def colorMapMax(self): """ Maximum value for the colormap. Returns ------- float """ return self.cm_max @colorMapMax.setter @Slot(float) def colorMapMax(self, new_max): """ Set the maximum value for the colormap. Parameters ---------- new_max : float """ if self.cm_max != new_max: self.cm_max = new_max if self.cm_max < self.cm_min: self.cm_min = self.cm_max def setColorMapLimits(self, mn, mx): """ Set the limit values for the colormap. Parameters ---------- mn : int The lower limit mx : int The upper limit """ if mn >= mx: return self.cm_max = mx self.cm_min = mn @Property(PyDMColorMap) def colorMap(self): """ Return the color map used by the ImageView. Returns ------- PyDMColorMap """ return self._colormap @colorMap.setter def colorMap(self, new_cmap): """ Set the color map used by the ImageView. Parameters ------- new_cmap : PyDMColorMap """ self._colormap = new_cmap self._cm_colors = self.color_maps[new_cmap] self.setColorMap() for action in self.cm_group.actions(): if self.cmap_for_action[action] == self._colormap: action.setChecked(True) else: action.setChecked(False) def setColorMap(self, cmap=None): """ Update the image colormap. Parameters ---------- cmap : ColorMap """ if not cmap: if not self._cm_colors.any(): return # Take default values pos = np.linspace(0.0, 1.0, num=len(self._cm_colors)) cmap = ColorMap(pos, self._cm_colors) self.getView().getViewBox().setBackgroundColor(cmap.map(0)) lut = cmap.getLookupTable(0.0, 1.0, alpha=False) self.getImageItem().setLookupTable(lut) @Slot(bool) def image_connection_state_changed(self, conn): """ Callback invoked when the Image Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ if conn: self.redraw_timer.start() else: self.redraw_timer.stop() @Slot(np.ndarray) def image_value_changed(self, new_image): """ Callback invoked when the Image Channel value is changed. We try to do as little as possible in this method, because it gets called every time the image channel updates, which might be extremely often. Basically just store the data, and set a flag requesting that the image be redrawn. Parameters ---------- new_image : np.ndarray The new image data. This can be a flat 1D array, or a 2D array. """ if new_image is None or new_image.size == 0: return logging.debug("ImageView Received New Image - Needs Redraw -> True") self.image_waveform = new_image self.needs_redraw = True @Slot(int) def image_width_changed(self, new_width): """ Callback invoked when the Image Width Channel value is changed. Parameters ---------- new_width : int The new image width """ if new_width is None: return self._image_width = int(new_width) def process_image(self, image): """ Boilerplate method to be used by applications in order to add calculations and also modify the image before it is displayed at the widget. .. warning:: This code runs in a separated QThread so it **MUST** not try to write to QWidgets. Parameters ---------- image : np.ndarray The Image Data as a 2D numpy array Returns ------- np.ndarray The Image Data as a 2D numpy array after processing. """ return image def redrawImage(self): """ Set the image data into the ImageItem, if needed. If necessary, reshape the image to 2D first. """ if self.thread is not None and not self.thread.isFinished(): logger.warning( "Image processing has taken longer than the refresh rate.") return self.thread = ImageUpdateThread(self) self.thread.updateSignal.connect(self.__updateDisplay) logging.debug("ImageView RedrawImage Thread Launched") self.thread.start() @Slot(list) def __updateDisplay(self, data): logging.debug("ImageView Update Display with new image") mini, maxi = data[0], data[1] img = data[2] self.getImageItem().setLevels([mini, maxi]) self.getImageItem().setImage( img, autoLevels=False, autoDownsample=self.autoDownsample) @Property(bool) def autoDownsample(self): """ Return if we should or not apply the autoDownsample option to PyQtGraph. Return ------ bool """ return self._auto_downsample @autoDownsample.setter def autoDownsample(self, new_value): """ Whether we should or not apply the autoDownsample option to PyQtGraph. Parameters ---------- new_value: bool """ if new_value != self._auto_downsample: self._auto_downsample = new_value @Property(int) def imageWidth(self): """ Return the width of the image. Return ------ int """ return self._image_width @imageWidth.setter def imageWidth(self, new_width): """ Set the width of the image. Can be overridden by :attr:`widthChannel`. Parameters ---------- new_width: int """ if (self._image_width != int(new_width) and (self._widthchannel is None or self._widthchannel == '')): self._image_width = int(new_width) @Property(bool) def normalizeData(self): """ Return True if the colors are relative to data maximum and minimum. Returns ------- bool """ return self._normalize_data @normalizeData.setter @Slot(bool) def normalizeData(self, new_norm): """ Define if the colors are relative to minimum and maximum of the data. Parameters ---------- new_norm: bool """ if self._normalize_data != new_norm: self._normalize_data = new_norm @Property(ReadingOrder) def readingOrder(self): """ Return the reading order of the :attr:`imageChannel` array. Returns ------- ReadingOrder """ return self._reading_order @readingOrder.setter def readingOrder(self, new_order): """ Set reading order of the :attr:`imageChannel` array. Parameters ---------- new_order: ReadingOrder """ if self._reading_order != new_order: self._reading_order = new_order def keyPressEvent(self, ev): """Handle keypress events.""" return @Property(str) def imageChannel(self): """ The channel address in use for the image data . Returns ------- str Channel address """ if self._imagechannel: return str(self._imagechannel.address) else: return '' @imageChannel.setter def imageChannel(self, value): """ The channel address in use for the image data . Parameters ---------- value : str Channel address """ if self._imagechannel != value: # Disconnect old channel if self._imagechannel: self._imagechannel.disconnect() # Create and connect new channel self._imagechannel = PyDMChannel( address=value, connection_slot=self.image_connection_state_changed, value_slot=self.image_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[0] = self._imagechannel self._imagechannel.connect() @Property(str) def widthChannel(self): """ The channel address in use for the image width . Returns ------- str Channel address """ if self._widthchannel: return str(self._widthchannel.address) else: return '' @widthChannel.setter def widthChannel(self, value): """ The channel address in use for the image width . Parameters ---------- value : str Channel address """ if self._widthchannel != value: # Disconnect old channel if self._widthchannel: self._widthchannel.disconnect() # Create and connect new channel self._widthchannel = PyDMChannel( address=value, connection_slot=self.connectionStateChanged, value_slot=self.image_width_changed, severity_slot=self.alarmSeverityChanged) self._channels[1] = self._widthchannel self._widthchannel.connect() def channels(self): """ Return the channels being used for this Widget. Returns ------- channels : list List of PyDMChannel objects """ return self._channels def channels_for_tools(self): """Return channels for tools.""" return [self._imagechannel] @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000)) @Property(bool) def showAxes(self): """ Whether or not axes should be shown on the widget. """ return self._show_axes @showAxes.setter def showAxes(self, show): self._show_axes = show self.getView().showAxis('left', show=show) self.getView().showAxis('bottom', show=show) @Property(float) def scaleXAxis(self): """ Sets the scale for the X Axis. For example, if your image has 100 pixels per millimeter, you can set xAxisScale to 1/100 = 0.01 to make the X Axis report in millimeter units. """ # protect against access to not yet initialized view if hasattr(self, 'view'): return self.getView().getAxis('bottom').scale return None @scaleXAxis.setter def scaleXAxis(self, new_scale): self.getView().getAxis('bottom').setScale(new_scale) @Property(float) def scaleYAxis(self): """ Sets the scale for the Y Axis. For example, if your image has 100 pixels per millimeter, you can set yAxisScale to 1/100 = 0.01 to make the Y Axis report in millimeter units. """ # protect against access to not yet initialized view if hasattr(self, 'view'): return self.getView().getAxis('left').scale return None @scaleYAxis.setter def scaleYAxis(self, new_scale): self.getView().getAxis('left').setScale(new_scale)
class _RequestsDownloadAPI(QObject): """ """ _sig_download_finished = Signal(str, str) _sig_download_progress = Signal(str, str, int, int) def __init__(self): super(QObject, self).__init__() self._conda_api = CondaAPI() self._queue = deque() self._threads = [] self._workers = [] self._timer = QTimer() self._chunk_size = 1024 self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) def _clean(self): """ Periodically check for inactive workers and remove their references. """ if self._workers: for w in self._workers: if w.is_finished(): self._workers.remove(w) if self._threads: for t in self._threads: if t.isFinished(): self._threads.remove(t) else: self._timer.stop() def _start(self): """ """ if len(self._queue) == 1: thread = self._queue.popleft() thread.start() self._timer.start() def _create_worker(self, method, *args, **kwargs): """ """ # FIXME: this might be heavy... thread = QThread() worker = RequestsDownloadWorker(method, args, kwargs) worker.moveToThread(thread) worker.sig_finished.connect(self._start) self._sig_download_finished.connect(worker.sig_download_finished) self._sig_download_progress.connect(worker.sig_download_progress) worker.sig_finished.connect(thread.quit) thread.started.connect(worker.start) self._queue.append(thread) self._threads.append(thread) self._workers.append(worker) self._start() return worker def _download(self, url, path=None, force=False): """ """ if path is None: path = url.split('/')[-1] # Make dir if non existent folder = os.path.dirname(os.path.abspath(path)) if not os.path.isdir(folder): os.makedirs(folder) # Start actual download try: r = requests.get(url, stream=True) except Exception as error: logger.error(str(error)) # Break if error found! # self._sig_download_finished.emit(url, path) # return path total_size = int(r.headers.get('Content-Length', 0)) # Check if file exists if os.path.isfile(path) and not force: file_size = os.path.getsize(path) # Check if existing file matches size of requested file if file_size == total_size: self._sig_download_finished.emit(url, path) return path # File not found or file size did not match. Download file. progress_size = 0 with open(path, 'wb') as f: for chunk in r.iter_content(chunk_size=self._chunk_size): if chunk: f.write(chunk) progress_size += len(chunk) self._sig_download_progress.emit(url, path, progress_size, total_size) self._sig_download_finished.emit(url, path) return path def _is_valid_url(self, url): try: r = requests.head(url) value = r.status_code in [200] except Exception as error: logger.error(str(error)) value = False return value def _is_valid_channel(self, channel, conda_url='https://conda.anaconda.org'): """ """ if channel.startswith('https://') or channel.startswith('http://'): url = channel else: url = "{0}/{1}".format(conda_url, channel) if url[-1] == '/': url = url[:-1] plat = self._conda_api.get_platform() repodata_url = "{0}/{1}/{2}".format(url, plat, 'repodata.json') try: r = requests.head(repodata_url) value = r.status_code in [200] except Exception as error: logger.error(str(error)) value = False return value def _is_valid_api_url(self, url): """ """ # Check response is a JSON with ok: 1 data = {} try: r = requests.get(url) content = to_text_string(r.content, encoding='utf-8') data = json.loads(content) except Exception as error: logger.error(str(error)) return data.get('ok', 0) == 1 def download(self, url, path=None, force=False): logger.debug(str((url, path, force))) method = self._download return self._create_worker(method, url, path=path, force=force) def terminate(self): for t in self._threads: t.quit() self._thread = [] self._workers = [] def is_valid_url(self, url, non_blocking=True): logger.debug(str((url))) if non_blocking: method = self._is_valid_url return self._create_worker(method, url) else: return self._is_valid_url(url) def is_valid_api_url(self, url, non_blocking=True): logger.debug(str((url))) if non_blocking: method = self._is_valid_api_url return self._create_worker(method, url) else: return self._is_valid_api_url(url=url) def is_valid_channel(self, channel, conda_url='https://conda.anaconda.org', non_blocking=True): logger.debug(str((channel, conda_url))) if non_blocking: method = self._is_valid_channel return self._create_worker(method, channel, conda_url) else: return self._is_valid_channel(channel, conda_url=conda_url)
class Task(QObject): """ A way to run tasks separate from the main UI thread. Not the cleanest abstraction, but works for now at least. """ started = Signal() finished = Signal() failed = Signal(Exception) progress = Signal(Report) _progressThrottled = Signal(Report) result = Signal(object) def __init__(self, *args, **kwargs): super().__init__() self._args = args self._kwargs = kwargs self._lastReport: Optional[Report] = None self._timer = QTimer() self._timer.setInterval(30) def onTimeout(): """ progress: A...B...C...D...E...F...G...H...I...J lastrepo: AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJ throttle: A.........C.........F.........H.........J """ if self._lastReport is not None: self._progressThrottled.emit(self._lastReport) self._lastReport = None self._timer.timeout.connect(onTimeout) self.started.connect(self._timer.start) self.finished.connect(self._timer.stop) self.failed.connect(self._timer.stop) def setReport(report: Report) -> None: self._lastReport = report self.progress.connect(lambda r: setReport(r)) def stop(self) -> None: """ Note that this won't actually have an effect if you don't check this condition in `task()`. """ self._stopped = True def execute(self) -> None: self.started.emit() self._stopped = False try: result = self.run(*self._args, **self._kwargs) except Exception as e: self.failed.emit(e) else: self.result.emit(result) self.finished.emit() def run(self, *args, **kwargs) -> Any: """ Subclasses of Worker must allow for checking the status of self._stopped so that the worker can be stopped in a responsive way. """ raise NotImplementedError
class _DownloadAPI(QObject): """ Download API based on QNetworkAccessManager """ def __init__(self, chunk_size=1024): super(_DownloadAPI, self).__init__() self._chunk_size = chunk_size self._head_requests = {} self._get_requests = {} self._paths = {} self._workers = {} self._manager = QNetworkAccessManager(self) self._timer = QTimer() # Setup self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) # Signals self._manager.finished.connect(self._request_finished) self._manager.sslErrors.connect(self._handle_ssl_errors) def _handle_ssl_errors(self, reply, errors): logger.error(str(('SSL Errors', errors))) def _clean(self): """ Periodically check for inactive workers and remove their references. """ if self._workers: for url in self._workers.copy(): w = self._workers[url] if w.is_finished(): self._workers.pop(url) self._paths.pop(url) if url in self._get_requests: self._get_requests.pop(url) else: self._timer.stop() def _request_finished(self, reply): url = to_text_string(reply.url().toEncoded(), encoding='utf-8') if url in self._paths: path = self._paths[url] if url in self._workers: worker = self._workers[url] if url in self._head_requests: self._head_requests.pop(url) start_download = True header_pairs = reply.rawHeaderPairs() headers = {} for hp in header_pairs: headers[to_text_string(hp[0]).lower()] = to_text_string(hp[1]) total_size = int(headers.get('content-length', 0)) # Check if file exists if os.path.isfile(path): file_size = os.path.getsize(path) # Check if existing file matches size of requested file start_download = file_size != total_size if start_download: # File sizes dont match, hence download file qurl = QUrl(url) request = QNetworkRequest(qurl) self._get_requests[url] = request reply = self._manager.get(request) error = reply.error() if error: logger.error(str(('Reply Error:', error))) reply.downloadProgress.connect( lambda r, t, w=worker: self._progress(r, t, w)) else: # File sizes match, dont download file worker.finished = True worker.sig_download_finished.emit(url, path) worker.sig_finished.emit(worker, path, None) elif url in self._get_requests: data = reply.readAll() self._save(url, path, data) def _save(self, url, path, data): """ """ worker = self._workers[url] path = self._paths[url] if len(data): with open(path, 'wb') as f: f.write(data) # Clean up worker.finished = True worker.sig_download_finished.emit(url, path) worker.sig_finished.emit(worker, path, None) self._get_requests.pop(url) self._workers.pop(url) self._paths.pop(url) def _progress(self, bytes_received, bytes_total, worker): """ """ worker.sig_download_progress.emit( worker.url, worker.path, bytes_received, bytes_total) def download(self, url, path): """ """ # original_url = url qurl = QUrl(url) url = to_text_string(qurl.toEncoded(), encoding='utf-8') logger.debug(str((url, path))) if url in self._workers: while not self._workers[url].finished: return self._workers[url] worker = DownloadWorker(url, path) # Check download folder exists folder = os.path.dirname(os.path.abspath(path)) if not os.path.isdir(folder): os.makedirs(folder) request = QNetworkRequest(qurl) self._head_requests[url] = request self._paths[url] = path self._workers[url] = worker self._manager.head(request) self._timer.start() return worker def terminate(self): pass
class FindReplace(QWidget): """Find widget""" STYLE = {False: "background-color:rgb(255, 175, 90);", True: ""} visibility_changed = Signal(bool) def __init__(self, parent, enable_replace=False): QWidget.__init__(self, parent) self.enable_replace = enable_replace self.editor = None self.is_code_editor = None glayout = QGridLayout() glayout.setContentsMargins(0, 0, 0, 0) self.setLayout(glayout) self.close_button = create_toolbutton(self, triggered=self.hide, icon=ima.icon('DialogCloseButton')) glayout.addWidget(self.close_button, 0, 0) # Find layout self.search_text = PatternComboBox(self, tip=_("Search string"), adjust_to_minimum=False) self.search_text.valid.connect( lambda state: self.find(changed=False, forward=True, rehighlight=False)) self.search_text.lineEdit().textEdited.connect( self.text_has_been_edited) self.previous_button = create_toolbutton(self, triggered=self.find_previous, icon=ima.icon('ArrowUp')) self.next_button = create_toolbutton(self, triggered=self.find_next, icon=ima.icon('ArrowDown')) self.next_button.clicked.connect(self.update_search_combo) self.previous_button.clicked.connect(self.update_search_combo) self.re_button = create_toolbutton(self, icon=ima.icon('advanced'), tip=_("Regular expression")) self.re_button.setCheckable(True) self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton(self, icon=get_icon("upper_lower.png"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) self.case_button.toggled.connect(lambda state: self.find()) self.words_button = create_toolbutton(self, icon=get_icon("whole_words.png"), tip=_("Whole words")) self.words_button.setCheckable(True) self.words_button.toggled.connect(lambda state: self.find()) self.highlight_button = create_toolbutton(self, icon=get_icon("highlight.png"), tip=_("Highlight matches")) self.highlight_button.setCheckable(True) self.highlight_button.toggled.connect(self.toggle_highlighting) hlayout = QHBoxLayout() self.widgets = [self.close_button, self.search_text, self.previous_button, self.next_button, self.re_button, self.case_button, self.words_button, self.highlight_button] for widget in self.widgets[1:]: hlayout.addWidget(widget) glayout.addLayout(hlayout, 0, 1) # Replace layout replace_with = QLabel(_("Replace with:")) self.replace_text = PatternComboBox(self, adjust_to_minimum=False, tip=_('Replace string')) self.replace_button = create_toolbutton(self, text=_('Replace/find'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find, text_beside_icon=True) self.replace_button.clicked.connect(self.update_replace_combo) self.replace_button.clicked.connect(self.update_search_combo) self.all_check = QCheckBox(_("Replace all")) self.replace_layout = QHBoxLayout() widgets = [replace_with, self.replace_text, self.replace_button, self.all_check] for widget in widgets: self.replace_layout.addWidget(widget) glayout.addLayout(self.replace_layout, 1, 1) self.widgets.extend(widgets) self.replace_widgets = widgets self.hide_replace() self.search_text.setTabOrder(self.search_text, self.replace_text) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.shortcuts = self.create_shortcuts(parent) self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(1000) self.highlight_timer.timeout.connect(self.highlight_matches) def create_shortcuts(self, parent): """Create shortcuts for this widget""" # Configurable findnext = config_shortcut(self.find_next, context='_', name='Find next', parent=parent) findprev = config_shortcut(self.find_previous, context='_', name='Find previous', parent=parent) togglefind = config_shortcut(self.show, context='_', name='Find text', parent=parent) togglereplace = config_shortcut(self.toggle_replace_widgets, context='_', name='Replace text', parent=parent) # Fixed fixed_shortcut("Escape", self, self.hide) return [findnext, findprev, togglefind, togglereplace] def get_shortcut_data(self): """ Returns shortcut data, a list of tuples (shortcut, text, default) shortcut (QShortcut or QAction instance) text (string): action/shortcut description default (string): default key sequence """ return [sc.data for sc in self.shortcuts] def update_search_combo(self): self.search_text.lineEdit().returnPressed.emit() def update_replace_combo(self): self.replace_text.lineEdit().returnPressed.emit() def toggle_replace_widgets(self): if self.enable_replace: # Toggle replace widgets if self.replace_widgets[0].isVisible(): self.hide_replace() self.hide() else: self.show_replace() self.replace_text.setFocus() @Slot(bool) def toggle_highlighting(self, state): """Toggle the 'highlight all results' feature""" if self.editor is not None: if state: self.highlight_matches() else: self.clear_matches() def show(self): """Overrides Qt Method""" QWidget.show(self) self.visibility_changed.emit(True) if self.editor is not None: text = self.editor.get_selected_text() # If no text is highlighted for search, use whatever word is under the cursor if not text: cursor = self.editor.textCursor() cursor.select(QTextCursor.WordUnderCursor) text = to_text_string(cursor.selectedText()) # Now that text value is sorted out, use it for the search if text: self.search_text.setEditText(text) self.search_text.lineEdit().selectAll() self.refresh() else: self.search_text.lineEdit().selectAll() self.search_text.setFocus() @Slot() def hide(self): """Overrides Qt Method""" for widget in self.replace_widgets: widget.hide() QWidget.hide(self) self.visibility_changed.emit(False) if self.editor is not None: self.editor.setFocus() self.clear_matches() def show_replace(self): """Show replace widgets""" self.show() for widget in self.replace_widgets: widget.show() def hide_replace(self): """Hide replace widgets""" for widget in self.replace_widgets: widget.hide() def refresh(self): """Refresh widget""" if self.isHidden(): if self.editor is not None: self.clear_matches() return state = self.editor is not None for widget in self.widgets: widget.setEnabled(state) if state: self.find() def set_editor(self, editor, refresh=True): """ Set associated editor/web page: codeeditor.base.TextEditBaseWidget browser.WebView """ self.editor = editor # Note: This is necessary to test widgets/editor.py # in Qt builds that don't have web widgets try: from qtpy.QtWebEngineWidgets import QWebEngineView except ImportError: QWebEngineView = type(None) self.words_button.setVisible(not isinstance(editor, QWebEngineView)) self.re_button.setVisible(not isinstance(editor, QWebEngineView)) from spyderlib.widgets.sourcecode.codeeditor import CodeEditor self.is_code_editor = isinstance(editor, CodeEditor) self.highlight_button.setVisible(self.is_code_editor) if refresh: self.refresh() if self.isHidden() and editor is not None: self.clear_matches() @Slot() def find_next(self): """Find next occurrence""" state = self.find(changed=False, forward=True, rehighlight=False) self.editor.setFocus() self.search_text.add_current_text() return state @Slot() def find_previous(self): """Find previous occurrence""" state = self.find(changed=False, forward=False, rehighlight=False) self.editor.setFocus() return state def text_has_been_edited(self, text): """Find text has been edited (this slot won't be triggered when setting the search pattern combo box text programmatically""" self.find(changed=True, forward=True, start_highlight_timer=True) def highlight_matches(self): """Highlight found results""" if self.is_code_editor and self.highlight_button.isChecked(): text = self.search_text.currentText() words = self.words_button.isChecked() regexp = self.re_button.isChecked() self.editor.highlight_found_results(text, words=words, regexp=regexp) def clear_matches(self): """Clear all highlighted matches""" if self.is_code_editor: self.editor.clear_found_results() def find(self, changed=True, forward=True, rehighlight=True, start_highlight_timer=False): """Call the find function""" text = self.search_text.currentText() if len(text) == 0: self.search_text.lineEdit().setStyleSheet("") return None else: case = self.case_button.isChecked() words = self.words_button.isChecked() regexp = self.re_button.isChecked() found = self.editor.find_text(text, changed, forward, case=case, words=words, regexp=regexp) self.search_text.lineEdit().setStyleSheet(self.STYLE[found]) if self.is_code_editor and found: if rehighlight or not self.editor.found_results: self.highlight_timer.stop() if start_highlight_timer: self.highlight_timer.start() else: self.highlight_matches() else: self.clear_matches() return found @Slot() def replace_find(self): """Replace and find""" if (self.editor is not None): replace_text = to_text_string(self.replace_text.currentText()) search_text = to_text_string(self.search_text.currentText()) pattern = search_text if self.re_button.isChecked() else None case = self.case_button.isChecked() first = True cursor = None while True: if first: # First found seltxt = to_text_string(self.editor.get_selected_text()) cmptxt1 = search_text if case else search_text.lower() cmptxt2 = seltxt if case else seltxt.lower() if self.editor.has_selected_text() and cmptxt1 == cmptxt2: # Text was already found, do nothing pass else: if not self.find(changed=False, forward=True, rehighlight=False): break first = False wrapped = False position = self.editor.get_position('cursor') position0 = position cursor = self.editor.textCursor() cursor.beginEditBlock() else: position1 = self.editor.get_position('cursor') if is_position_inf(position1, position0 + len(replace_text) - len(search_text) + 1): # Identify wrapping even when the replace string # includes part of the search string wrapped = True if wrapped: if position1 == position or \ is_position_sup(position1, position): # Avoid infinite loop: replace string includes # part of the search string break if position1 == position0: # Avoid infinite loop: single found occurrence break position0 = position1 if pattern is None: cursor.removeSelectedText() cursor.insertText(replace_text) else: seltxt = to_text_string(cursor.selectedText()) cursor.removeSelectedText() cursor.insertText(re.sub(pattern, replace_text, seltxt)) if self.find_next(): found_cursor = self.editor.textCursor() cursor.setPosition(found_cursor.selectionStart(), QTextCursor.MoveAnchor) cursor.setPosition(found_cursor.selectionEnd(), QTextCursor.KeepAnchor) else: break if not self.all_check.isChecked(): break self.all_check.setCheckState(Qt.Unchecked) if cursor is not None: cursor.endEditBlock()
class ConnectionTableModel(QAbstractTableModel): def __init__(self, connections=[], parent=None): super(ConnectionTableModel, self).__init__(parent=parent) self._column_names = ("protocol", "address", "connected") self.update_timer = QTimer(self) self.update_timer.setInterval(1000) self.update_timer.timeout.connect(self.update_values) self.connections = connections def sort(self, col, order=Qt.AscendingOrder): if self._column_names[col] == "value": return self.layoutAboutToBeChanged.emit() sort_reversed = (order == Qt.AscendingOrder) self._connections.sort(key=attrgetter(self._column_names[col]), reverse=sort_reversed) self.layoutChanged.emit() @property def connections(self): return self._connections @connections.setter def connections(self, new_connections): self.beginResetModel() self._connections = new_connections self.endResetModel() if len(self._connections) > 0: self.update_timer.start() else: self.update_timer.stop() # QAbstractItemModel Implementation def flags(self, index): return Qt.ItemIsSelectable | Qt.ItemIsEnabled def rowCount(self, parent=None): if parent is not None and parent.isValid(): return 0 return len(self._connections) def columnCount(self, parent=None): return len(self._column_names) def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() if index.row() >= self.rowCount(): return QVariant() if index.column() >= self.columnCount(): return QVariant() column_name = self._column_names[index.column()] conn = self.connections[index.row()] if role == Qt.DisplayRole or role == Qt.EditRole: return str(getattr(conn, column_name)) else: return QVariant() def headerData(self, section, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole: return super(ConnectionTableModel, self).headerData( section, orientation, role) if orientation == Qt.Horizontal and section < self.columnCount(): return str(self._column_names[section]).capitalize() elif orientation == Qt.Vertical and section < self.rowCount(): return section # End QAbstractItemModel implementation. @Slot() def update_values(self): self.dataChanged.emit(self.index(0,2), self.index(self.rowCount(),2))
class TextDecorationsManager(Manager, QObject): """ Manages the collection of TextDecoration that have been set on the editor widget. """ def __init__(self, editor): super(TextDecorationsManager, self).__init__(editor) QObject.__init__(self, None) self._decorations = [] # Timer to not constantly update decorations. self.update_timer = QTimer(self) self.update_timer.setSingleShot(True) self.update_timer.setInterval(UPDATE_TIMEOUT) self.update_timer.timeout.connect(self._update) def add(self, decorations): """ Add text decorations on a CodeEditor instance. Don't add duplicated decorations, and order decorations according draw_order and the size of the selection. Args: decorations (sourcecode.api.TextDecoration) (could be a list) Returns: int: Amount of decorations added. """ added = 0 if isinstance(decorations, list): not_repeated = set(decorations) - set(self._decorations) self._decorations.extend(list(not_repeated)) added = len(not_repeated) elif decorations not in self._decorations: self._decorations.append(decorations) added = 1 if added > 0: self._order_decorations() self.update() return added def remove(self, decoration): """ Removes a text decoration from the editor. :param decoration: Text decoration to remove :type decoration: spyder.api.TextDecoration update: Bool: should the decorations be updated immediately? Set to False to avoid updating several times while removing several decorations """ try: self._decorations.remove(decoration) self.update() return True except ValueError: return False def clear(self): """Removes all text decoration from the editor.""" self._decorations[:] = [] self.update() def update(self): """ Update decorations. This starts a timer to update decorations only after UPDATE_TIMEOUT has passed. That avoids multiple calls to _update in a very short amount of time. """ self.update_timer.start() @Slot() def _update(self): """Update editor extra selections with added decorations. NOTE: Update TextDecorations to use editor font, using a different font family and point size could cause unwanted behaviors. """ try: font = self.editor.font() # Get the current visible block numbers first, last = self.editor.get_buffer_block_numbers() # Update visible decorations visible_decorations = [] for decoration in self._decorations: need_update_sel = False cursor = decoration.cursor sel_start = cursor.selectionStart() # This is required to update extra selections from the point # an initial selection was made. # Fixes spyder-ide/spyder#14282 if sel_start is not None: doc = cursor.document() block_nb_start = doc.findBlock(sel_start).blockNumber() need_update_sel = first <= block_nb_start <= last block_nb = decoration.cursor.block().blockNumber() if (first <= block_nb <= last or need_update_sel or decoration.kind == 'current_cell'): visible_decorations.append(decoration) try: decoration.format.setFont( font, QTextCharFormat.FontPropertiesSpecifiedOnly) except (TypeError, AttributeError): # Qt < 5.3 decoration.format.setFontFamily(font.family()) decoration.format.setFontPointSize(font.pointSize()) self.editor.setExtraSelections(visible_decorations) except RuntimeError: # This is needed to fix spyder-ide/spyder#9173. return def __iter__(self): return iter(self._decorations) def __len__(self): return len(self._decorations) def _order_decorations(self): """Order decorations according draw_order and size of selection. Highest draw_order will appear on top of the lowest values. If draw_order is equal,smaller selections are draw in top of bigger selections. """ def order_function(sel): end = sel.cursor.selectionEnd() start = sel.cursor.selectionStart() return sel.draw_order, -(end - start) self._decorations = sorted(self._decorations, key=order_function)
class GUI(QWidget): '''GUI for choosing course and monitoring scans of NFC ID cards. Attributes (other than Qt objects): roster: A Roster object. buzzer: A Buzzer object. last_student_id, blocked, wait: Used to avoid repeated GUI refreshes when an ID card is placed for prolonged period. ''' def __init__(self, parent=None): super(GUI, self).__init__(parent) self.resize(600, 500) self.setWindowTitle('KIT-Card Reader') self.setFont(QFont('Helvetica', 24)) # 名簿をロード self.roster = roster.Roster() # 空の縦レイアウトを作る layout = QVBoxLayout() self.setLayout(layout) # コンボボックス cbox_labels = [ f'{course_code} {course_name}' for course_code, course_name in self.roster.courses.items() ] self.cb1 = QComboBox(self) self.cb1.addItems(cbox_labels) layout.addWidget(self.cb1) # ラベル self.state = 'IDLE' self.l1 = QLabel(self.state) self.l1.setAlignment(Qt.AlignCenter) self.l1.setStyleSheet('color: black; font-size: 64pt') layout.addWidget(self.l1) # ボタン self.b1 = QPushButton('受付開始', self) self.b1.setStyleSheet('background-color: darkblue;' 'color: white; font-size: 32pt') self.b1.clicked.connect(self.b1_callback) layout.addWidget(self.b1) # ブザー self.buzzer = Buzzer() # タイマー(100ミリ秒ごとのインタバルタイマ) self.timer = QTimer() self.timer.setSingleShot(False) self.timer.setInterval(100) self.timer.timeout.connect(self.on_timer) self.wait = DELAY self.blocked = False self.last_student_id = None self.timer.start() def b1_callback(self): '''Starts taking attendance.''' if self.state != 'IDLE': self.roster.report_absent_students() exit() self.state = 'RUNNING' idx = self.cb1.currentIndex() # 現在のコンボボックスの選択番号 self.cb1.setStyleSheet('background-color: gray;' 'color: white; font-size: 24pt') self.cb1.setEnabled(False) course_code = list(self.roster.courses)[idx] self.roster.set_course_code(course_code) # ボタンラベル変更 self.update_button_label() self.b1.setStyleSheet('background-color: maroon;' 'color: white; font-size: 32pt') # NFCカードリーダスレッド開始 reader = threading.Thread(target=self.nfc_thread) reader.daemon = True reader.start() def l1_change(self, text): self.l1.setText(text) def update_button_label(self): num_students = len(self.roster.students) num_present = len(self.roster.present) self.b1.setText('受付終了 (%d/%d)' % (num_present, num_students)) def check_in(self, student_id): '''Checks in a student.''' if self.last_student_id == student_id: return self.last_student_id = student_id ok, msg = self.roster.check_in(student_id) if ok: timestamp = datetime.datetime.now().strftime('%H:%M:%S') self.l1_change(f'{timestamp}\n{msg}') self.update_button_label() self.buzzer.ring(SUCCESS) else: self.l1_change(msg) self.buzzer.ring(FAILURE) def nfc_thread(self): def nfc_detected(tag): self.check_in(get_student_id(tag)) time.sleep(1) while True: while not self.blocked: time.sleep(0.05) try: with nfc.ContactlessFrontend('usb') as clf: clf.connect(rdwr={'on-connect': nfc_detected}) except Exception as e: logging.error(str(e)) self.buzzer.ring(FAILURE) self.blocked = False def on_timer(self): if not self.blocked: self.blocked = True self.wait = DELAY elif self.wait > 0: self.wait -= 1 if self.wait == 0: self.l1_change('READY') # カードが見えないときはREADYに強制 self.last_student_id = None
class AnimateDialog(QDialog): def __init__(self, vpoints: Sequence[VPoint], vlinks: Sequence[VLink], path: _Paths, slider_path: _SliderPaths, monochrome: bool, parent: QWidget): super(AnimateDialog, self).__init__(parent) self.setWindowTitle("Vector Animation") self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint & ~Qt.WindowContextHelpButtonHint) self.setMinimumSize(800, 600) self.setModal(True) main_layout = QVBoxLayout(self) self.canvas = _DynamicCanvas(vpoints, vlinks, path, slider_path, self) self.canvas.set_monochrome_mode(monochrome) self.canvas.update_pos.connect(self.__set_pos) layout = QHBoxLayout(self) pt_option = QComboBox(self) pt_option.addItems([f"P{p}" for p in range(len(vpoints))]) layout.addWidget(pt_option) value_label = QLabel(self) @Slot(int) def show_values(ind: int): vel, vel_deg = self.canvas.get_vel(ind) acc, acc_deg = self.canvas.get_acc(ind) value_label.setText( f"Velocity: {vel:.04f} ({vel_deg:.04f}deg) | " f"Acceleration: {acc:.04f} ({acc_deg:.04f}deg)") pt_option.currentIndexChanged.connect(show_values) layout.addWidget(value_label) self.pos_label = QLabel(self) layout.addItem( QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) layout.addWidget(self.pos_label) main_layout.addLayout(layout) main_layout.addWidget(self.canvas) layout = QHBoxLayout(self) self.play = QPushButton(QIcon(QPixmap("icons:play.png")), "", self) self.play.setCheckable(True) self.play.clicked.connect(self.__play) layout.addWidget(self.play) self.slider = QSlider(Qt.Horizontal, self) self.slider.setMaximum(max(len(p) for p in path) - 1) self.slider.valueChanged.connect(self.canvas.set_index) layout.addWidget(self.slider) layout.addWidget(QLabel("Total times:", self)) factor = QDoubleSpinBox(self) factor.valueChanged.connect(self.canvas.set_factor) factor.setSuffix('s') factor.setRange(0.01, 999999) factor.setValue(10) layout.addWidget(factor) main_layout.addLayout(layout) self.timer = QTimer() self.timer.setInterval(10) self.timer.timeout.connect(self.__move_ind) @Slot() def __move_ind(self): """Move indicator.""" value = self.slider.value() + 1 self.slider.setValue(value) if value > self.slider.maximum(): self.slider.setValue(0) @Slot(float, float) def __set_pos(self, x: float, y: float) -> None: """Set mouse position.""" self.pos_label.setText(f"({x:.04f}, {y:.04f})") @Slot() def __play(self): """Start playing.""" if self.play.isChecked(): self.timer.start() else: self.timer.stop()
class ScriptRunner(object): """ Runs a script that interacts with a widget (tests it). If the script is a python generator then after each iteration controls returns to the QApplication's event loop. Generator scripts can yield a positive number. It is treated as the number of seconds before the next iteration is called. During the wait time the event loop is running. """ def __init__(self, script, widget=None, close_on_finish=True, pause=0, is_cli=False): """ Initialise a runner. :param script: The script to run. :param widget: The widget to test. :param close_on_finish: If true close the widget after the script has finished. :param is_cli: If true the script is to be run from a command line tool. Exceptions are treated slightly differently in this case. """ app = get_application() self.script = script self.widget = widget self.close_on_finish = close_on_finish self.pause = pause self.is_cli = is_cli self.error = None self.script_iter = [None] self.pause_timer = QTimer(app) self.pause_timer.setSingleShot(True) self.script_timer = QTimer(app) def run(self): ret = run_script(self.script, self.widget) if isinstance(ret, Exception): raise ret self.script_iter = [iter(ret) if inspect.isgenerator(ret) else None] if self.pause != 0: self.script_timer.setInterval(self.pause * 1000) # Zero-timeout timer runs script_runner() between Qt events self.script_timer.timeout.connect(self, Qt.QueuedConnection) QMetaObject.invokeMethod(self.script_timer, 'start', Qt.QueuedConnection) def __call__(self): app = get_application() if not self.pause_timer.isActive(): try: script_iter = self.script_iter[-1] if script_iter is None: if self.close_on_finish: app.closeAllWindows() app.exit() return # Run test script until the next 'yield' try: ret = next(script_iter) except ValueError: return while ret is not None: if inspect.isgenerator(ret): self.script_iter.append(ret) ret = None elif isinstance(ret, six.integer_types) or isinstance(ret, float): # Start non-blocking pause in seconds self.pause_timer.start(int(ret * 1000)) ret = None else: ret = ret() except StopIteration: if len(self.script_iter) > 1: self.script_iter.pop() else: self.script_iter = [None] self.script_timer.stop() if self.close_on_finish: app.closeAllWindows() app.exit(0) except Exception as e: self.script_iter = [None] traceback.print_exc() if self.close_on_finish: app.exit(1) self.error = e
class ProcessWorker(QObject): """ """ sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs={}): super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs self._timer = QTimer() self._process = QProcess() self._timer.setInterval(50) self._timer.timeout.connect(self._communicate) self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """ """ if not self._communicate_first: if self._process.state() == QProcess.NotRunning: self.communicate() elif self._fired: self._timer.stop() def communicate(self): """ """ self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda cloud api site' not in stderr.lower(): if stderr.strip() and self._conda: raise Exception('{0}:\n' 'STDERR:\n{1}\nEND' ''.format(' '.join(self._cmd_list), stderr)) # elif stderr.strip() and self._pip: # raise PipError(self._cmd_list) else: result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except ValueError as error: result = stdout, error if 'error' in result[0]: error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """ """ self._process.close() def is_finished(self): """ """ return self._process.state() == QProcess.NotRunning and self._fired def start(self): """ """ logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class DesignerHooks(object): """ Class that handles the integration with PyDM and the Qt Designer by hooking up slots to signals provided by FormEditor and other classes. """ __instance = None def __init__(self): if self.__initialized: return self.__form_editor = None self.__initialized = True self.__timer = None def __new__(cls, *args, **kwargs): if cls.__instance is None: cls.__instance = object.__new__(DesignerHooks) cls.__instance.__initialized = False return cls.__instance @property def form_editor(self): return self.__form_editor @form_editor.setter def form_editor(self, editor): if self.form_editor is not None: return if not editor: return self.__form_editor = editor self.setup_hooks() def setup_hooks(self): sys.excepthook = self.__handle_exceptions # Set PyDM to be read-only data_plugins.set_read_only(True) if self.form_editor: fwman = self.form_editor.formWindowManager() if fwman: fwman.formWindowAdded.connect(self.__new_form_added) def __new_form_added(self, form_window_interface): style_data = stylesheet._get_style_data(None) widget = form_window_interface.formContainer() widget.setStyleSheet(style_data) if not self.__timer: self.__start_kicker() def __kick(self): fwman = self.form_editor.formWindowManager() if fwman: widget = fwman.activeFormWindow() if widget: widget.update() def __handle_exceptions(self, exc_type, value, trace): print("Exception occurred while running Qt Designer.") msg = ''.join(traceback.format_exception(exc_type, value, trace)) print(msg) def __start_kicker(self): self.__timer = QTimer() self.__timer.setInterval(100) self.__timer.timeout.connect(self.__kick) self.__timer.start()
class QtLayerList(QScrollArea): """Widget storing a list of all the layers present in the current window. Parameters ---------- layers : napari.components.LayerList The layer list to track and display. Attributes ---------- centers : list List of layer widgets center coordinates. layers : napari.components.LayerList The layer list to track and display. vbox_layout : QVBoxLayout The layout instance in which the layouts appear. """ def __init__(self, layers): super().__init__() self.layers = layers self.setAttribute(Qt.WA_DeleteOnClose) self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollWidget = QWidget() self.setWidget(scrollWidget) self.vbox_layout = QVBoxLayout(scrollWidget) self.vbox_layout.addWidget(QtDivider()) self.vbox_layout.addStretch(1) self.vbox_layout.setContentsMargins(0, 0, 0, 0) self.vbox_layout.setSpacing(2) self.centers = [] # Create a timer to be used for autoscrolling the layers list up and # down when dragging a layer near the end of the displayed area self._drag_timer = QTimer() self._drag_timer.setSingleShot(False) self._drag_timer.setInterval(20) self._drag_timer.timeout.connect(self._force_scroll) self._scroll_up = True self._min_scroll_region = 24 self.setAcceptDrops(True) self.setToolTip(trans._('Layer list')) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.layers.events.inserted.connect(self._add) self.layers.events.removed.connect(self._remove) self.layers.events.reordered.connect(self._reorder) self._drag_start_position = np.zeros(2) self._drag_name = None self.chunk_receiver = _create_chunk_receiver(self) def _add(self, event): """Insert widget for layer `event.value` at index `event.index`. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ layer = event.value total = len(self.layers) index = 2 * (total - event.index) - 1 widget = QtLayerWidget(layer) self.vbox_layout.insertWidget(index, widget) self.vbox_layout.insertWidget(index + 1, QtDivider()) layer.events.select.connect(self._scroll_on_select) def _remove(self, event): """Remove widget for layer at index `event.index`. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ layer_index = event.index total = len(self.layers) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) + 1 widget = self.vbox_layout.itemAt(index).widget() divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(widget) disconnect_events(widget.layer.events, self) widget.close() self.vbox_layout.removeWidget(divider) divider.deleteLater() def _reorder(self, event=None): """Reorder list of layer widgets. Loops through all widgets in list, sequentially removing them and inserting them into the correct place in the final list. Parameters ---------- event : napari.utils.event.Event, optional The napari event that triggered this method. """ total = len(self.layers) # Create list of the current property and divider widgets widgets = [ self.vbox_layout.itemAt(i + 1).widget() for i in range(2 * total) ] # Take every other widget to ignore the dividers and get just the # property widgets indices = [ self.layers.index(w.layer) for i, w in enumerate(widgets) if i % 2 == 0 ] # Move through the layers in order for i in range(total): # Find index of property widget in list of the current layer index = 2 * indices.index(i) widget = widgets[index] divider = widgets[index + 1] # Check if current index does not match new index index_current = self.vbox_layout.indexOf(widget) index_new = 2 * (total - i) - 1 if index_current != index_new: # Remove that property widget and divider self.vbox_layout.removeWidget(widget) self.vbox_layout.removeWidget(divider) # Insert the property widget and divider into new location self.vbox_layout.insertWidget(index_new, widget) self.vbox_layout.insertWidget(index_new + 1, divider) def _force_scroll(self): """Force the scroll bar to automattically scroll either up or down.""" cur_value = self.verticalScrollBar().value() if self._scroll_up: new_value = cur_value - self.verticalScrollBar().singleStep() / 4 if new_value < 0: new_value = 0 self.verticalScrollBar().setValue(new_value) else: new_value = cur_value + self.verticalScrollBar().singleStep() / 4 if new_value > self.verticalScrollBar().maximum(): new_value = self.verticalScrollBar().maximum() self.verticalScrollBar().setValue(new_value) def _scroll_on_select(self, event): """Scroll to ensure that the currently selected layer is visible. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ layer = event.source self._ensure_visible(layer) def _ensure_visible(self, layer): """Ensure layer widget for at particular layer is visible. Parameters ---------- layer : napari.layers.Layer An instance of a napari layer. """ total = len(self.layers) layer_index = self.layers.index(layer) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) - 1 widget = self.vbox_layout.itemAt(index).widget() self.ensureWidgetVisible(widget) def keyPressEvent(self, event): """Ignore a key press event. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() def keyReleaseEvent(self, event): """Ignore key release event. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() def mousePressEvent(self, event): """Register mouse click if it happens on a layer widget. Checks if mouse press happens on a layer properties widget or a child of such a widget. If not, the press has happened on the Layers Widget itself and should be ignored. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ widget = self.childAt(event.pos()) layer = (getattr(widget, 'layer', None) or getattr(widget.parentWidget(), 'layer', None) or getattr( widget.parentWidget().parentWidget(), 'layer', None)) if layer is not None: self._drag_start_position = np.array( [event.pos().x(), event.pos().y()]) self._drag_name = layer.name else: self._drag_name = None def mouseReleaseEvent(self, event): """Select layer using mouse click. Key modifiers: Shift - If the Shift button is pressed, select all layers in between currently selected one and the clicked one. Control - If the Control button is pressed, mouse click will toggle selected state of the layer. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if self._drag_name is None: # Unselect all the layers if not dragging a layer self.layers.unselect_all() return modifiers = event.modifiers() layer = self.layers[self._drag_name] if modifiers == Qt.ShiftModifier: # If shift select all layers in between currently selected one and # clicked one index = self.layers.index(layer) lastSelected = None for i in range(len(self.layers)): if self.layers[i].selected: lastSelected = i r = [index, lastSelected] r.sort() for i in range(r[0], r[1] + 1): self.layers[i].selected = True elif modifiers == Qt.ControlModifier: # If control click toggle selected state layer.selected = not layer.selected else: # If otherwise unselect all and leave clicked one selected self.layers.unselect_all(ignore=layer) layer.selected = True def mouseMoveEvent(self, event): """Drag and drop layer with mouse movement. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ position = np.array([event.pos().x(), event.pos().y()]) distance = np.linalg.norm(position - self._drag_start_position) if (distance < QApplication.startDragDistance() or self._drag_name is None): return mimeData = QMimeData() mimeData.setText(self._drag_name) drag = QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) drag.exec_() # Check if dragged layer still exists or was deleted during drag names = [layer.name for layer in self.layers] dragged_layer_exists = self._drag_name in names if self._drag_name is not None and dragged_layer_exists: index = self.layers.index(self._drag_name) layer = self.layers[index] self._ensure_visible(layer) def dragLeaveEvent(self, event): """Unselects layer dividers. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() self._drag_timer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) def dragEnterEvent(self, event): """Update divider position before dragging layer widget to new position Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.source() == self: event.accept() divs = [] for i in range(0, self.vbox_layout.count(), 2): widget = self.vbox_layout.itemAt(i).widget() divs.append(widget.y() + widget.frameGeometry().height() / 2) self.centers = [(divs[i + 1] + divs[i]) / 2 for i in range(len(divs) - 1)] else: event.ignore() def dragMoveEvent(self, event): """Highlight appriate divider when dragging layer to new position. Sets the appropriate layers list divider to be highlighted when dragging a layer to a new position in the layers list. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ max_height = self.frameGeometry().height() if (event.pos().y() < self._min_scroll_region and not self._drag_timer.isActive()): self._scroll_up = True self._drag_timer.start() elif (event.pos().y() > max_height - self._min_scroll_region and not self._drag_timer.isActive()): self._scroll_up = False self._drag_timer.start() elif (self._drag_timer.isActive() and event.pos().y() >= self._min_scroll_region and event.pos().y() <= max_height - self._min_scroll_region): self._drag_timer.stop() # Determine which widget center is the mouse currently closed to cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) # Determine the current location of the widget being dragged total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self._drag_name) # If the widget being dragged hasn't moved above or below any other # widgets then don't highlight any dividers selected = not (insert == index) and not (insert - 1 == index) # Set the selected state of all the dividers for i in range(0, self.vbox_layout.count(), 2): if i == 2 * divider_index: self.vbox_layout.itemAt(i).widget().setSelected(selected) else: self.vbox_layout.itemAt(i).widget().setSelected(False) def dropEvent(self, event): """Drop dragged layer widget into new position in the list of layers. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if self._drag_timer.isActive(): self._drag_timer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self._drag_name) if index != insert and index + 1 != insert: if insert >= index: insert -= 1 self.layers.move_selected(index, insert) event.accept()
class EnvironmentsTab(WidgetBase): """ This tab holds the list of named and application environments in the local machine. Available options include, `create`, `clone` and `remove` and package management. """ BLACKLIST = ['anaconda-navigator'] # Do not show in package manager. sig_status_updated = Signal(object, object, object, object) def __init__(self, parent=None): super(EnvironmentsTab, self).__init__(parent) self.api = AnacondaAPI() self.last_env_prefix = None self.last_env_name = None self.previous_environments = None self.tracker = GATracker() self.metadata = {} active_channels = CONF.get('main', 'conda_active_channels', tuple()) channels = CONF.get('main', 'conda_channels', tuple()) conda_url = CONF.get('main', 'conda_url', 'https:/conda.anaconda.org') conda_api_url = CONF.get('main', 'anaconda_api_url', 'https://api.anaconda.org') # Widgets self.button_clone = ButtonEnvironmentPrimary("Clone") self.button_create = ButtonEnvironmentPrimary("Create") self.button_remove = ButtonEnvironmentCancel("Remove") self.frame_environments = FrameEnvironments(self) self.frame_environments_list = FrameEnvironmentsList(self) self.frame_environments_list_buttons = FrameEnvironmentsListButtons(self) self.frame_environments_packages = FrameEnvironmentsPackages(self) self.list_environments = ListWidgetEnvironment() self.packages_widget = CondaPackagesWidget( self, setup=False, active_channels=active_channels, channels=channels, data_directory=CHANNELS_PATH, conda_api_url=conda_api_url, conda_url=conda_url) self.menu_list = QMenu() self.text_search = LineEditSearch() self.timer_environments = QTimer() # Widgets setup self.list_environments.setAttribute(Qt.WA_MacShowFocusRect, False) self.list_environments.setContextMenuPolicy(Qt.CustomContextMenu) self.packages_widget.textbox_search.setAttribute( Qt.WA_MacShowFocusRect, False) self.packages_widget.textbox_search.set_icon_visibility(False) self.text_search.setPlaceholderText("Search Environments") self.text_search.setAttribute(Qt.WA_MacShowFocusRect, False) self.timer_environments.setInterval(5000) # Layouts environments_layout = QVBoxLayout() environments_layout.addWidget(self.text_search) buttons_layout = QHBoxLayout() buttons_layout.addWidget(self.button_create) buttons_layout.addWidget(self.button_clone) buttons_layout.addWidget(self.button_remove) buttons_layout.setContentsMargins(0, 0, 0, 0) list_buttons_layout = QVBoxLayout() list_buttons_layout.addWidget(self.list_environments) list_buttons_layout.addLayout(buttons_layout) self.frame_environments_list_buttons.setLayout(list_buttons_layout) list_buttons_layout.setContentsMargins(0, 0, 0, 0) environments_layout.addWidget(self.frame_environments_list_buttons) self.frame_environments_list.setLayout(environments_layout) packages_layout = QHBoxLayout() packages_layout.addWidget(self.packages_widget) packages_layout.setContentsMargins(0, 0, 0, 0) self.frame_environments_packages.setLayout(packages_layout) main_layout = QHBoxLayout() main_layout.addWidget(self.frame_environments_list, 1) main_layout.addWidget(self.frame_environments_packages, 3) main_layout.setContentsMargins(0, 0, 0, 0) self.frame_environments.setLayout(main_layout) layout = QHBoxLayout() layout.addWidget(self.frame_environments) self.setLayout(layout) # Signals self.button_clone.clicked.connect(self.clone_environment) self.button_create.clicked.connect(self.create_environment) self.button_remove.clicked.connect(self.remove_environment) self.list_environments.sig_item_selected.connect( self.load_environment) self.packages_widget.sig_packages_ready.connect(self.refresh) self.packages_widget.sig_channels_updated.connect(self.update_channels) # self.packages_widget.sig_environment_cloned.connect( # self._environment_created) # self.packages_widget.sig_environment_created.connect( # self._environment_created) # self.packages_widget.sig_environment_removed.connect( # self._environment_removed) self.text_search.textChanged.connect(self.filter_environments) self.timer_environments.timeout.connect(self.refresh_environments) self.packages_widget.sig_process_cancelled.connect( lambda: self.update_visibility(True)) # --- Helpers # ------------------------------------------------------------------------- def update_visibility(self, enabled=True): self.button_create.setDisabled(not enabled) self.button_remove.setDisabled(not enabled) self.button_clone.setDisabled(not enabled) self.list_environments.setDisabled(not enabled) update_pointer() def update_style_sheet(self, style_sheet=None): if style_sheet is None: style_sheet = load_style_sheet() self.setStyleSheet(style_sheet) self.menu_list.setStyleSheet(style_sheet) self.list_environments.setFrameStyle(QFrame.NoFrame) self.list_environments.setFrameShape(QFrame.NoFrame) self.packages_widget.table.setFrameStyle(QFrame.NoFrame) self.packages_widget.table.setFrameShape(QFrame.NoFrame) self.packages_widget.layout().setContentsMargins(0, 0, 0, 0) size = QSize(16, 16) palette = { 'icon.action.not_installed': QIcon(images.CONDA_MANAGER_NOT_INSTALLED).pixmap(size), 'icon.action.installed': QIcon(images.CONDA_MANAGER_INSTALLED).pixmap(size), 'icon.action.remove': QIcon(images.CONDA_MANAGER_REMOVE).pixmap(size), 'icon.action.add': QIcon(images.CONDA_MANAGER_ADD).pixmap(size), 'icon.action.upgrade': QIcon(images.CONDA_MANAGER_UPGRADE).pixmap(size), 'icon.action.downgrade': QIcon(images.CONDA_MANAGER_DOWNGRADE).pixmap(size), 'icon.upgrade.arrow': QIcon(images.CONDA_MANAGER_UPGRADE_ARROW).pixmap(size), 'background.remove': QColor(0, 0, 0, 0), 'background.install': QColor(0, 0, 0, 0), 'background.upgrade': QColor(0, 0, 0, 0), 'background.downgrade': QColor(0, 0, 0, 0), 'foreground.not.installed': QColor("#666"), 'foreground.upgrade': QColor("#0071a0"), } self.packages_widget.update_style_sheet( style_sheet=style_sheet, extra_dialogs={'cancel_dialog': ClosePackageManagerDialog, 'apply_actions_dialog': ActionsDialog, 'message_box_error': MessageBoxError, }, palette=palette, ) def get_environments(self): """ Return an ordered dictionary of all existing named environments as keys and the prefix as items. The dictionary includes the root environment as the first entry. """ environments = OrderedDict() environments_prefix = sorted(self.api.conda_get_envs()) environments['root'] = self.api.ROOT_PREFIX for prefix in environments_prefix: name = os.path.basename(prefix) environments[name] = prefix return environments def refresh_environments(self): """ Check every `timer_refresh_envs` amount of miliseconds for newly created environments and update the list if new ones are found. """ environments = self.get_environments() if self.previous_environments is None: self.previous_environments = environments.copy() if self.previous_environments != environments: self.previous_environments = environments.copy() self.setup_tab() def open_environment_in(self, which): environment_prefix = self.list_environments.currentItem().prefix() environment_name = self.list_environments.currentItem().text() logger.debug("%s, %s", which, environment_prefix) if environment_name == 'root': environment_prefix = None if which == 'terminal': launch.console(environment_prefix) else: launch.py_in_console(environment_prefix, which) def set_last_active_prefix(self): current_item = self.list_environments.currentItem() if current_item: self.last_env_prefix = getattr(current_item, '_prefix') else: self.last_env_prefix = self.api.ROOT_PREFIX CONF.set('main', 'last_active_prefix', self.last_env_prefix) def setup_tab(self, metadata={}, load_environment=True): if metadata: self.metadata = metadata # show_apps = CONF.get('main', 'show_application_environments') envs = self.get_environments() self.timer_environments.start() self.menu_list.clear() menu_item = self.menu_list.addAction('Open Terminal') menu_item.triggered.connect( lambda: self.open_environment_in('terminal')) for word in ['Python', 'IPython', 'Jupyter Notebook']: menu_item = self.menu_list.addAction("Open with " + word) menu_item.triggered.connect( lambda x, w=word: self.open_environment_in(w.lower())) def select(value=None, position=None): current_item = self.list_environments.currentItem() prefix = current_item.prefix() if isinstance(position, bool) or position is None: width = current_item.button_options.width() position = QPoint(width, 0) # parent_position = self.list_environments.mapToGlobal(QPoint(0, 0)) point = QPoint(0, 0) parent_position = current_item.button_options.mapToGlobal(point) self.menu_list.move(parent_position + position) self.menu_list.actions()[2].setEnabled( launch.check_prog('ipython', prefix)) self.menu_list.actions()[3].setEnabled( launch.check_prog('notebook', prefix)) self.menu_list.exec_() self.set_last_active_prefix() self.list_environments.clear() # if show_apps: # separator_item = ListItemSeparator('My environments:') # self.list_environments.addItem(separator_item) for env in envs: prefix = envs[env] item = ListItemEnvironment(env, prefix=prefix) item.button_options.clicked.connect(select) self.list_environments.addItem(item) # if show_apps: # application_envs = self.api.get_application_environments() # separator_item = ListItemSeparator('Application environments:') # self.list_environments.addItem(separator_item) # for app in application_envs: # env_prefix = application_envs[app] # item = ListItemEnvironment(name=app, prefix=env_prefix) # item.button_options.clicked.connect(select) # self.list_environments.addItem(item) if load_environment: self.load_environment() else: return # Adjust Tab Order self.setTabOrder(self.text_search, self.list_environments._items[0].widget) for i in range(len(self.list_environments._items) - 1): self.setTabOrder(self.list_environments._items[i].widget, self.list_environments._items[i+1].widget) self.setTabOrder(self.list_environments._items[-1].button_name, self.button_create) self.setTabOrder(self.button_create, self.button_clone) self.setTabOrder(self.button_clone, self.button_remove) self.setTabOrder(self.button_remove, self.packages_widget.combobox_filter) self.setTabOrder(self.packages_widget.combobox_filter, self.packages_widget.button_channels) self.setTabOrder(self.packages_widget.button_channels, self.packages_widget.button_update) self.setTabOrder(self.packages_widget.button_update, self.packages_widget.textbox_search) self.setTabOrder(self.packages_widget.textbox_search, self.packages_widget.table_first_row) self.setTabOrder(self.packages_widget.table_last_row, self.packages_widget.button_apply) self.setTabOrder(self.packages_widget.button_apply, self.packages_widget.button_clear) self.setTabOrder(self.packages_widget.button_clear, self.packages_widget.button_cancel) def filter_environments(self): """ Filter displayed environments by matching search text. """ text = self.text_search.text().lower() for i in range(self.list_environments.count()): item = self.list_environments.item(i) item.setHidden(text not in item.text().lower()) if not item.widget.isVisible(): item.widget.repaint() def load_environment(self, item=None): self.update_visibility(False) if item is None: item = self.list_environments.currentItem() if item is None or not isinstance(item, ListItemEnvironment): prefix = self.api.ROOT_PREFIX index = 0 elif item and isinstance(item, ListItemEnvironment): prefix = item.prefix() else: prefix = self.last_env_prefix if self.last_env_prefix else None index = [i for i, it in enumerate(self.list_environments._items) if prefix in it.prefix()] index = index[0] if len(index) else 0 self.list_environments.setCurrentRow(index) self.packages_widget.set_environment(prefix=prefix) self.packages_widget.setup(check_updates=False, blacklist=self.BLACKLIST, metadata=self.metadata) self.list_environments.setDisabled(True) self.update_visibility(False) self.set_last_active_prefix() # update_pointer(Qt.BusyCursor) def refresh(self): self.update_visibility(True) self.list_environments.setDisabled(False) item = self.list_environments.currentItem() try: item.set_loading(False) except RuntimeError: pass # C/C++ object not found is_root = item.text() == 'root' self.button_remove.setDisabled(is_root) self.button_clone.setDisabled(is_root) def update_channels(self, channels, active_channels): """ Save updated channels to the CONF. """ CONF.set('main', 'conda_active_channels', active_channels) CONF.set('main', 'conda_channels', channels) # --- Callbacks # ------------------------------------------------------------------------- def _environment_created(self, worker, output, error): if error: logger.error(str(error)) self.update_visibility(False) for row, environment in enumerate(self.get_environments()): if worker.name == environment: break self.last_env_prefix = self.api.conda_get_prefix_envname(environment) self.setup_tab(load_environment=False) self.list_environments.setCurrentRow(row) self.load_environment() self.refresh() self.update_visibility(True) update_pointer() def _environment_removed(self, worker, output, error): self.update_visibility(True) if error: logger.error(str(error)) self.setup_tab() self.list_environments.setCurrentRow(0) # --- Public API # ------------------------------------------------------------------------- def update_domains(self, anaconda_api_url, conda_url): self.packages_widget.update_domains( anaconda_api_url=anaconda_api_url, conda_url=conda_url, ) def create_environment(self): """ Create new basic environment with selectable python version. Actually makes new env on disc, in directory within the project whose name depends on the env name. New project state is saved. Should also sync to spec file. """ dlg = CreateEnvironmentDialog(parent=self, environments=self.get_environments()) self.tracker.track_page('/environments/create', pagetitle='Create new environment dialog') if dlg.exec_(): name = dlg.text_name.text().strip() pyver = dlg.combo_version.currentText() if name: logger.debug(str('{0}, {1}'.format(name, pyver))) self.update_visibility(False) update_pointer(Qt.BusyCursor) if pyver: pkgs = ['python=' + pyver, 'jupyter'] else: pkgs = ['jupyter'] channels = self.packages_widget._active_channels logger.debug(str((name, pkgs, channels))) self.update_visibility(False) worker = self.packages_widget.create_environment(name=name, packages=pkgs) # worker = self.api.conda_create(name=name, pkgs=pkgs, # channels=channels) worker.name = name worker.sig_finished.connect(self._environment_created) self.tracker.track_page('/environments') def remove_environment(self): """ Clone currently selected environment. """ current_item = self.list_environments.currentItem() if current_item is not None: name = current_item.text() if name == 'root': return dlg = RemoveEnvironmentDialog(environment=name) self.tracker.track_page('/environments/remove', pagetitle='Remove environment dialog') if dlg.exec_(): logger.debug(str(name)) self.update_visibility(False) update_pointer(Qt.BusyCursor) worker = self.packages_widget.remove_environment(name=name) # worker = self.api.conda_remove(name=name, all_=True) worker.sig_finished.connect(self._environment_removed) # self.sig_status_updated.emit('Deleting environment ' # '"{0}"'.format(name), # 0, -1, -1) self.tracker.track_page('/environments') def clone_environment(self): """ Clone currently selected environment. """ current_item = self.list_environments.currentItem() if current_item is not None: current_name = current_item.text() dlg = CloneEnvironmentDialog(parent=self, environments=self.get_environments()) self.tracker.track_page('/environments/clone', pagetitle='Clone environment dialog') if dlg.exec_(): name = dlg.text_name.text().strip() if name and current_name: logger.debug(str("{0}, {1}".format(current_name, name))) self.update_visibility(False) update_pointer(Qt.BusyCursor) worker = self.packages_widget.clone_environment(clone=current_name, name=name) # worker = self.api.conda_clone(current_name, name=name) worker.name = name worker.sig_finished.connect(self._environment_created) self.tracker.track_page('/environments') def import_environment(self): """
class ClientWidget(QWidget, SaveHistoryMixin): """ Client widget for the IPython Console This widget is necessary to handle the interaction between the plugin and each shell widget. """ SEPARATOR = '{0}## ---({1})---'.format(os.linesep * 2, time.ctime()) INITHISTORY = [ '# -*- coding: utf-8 -*-', '# *** Spyder Python Console History Log ***', ] append_to_history = Signal(str, str) def __init__(self, plugin, id_, history_filename, config_options, additional_options, interpreter_versions, connection_file=None, hostname=None, menu_actions=None, slave=False, external_kernel=False, given_name=None, options_button=None, show_elapsed_time=False, reset_warning=True, ask_before_restart=True, ask_before_closing=False, css_path=None): super(ClientWidget, self).__init__(plugin) SaveHistoryMixin.__init__(self, history_filename) # --- Init attrs self.plugin = plugin self.id_ = id_ self.connection_file = connection_file self.hostname = hostname self.menu_actions = menu_actions self.slave = slave self.external_kernel = external_kernel self.given_name = given_name self.show_elapsed_time = show_elapsed_time self.reset_warning = reset_warning self.ask_before_restart = ask_before_restart self.ask_before_closing = ask_before_closing # --- Other attrs self.options_button = options_button self.stop_button = None self.reset_button = None self.stop_icon = ima.icon('stop') self.history = [] self.allow_rename = True self.stderr_dir = None self.is_error_shown = False self.restart_thread = None self.give_focus = True if css_path is None: self.css_path = CSS_PATH else: self.css_path = css_path # --- Widgets self.shellwidget = ShellWidget( config=config_options, ipyclient=self, additional_options=additional_options, interpreter_versions=interpreter_versions, external_kernel=external_kernel, local_kernel=True) self.infowidget = plugin.infowidget self.blank_page = self._create_blank_page() self.loading_page = self._create_loading_page() # To keep a reference to the page to be displayed # in infowidget self.info_page = None self._before_prompt_is_ready() # Elapsed time self.time_label = None self.t0 = time.monotonic() self.timer = QTimer(self) self.show_time_action = create_action( self, _("Show elapsed time"), toggled=self.set_elapsed_time_visible) # --- Layout self.layout = QVBoxLayout() toolbar_buttons = self.get_toolbar_buttons() hlayout = QHBoxLayout() hlayout.addWidget(self.create_time_label()) hlayout.addStretch(0) for button in toolbar_buttons: hlayout.addWidget(button) self.layout.addLayout(hlayout) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.shellwidget) self.layout.addWidget(self.infowidget) self.setLayout(self.layout) # --- Exit function self.exit_callback = lambda: plugin.close_client(client=self) # --- Dialog manager self.dialog_manager = DialogManager() # Show timer self.update_time_label_visibility() # Poll for stderr changes self.stderr_mtime = 0 self.stderr_timer = QTimer(self) self.stderr_timer.timeout.connect(self.poll_stderr_file_change) self.stderr_timer.setInterval(1000) self.stderr_timer.start() def __del__(self): """Close threads to avoid segfault""" if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.wait() #------ Public API -------------------------------------------------------- @property def kernel_id(self): """Get kernel id""" if self.connection_file is not None: json_file = osp.basename(self.connection_file) return json_file.split('.json')[0] @property def stderr_file(self): """Filename to save kernel stderr output.""" stderr_file = None if self.connection_file is not None: stderr_file = self.kernel_id + '.stderr' if self.stderr_dir is not None: stderr_file = osp.join(self.stderr_dir, stderr_file) else: try: stderr_file = osp.join(get_temp_dir(), stderr_file) except (IOError, OSError): stderr_file = None return stderr_file @property def stderr_handle(self): """Get handle to stderr_file.""" if self.stderr_file is not None: # Needed to prevent any error that could appear. # See spyder-ide/spyder#6267. try: handle = codecs.open(self.stderr_file, 'w', encoding='utf-8') except Exception: handle = None else: handle = None return handle def remove_stderr_file(self): """Remove stderr_file associated with the client.""" try: # Defer closing the stderr_handle until the client # is closed because jupyter_client needs it open # while it tries to restart the kernel self.stderr_handle.close() os.remove(self.stderr_file) except Exception: pass def get_stderr_contents(self): """Get the contents of the stderr kernel file.""" try: stderr = self._read_stderr() except Exception: stderr = None return stderr @Slot() def poll_stderr_file_change(self): """Check if the stderr file just changed""" try: mtime = os.stat(self.stderr_file).st_mtime except Exception: return if mtime == self.stderr_mtime: return self.stderr_mtime = mtime stderr = self.get_stderr_contents() if stderr: self.shellwidget._append_plain_text('\n' + stderr, before_prompt=True) def configure_shellwidget(self, give_focus=True): """Configure shellwidget after kernel is connected.""" self.give_focus = give_focus # Make sure the kernel sends the comm config over self.shellwidget.call_kernel()._send_comm_config() # Set exit callback self.shellwidget.set_exit_callback() # To save history self.shellwidget.executing.connect(self.add_to_history) # For Mayavi to run correctly self.shellwidget.executing.connect( self.shellwidget.set_backend_for_mayavi) # To update history after execution self.shellwidget.executed.connect(self.update_history) # To update the Variable Explorer after execution self.shellwidget.executed.connect( self.shellwidget.refresh_namespacebrowser) # To enable the stop button when executing a process self.shellwidget.executing.connect(self.enable_stop_button) # To disable the stop button after execution stopped self.shellwidget.executed.connect(self.disable_stop_button) # To show kernel restarted/died messages self.shellwidget.sig_kernel_restarted_message.connect( self.kernel_restarted_message) self.shellwidget.sig_kernel_restarted.connect(self._finalise_restart) # To correctly change Matplotlib backend interactively self.shellwidget.executing.connect(self.shellwidget.change_mpl_backend) # To show env and sys.path contents self.shellwidget.sig_show_syspath.connect(self.show_syspath) self.shellwidget.sig_show_env.connect(self.show_env) # To sync with working directory toolbar self.shellwidget.executed.connect(self.shellwidget.update_cwd) # To apply style self.set_color_scheme(self.shellwidget.syntax_style, reset=False) def add_to_history(self, command): """Add command to history""" if self.shellwidget.is_debugging(): return return super(ClientWidget, self).add_to_history(command) def _before_prompt_is_ready(self): """Configure shellwidget before kernel is connected.""" self._show_loading_page() self.shellwidget.sig_prompt_ready.connect(self._when_prompt_is_ready) # If remote execution, the loading page should be hidden as well self.shellwidget.sig_remote_execute.connect(self._when_prompt_is_ready) def _when_prompt_is_ready(self): """Configuration after the prompt is shown.""" # To hide the loading page self._hide_loading_page() # Show possible errors when setting Matplotlib backend self._show_mpl_backend_errors() # To show if special console is valid self._check_special_console_error() self.shellwidget.sig_prompt_ready.disconnect( self._when_prompt_is_ready) self.shellwidget.sig_remote_execute.disconnect( self._when_prompt_is_ready) # It's necessary to do this at this point to avoid giving # focus to _control at startup. self._connect_control_signals() if self.give_focus: self.shellwidget._control.setFocus() def enable_stop_button(self): self.stop_button.setEnabled(True) def disable_stop_button(self): # This avoids disabling automatically the button when # re-running files on dedicated consoles. # See spyder-ide/spyder#5958. if not self.shellwidget._executing: # This avoids disabling the button while debugging # see spyder-ide/spyder#13283 if not self.shellwidget.is_waiting_pdb_input(): self.stop_button.setDisabled(True) else: self.stop_button.setEnabled(True) @Slot() def stop_button_click_handler(self): """Method to handle what to do when the stop button is pressed""" self.stop_button.setDisabled(True) # Interrupt computations or stop debugging if not self.shellwidget.is_waiting_pdb_input(): self.interrupt_kernel() else: self.shellwidget.pdb_execute_command('exit') def show_kernel_error(self, error): """Show kernel initialization errors in infowidget.""" # Replace end of line chars with <br> eol = sourcecode.get_eol_chars(error) if eol: error = error.replace(eol, '<br>') # Don't break lines in hyphens # From https://stackoverflow.com/q/7691569/438386 error = error.replace('-', '‑') # Create error page message = _("An error ocurred while starting the kernel") kernel_error_template = Template(KERNEL_ERROR) self.info_page = kernel_error_template.substitute( css_path=self.css_path, message=message, error=error) # Show error self.set_info_page() self.shellwidget.hide() self.infowidget.show() # Tell the client we're in error mode self.is_error_shown = True def get_name(self): """Return client name""" if self.given_name is None: # Name according to host if self.hostname is None: name = _("Console") else: name = self.hostname # Adding id to name client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = name + u' ' + client_id elif self.given_name in ["Pylab", "SymPy", "Cython"]: client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = self.given_name + u' ' + client_id else: name = self.given_name + u'/' + self.id_['str_id'] return name def get_control(self): """Return the text widget (or similar) to give focus to""" # page_control is the widget used for paging page_control = self.shellwidget._page_control if page_control and page_control.isVisible(): return page_control else: return self.shellwidget._control def get_kernel(self): """Get kernel associated with this client""" return self.shellwidget.kernel_manager def get_options_menu(self): """Return options menu""" env_action = create_action(self, _("Show environment variables"), icon=ima.icon('environ'), triggered=self.shellwidget.request_env) syspath_action = create_action( self, _("Show sys.path contents"), icon=ima.icon('syspath'), triggered=self.shellwidget.request_syspath) self.show_time_action.setChecked(self.show_elapsed_time) additional_actions = [ MENU_SEPARATOR, env_action, syspath_action, self.show_time_action ] if self.menu_actions is not None: console_menu = self.menu_actions + additional_actions return console_menu else: return additional_actions def get_toolbar_buttons(self): """Return toolbar buttons list.""" buttons = [] # Code to add the stop button if self.stop_button is None: self.stop_button = create_toolbutton( self, text=_("Stop"), icon=self.stop_icon, tip=_("Stop the current command")) self.disable_stop_button() # set click event handler self.stop_button.clicked.connect(self.stop_button_click_handler) if self.stop_button is not None: buttons.append(self.stop_button) # Reset namespace button if self.reset_button is None: self.reset_button = create_toolbutton( self, text=_("Remove"), icon=ima.icon('editdelete'), tip=_("Remove all variables"), triggered=self.reset_namespace) if self.reset_button is not None: buttons.append(self.reset_button) if self.options_button is None: options = self.get_options_menu() if options: self.options_button = create_toolbutton( self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, options) self.options_button.setMenu(menu) if self.options_button is not None: buttons.append(self.options_button) return buttons def add_actions_to_context_menu(self, menu): """Add actions to IPython widget context menu""" inspect_action = create_action( self, _("Inspect current object"), QKeySequence(CONF.get_shortcut('console', 'inspect current object')), icon=ima.icon('MessageBoxInformation'), triggered=self.inspect_object) clear_line_action = create_action( self, _("Clear line or block"), QKeySequence(CONF.get_shortcut('console', 'clear line')), triggered=self.clear_line) reset_namespace_action = create_action(self, _("Remove all variables"), QKeySequence( CONF.get_shortcut( 'ipython_console', 'reset namespace')), icon=ima.icon('editdelete'), triggered=self.reset_namespace) clear_console_action = create_action( self, _("Clear console"), QKeySequence(CONF.get_shortcut('console', 'clear shell')), triggered=self.clear_console) quit_action = create_action(self, _("&Quit"), icon=ima.icon('exit'), triggered=self.exit_callback) add_actions( menu, (None, inspect_action, clear_line_action, clear_console_action, reset_namespace_action, None, quit_action)) return menu def set_font(self, font): """Set IPython widget's font""" self.shellwidget._control.setFont(font) self.shellwidget.font = font def set_color_scheme(self, color_scheme, reset=True): """Set IPython color scheme.""" # Needed to handle not initialized kernel_client # See spyder-ide/spyder#6996. try: self.shellwidget.set_color_scheme(color_scheme, reset) except AttributeError: pass def shutdown(self): """Shutdown kernel""" if self.get_kernel() is not None and not self.slave: self.shellwidget.shutdown() def close(self): """Close client""" self.shellwidget.will_close(self.get_kernel() is None or self.slave) super(ClientWidget, self).close() def interrupt_kernel(self): """Interrupt the associanted Spyder kernel if it's running""" # Needed to prevent a crash when a kernel is not running. # See spyder-ide/spyder#6299. try: self.shellwidget.request_interrupt_kernel() except RuntimeError: pass @Slot() def restart_kernel(self): """ Restart the associated kernel. Took this code from the qtconsole project Licensed under the BSD license """ sw = self.shellwidget if not running_under_pytest() and self.ask_before_restart: message = _('Are you sure you want to restart the kernel?') buttons = QMessageBox.Yes | QMessageBox.No result = QMessageBox.question(self, _('Restart kernel?'), message, buttons) else: result = None if (result == QMessageBox.Yes or running_under_pytest() or not self.ask_before_restart): if sw.kernel_manager: if self.infowidget.isVisible(): self.infowidget.hide() if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return self._show_loading_page() # Close comm sw.spyder_kernel_comm.close() # Stop autorestart mechanism sw.kernel_manager.stop_restarter() sw.kernel_manager.autorestart = False # Create and run restarting thread if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.finished.disconnect() self.restart_thread.terminate() self.restart_thread.wait() self.restart_thread = QThread() self.restart_thread.run = self._restart_thread_main self.restart_thread.error = None self.restart_thread.finished.connect( lambda: self._finalise_restart(True)) self.restart_thread.start() else: sw._append_plain_text( _('Cannot restart a kernel not started by Spyder\n'), before_prompt=True) self._hide_loading_page() def _restart_thread_main(self): """Restart the kernel in a thread.""" try: self.shellwidget.kernel_manager.restart_kernel( stderr=self.stderr_handle) except RuntimeError as e: self.restart_thread.error = e def _finalise_restart(self, reset=False): """Finishes the restarting of the kernel.""" sw = self.shellwidget if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return if self.restart_thread and self.restart_thread.error is not None: sw._append_plain_text(_('Error restarting kernel: %s\n') % self.restart_thread.error, before_prompt=True) else: # Reset Pdb state and reopen comm sw._pdb_in_loop = False sw.spyder_kernel_comm.remove() sw.spyder_kernel_comm.open_comm(sw.kernel_client) # Start autorestart mechanism sw.kernel_manager.autorestart = True sw.kernel_manager.start_restarter() # For spyder-ide/spyder#6235, IPython was changing the # setting of %colors on windows by assuming it was using a # dark background. This corrects it based on the scheme. self.set_color_scheme(sw.syntax_style, reset=reset) sw._append_html(_("<br>Restarting kernel...\n<hr><br>"), before_prompt=True) self._hide_loading_page() self.stop_button.setDisabled(True) self.restart_thread = None @Slot(str) def kernel_restarted_message(self, msg): """Show kernel restarted/died messages.""" if not self.is_error_shown: # If there are kernel creation errors, jupyter_client will # try to restart the kernel and qtconsole prints a # message about it. # So we read the kernel's stderr_file and display its # contents in the client instead of the usual message shown # by qtconsole. stderr = self.get_stderr_contents() if stderr: self.show_kernel_error('<tt>%s</tt>' % stderr) else: self.shellwidget._append_html("<br>%s<hr><br>" % msg, before_prompt=False) @Slot() def inspect_object(self): """Show how to inspect an object with our Help plugin""" self.shellwidget._control.inspect_current_object() @Slot() def clear_line(self): """Clear a console line""" self.shellwidget._keyboard_quit() @Slot() def clear_console(self): """Clear the whole console""" self.shellwidget.clear_console() @Slot() def reset_namespace(self): """Resets the namespace by removing all names defined by the user""" self.shellwidget.reset_namespace(warning=self.reset_warning, message=True) def update_history(self): self.history = self.shellwidget._history @Slot(object) def show_syspath(self, syspath): """Show sys.path contents.""" if syspath is not None: editor = CollectionsEditor(self) editor.setup(syspath, title="sys.path contents", readonly=True, icon=ima.icon('syspath')) self.dialog_manager.show(editor) else: return @Slot(object) def show_env(self, env): """Show environment variables.""" self.dialog_manager.show(RemoteEnvDialog(env, parent=self)) def create_time_label(self): """Create elapsed time label widget (if necessary) and return it""" if self.time_label is None: self.time_label = QLabel() return self.time_label def show_time(self, end=False): """Text to show in time_label.""" if self.time_label is None: return elapsed_time = time.monotonic() - self.t0 # System time changed to past date, so reset start. if elapsed_time < 0: self.t0 = time.monotonic() elapsed_time = 0 if elapsed_time > 24 * 3600: # More than a day...! fmt = "%d %H:%M:%S" else: fmt = "%H:%M:%S" if end: color = QStylePalette.COLOR_TEXT_3 else: color = QStylePalette.COLOR_ACCENT_4 text = "<span style=\'color: %s\'><b>%s" \ "</b></span>" % (color, time.strftime(fmt, time.gmtime(elapsed_time))) self.time_label.setText(text) def update_time_label_visibility(self): """Update elapsed time visibility.""" self.time_label.setVisible(self.show_elapsed_time) @Slot(bool) def set_elapsed_time_visible(self, state): """Slot to show/hide elapsed time label.""" self.show_elapsed_time = state if self.time_label is not None: self.time_label.setVisible(state) def set_info_page(self): """Set current info_page.""" if self.info_page is not None: self.infowidget.setHtml(self.info_page, QUrl.fromLocalFile(self.css_path)) #------ Private API ------------------------------------------------------- def _create_loading_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(LOADING) loading_img = get_image_path('loading_sprites') if os.name == 'nt': loading_img = loading_img.replace('\\', '/') message = _("Connecting to kernel...") page = loading_template.substitute(css_path=self.css_path, loading_img=loading_img, message=message) return page def _create_blank_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(BLANK) page = loading_template.substitute(css_path=self.css_path) return page def _show_loading_page(self): """Show animation while the kernel is loading.""" self.shellwidget.hide() self.infowidget.show() self.info_page = self.loading_page self.set_info_page() def _hide_loading_page(self): """Hide animation shown while the kernel is loading.""" self.infowidget.hide() self.info_page = self.blank_page self.set_info_page() self.shellwidget.show() def _read_stderr(self): """Read the stderr file of the kernel.""" # We need to read stderr_file as bytes to be able to # detect its encoding with chardet f = open(self.stderr_file, 'rb') try: stderr_text = f.read() # This is needed to avoid showing an empty error message # when the kernel takes too much time to start. # See spyder-ide/spyder#8581. if not stderr_text: return '' # This is needed since the stderr file could be encoded # in something different to utf-8. # See spyder-ide/spyder#4191. encoding = get_coding(stderr_text) stderr_text = to_text_string(stderr_text, encoding) return stderr_text finally: f.close() def _show_mpl_backend_errors(self): """ Show possible errors when setting the selected Matplotlib backend. """ if not self.external_kernel: self.shellwidget.call_kernel().show_mpl_backend_errors() def _check_special_console_error(self): """Check if the dependecies for special consoles are available.""" self.shellwidget.call_kernel(callback=self._show_special_console_error ).is_special_kernel_valid() def _show_special_console_error(self, missing_dependency): if missing_dependency is not None: error_message = _( "Your Python environment or installation doesn't have the " "<tt>{missing_dependency}</tt> module installed or it " "occurred a problem importing it. Due to that, it is not " "possible for Spyder to create this special console for " "you.").format(missing_dependency=missing_dependency) self.show_kernel_error(error_message) def _abort_kernel_restart(self): """ Abort kernel restart if there are errors while starting it. We also ignore errors about comms, which are irrelevant. """ stderr = self.get_stderr_contents() if stderr and 'No such comm' not in stderr: return True else: return False def _connect_control_signals(self): """Connect signals of control widgets.""" control = self.shellwidget._control page_control = self.shellwidget._page_control control.focus_changed.connect(lambda: self.plugin.focus_changed.emit()) page_control.focus_changed.connect( lambda: self.plugin.focus_changed.emit()) control.visibility_changed.connect(self.plugin.refresh_plugin) page_control.visibility_changed.connect(self.plugin.refresh_plugin) page_control.show_find_widget.connect(self.plugin.find_widget.show)
class FindReplace(QWidget): """Find widget""" STYLE = {False: "background-color:rgb(255, 175, 90);", True: "", None: ""} visibility_changed = Signal(bool) def __init__(self, parent, enable_replace=False): QWidget.__init__(self, parent) self.enable_replace = enable_replace self.editor = None self.is_code_editor = None glayout = QGridLayout() glayout.setContentsMargins(0, 0, 0, 0) self.setLayout(glayout) self.close_button = create_toolbutton( self, triggered=self.hide, icon=ima.icon('DialogCloseButton')) glayout.addWidget(self.close_button, 0, 0) # Find layout self.search_text = PatternComboBox(self, tip=_("Search string"), adjust_to_minimum=False) self.search_text.valid.connect(lambda state: self.find( changed=False, forward=True, rehighlight=False)) self.search_text.lineEdit().textEdited.connect( self.text_has_been_edited) self.previous_button = create_toolbutton(self, triggered=self.find_previous, icon=ima.icon('ArrowUp')) self.next_button = create_toolbutton(self, triggered=self.find_next, icon=ima.icon('ArrowDown')) self.next_button.clicked.connect(self.update_search_combo) self.previous_button.clicked.connect(self.update_search_combo) self.re_button = create_toolbutton(self, icon=ima.icon('advanced'), tip=_("Regular expression")) self.re_button.setCheckable(True) self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton(self, icon=get_icon("upper_lower.png"), tip=_("Case Sensitive")) self.case_button.setCheckable(True) self.case_button.toggled.connect(lambda state: self.find()) self.words_button = create_toolbutton(self, icon=get_icon("whole_words.png"), tip=_("Whole words")) self.words_button.setCheckable(True) self.words_button.toggled.connect(lambda state: self.find()) self.highlight_button = create_toolbutton( self, icon=get_icon("highlight.png"), tip=_("Highlight matches")) self.highlight_button.setCheckable(True) self.highlight_button.toggled.connect(self.toggle_highlighting) hlayout = QHBoxLayout() self.widgets = [ self.close_button, self.search_text, self.previous_button, self.next_button, self.re_button, self.case_button, self.words_button, self.highlight_button ] for widget in self.widgets[1:]: hlayout.addWidget(widget) glayout.addLayout(hlayout, 0, 1) # Replace layout replace_with = QLabel(_("Replace with:")) self.replace_text = PatternComboBox(self, adjust_to_minimum=False, tip=_('Replace string')) self.replace_button = create_toolbutton( self, text=_('Replace/find'), icon=ima.icon('DialogApplyButton'), triggered=self.replace_find, text_beside_icon=True) self.replace_button.clicked.connect(self.update_replace_combo) self.replace_button.clicked.connect(self.update_search_combo) self.all_check = QCheckBox(_("Replace all")) self.replace_layout = QHBoxLayout() widgets = [ replace_with, self.replace_text, self.replace_button, self.all_check ] for widget in widgets: self.replace_layout.addWidget(widget) glayout.addLayout(self.replace_layout, 1, 1) self.widgets.extend(widgets) self.replace_widgets = widgets self.hide_replace() self.search_text.setTabOrder(self.search_text, self.replace_text) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.shortcuts = self.create_shortcuts(parent) self.highlight_timer = QTimer(self) self.highlight_timer.setSingleShot(True) self.highlight_timer.setInterval(1000) self.highlight_timer.timeout.connect(self.highlight_matches) def create_shortcuts(self, parent): """Create shortcuts for this widget""" # Configurable findnext = config_shortcut(self.find_next, context='_', name='Find next', parent=parent) findprev = config_shortcut(self.find_previous, context='_', name='Find previous', parent=parent) togglefind = config_shortcut(self.show, context='_', name='Find text', parent=parent) togglereplace = config_shortcut(self.toggle_replace_widgets, context='_', name='Replace text', parent=parent) # Fixed fixed_shortcut("Escape", self, self.hide) return [findnext, findprev, togglefind, togglereplace] def get_shortcut_data(self): """ Returns shortcut data, a list of tuples (shortcut, text, default) shortcut (QShortcut or QAction instance) text (string): action/shortcut description default (string): default key sequence """ return [sc.data for sc in self.shortcuts] def update_search_combo(self): self.search_text.lineEdit().returnPressed.emit() def update_replace_combo(self): self.replace_text.lineEdit().returnPressed.emit() def toggle_replace_widgets(self): if self.enable_replace: # Toggle replace widgets if self.replace_widgets[0].isVisible(): self.hide_replace() self.hide() else: self.show_replace() self.replace_text.setFocus() @Slot(bool) def toggle_highlighting(self, state): """Toggle the 'highlight all results' feature""" if self.editor is not None: if state: self.highlight_matches() else: self.clear_matches() def show(self): """Overrides Qt Method""" QWidget.show(self) self.visibility_changed.emit(True) if self.editor is not None: text = self.editor.get_selected_text() # If no text is highlighted for search, use whatever word is under # the cursor if not text: try: cursor = self.editor.textCursor() cursor.select(QTextCursor.WordUnderCursor) text = to_text_string(cursor.selectedText()) except AttributeError: # We can't do this for all widgets, e.g. WebView's pass # Now that text value is sorted out, use it for the search if text: self.search_text.setEditText(text) self.search_text.lineEdit().selectAll() self.refresh() else: self.search_text.lineEdit().selectAll() self.search_text.setFocus() @Slot() def hide(self): """Overrides Qt Method""" for widget in self.replace_widgets: widget.hide() QWidget.hide(self) self.visibility_changed.emit(False) if self.editor is not None: self.editor.setFocus() self.clear_matches() def show_replace(self): """Show replace widgets""" self.show() for widget in self.replace_widgets: widget.show() def hide_replace(self): """Hide replace widgets""" for widget in self.replace_widgets: widget.hide() def refresh(self): """Refresh widget""" if self.isHidden(): if self.editor is not None: self.clear_matches() return state = self.editor is not None for widget in self.widgets: widget.setEnabled(state) if state: self.find() def set_editor(self, editor, refresh=True): """ Set associated editor/web page: codeeditor.base.TextEditBaseWidget browser.WebView """ self.editor = editor # Note: This is necessary to test widgets/editor.py # in Qt builds that don't have web widgets try: from qtpy.QtWebEngineWidgets import QWebEngineView except ImportError: QWebEngineView = type(None) self.words_button.setVisible(not isinstance(editor, QWebEngineView)) self.re_button.setVisible(not isinstance(editor, QWebEngineView)) from spyder.widgets.sourcecode.codeeditor import CodeEditor self.is_code_editor = isinstance(editor, CodeEditor) self.highlight_button.setVisible(self.is_code_editor) if refresh: self.refresh() if self.isHidden() and editor is not None: self.clear_matches() @Slot() def find_next(self): """Find next occurrence""" state = self.find(changed=False, forward=True, rehighlight=False) self.editor.setFocus() self.search_text.add_current_text() return state @Slot() def find_previous(self): """Find previous occurrence""" state = self.find(changed=False, forward=False, rehighlight=False) self.editor.setFocus() return state def text_has_been_edited(self, text): """Find text has been edited (this slot won't be triggered when setting the search pattern combo box text programmatically""" self.find(changed=True, forward=True, start_highlight_timer=True) def highlight_matches(self): """Highlight found results""" if self.is_code_editor and self.highlight_button.isChecked(): text = self.search_text.currentText() words = self.words_button.isChecked() regexp = self.re_button.isChecked() self.editor.highlight_found_results(text, words=words, regexp=regexp) def clear_matches(self): """Clear all highlighted matches""" if self.is_code_editor: self.editor.clear_found_results() def find(self, changed=True, forward=True, rehighlight=True, start_highlight_timer=False): """Call the find function""" text = self.search_text.currentText() if len(text) == 0: self.search_text.lineEdit().setStyleSheet("") if not self.is_code_editor: # Clears the selection for WebEngine self.editor.find_text('') return None else: case = self.case_button.isChecked() words = self.words_button.isChecked() regexp = self.re_button.isChecked() found = self.editor.find_text(text, changed, forward, case=case, words=words, regexp=regexp) self.search_text.lineEdit().setStyleSheet(self.STYLE[found]) if self.is_code_editor and found: if rehighlight or not self.editor.found_results: self.highlight_timer.stop() if start_highlight_timer: self.highlight_timer.start() else: self.highlight_matches() else: self.clear_matches() return found
class NapariQtNotification(QDialog): """Notification dialog frame, appears at the bottom right of the canvas. By default, only the first line of the notification is shown, and the text is elided. Double-clicking on the text (or clicking the chevron icon) will expand to show the full notification. The dialog will autmatically disappear in ``DISMISS_AFTER`` milliseconds, unless hovered or clicked. Parameters ---------- message : str The message that will appear in the notification severity : str or NotificationSeverity, optional Severity level {'error', 'warning', 'info', 'none'}. Will determine the icon associated with the message. by default NotificationSeverity.WARNING. source : str, optional A source string for the notifcation (intended to show the module and or package responsible for the notification), by default None actions : list of tuple, optional A sequence of 2-tuples, where each tuple is a string and a callable. Each tuple will be used to create button in the dialog, where the text on the button is determine by the first item in the tuple, and a callback function to call when the button is pressed is the second item in the tuple. by default () """ MAX_OPACITY = 0.9 FADE_IN_RATE = 220 FADE_OUT_RATE = 120 DISMISS_AFTER = 4000 MIN_WIDTH = 400 MIN_EXPANSION = 18 message: MultilineElidedLabel source_label: QLabel severity_icon: QLabel def __init__( self, message: str, severity: Union[str, NotificationSeverity] = 'WARNING', source: Optional[str] = None, actions: ActionSequence = (), ): super().__init__() from ..qt_main_window import _QtMainWindow current_window = _QtMainWindow.current() if current_window is not None: canvas = current_window.qt_viewer._canvas_overlay self.setParent(canvas) canvas.resized.connect(self.move_to_bottom_right) self.setupUi() self.setAttribute(Qt.WA_DeleteOnClose) self.setup_buttons(actions) self.setMouseTracking(True) self.severity_icon.setText(NotificationSeverity(severity).as_icon()) self.message.setText(message) if source: self.source_label.setText( trans._('Source: {source}', source=source)) self.close_button.clicked.connect(self.close) self.expand_button.clicked.connect(self.toggle_expansion) self.timer = QTimer() self.opacity = QGraphicsOpacityEffect() self.setGraphicsEffect(self.opacity) self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity", self) self.geom_anim = QPropertyAnimation(self, b"geometry", self) self.move_to_bottom_right() def move_to_bottom_right(self, offset=(8, 8)): """Position widget at the bottom right edge of the parent.""" if not self.parent(): return sz = self.parent().size() - self.size() - QSize(*offset) self.move(QPoint(sz.width(), sz.height())) def slide_in(self): """Run animation that fades in the dialog with a slight slide up.""" geom = self.geometry() self.geom_anim.setDuration(self.FADE_IN_RATE) self.geom_anim.setStartValue(geom.translated(0, 20)) self.geom_anim.setEndValue(geom) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) # fade in self.opacity_anim.setDuration(self.FADE_IN_RATE) self.opacity_anim.setStartValue(0) self.opacity_anim.setEndValue(self.MAX_OPACITY) self.geom_anim.start() self.opacity_anim.start() def show(self): """Show the message with a fade and slight slide in from the bottom.""" super().show() self.slide_in() if self.DISMISS_AFTER > 0: self.timer.setInterval(self.DISMISS_AFTER) self.timer.setSingleShot(True) self.timer.timeout.connect(self.close) self.timer.start() def mouseMoveEvent(self, event): """On hover, stop the self-destruct timer""" self.timer.stop() def mouseDoubleClickEvent(self, event): """Expand the notification on double click.""" self.toggle_expansion() def close(self): """Fade out then close.""" self.opacity_anim.setDuration(self.FADE_OUT_RATE) self.opacity_anim.setStartValue(self.MAX_OPACITY) self.opacity_anim.setEndValue(0) self.opacity_anim.start() self.opacity_anim.finished.connect(super().close) def toggle_expansion(self): """Toggle the expanded state of the notification frame.""" self.contract() if self.property('expanded') else self.expand() self.timer.stop() def expand(self): """Expanded the notification so that the full message is visible.""" curr = self.geometry() self.geom_anim.setDuration(100) self.geom_anim.setStartValue(curr) new_height = self.sizeHint().height() if new_height < curr.height(): # new height would shift notification down, ensure some expansion new_height = curr.height() + self.MIN_EXPANSION delta = new_height - curr.height() self.geom_anim.setEndValue( QRect(curr.x(), curr.y() - delta, curr.width(), new_height)) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) self.geom_anim.start() self.setProperty('expanded', True) self.style().unpolish(self.expand_button) self.style().polish(self.expand_button) def contract(self): """Contract notification to a single elided line of the message.""" geom = self.geometry() self.geom_anim.setDuration(100) self.geom_anim.setStartValue(geom) dlt = geom.height() - self.minimumHeight() self.geom_anim.setEndValue( QRect(geom.x(), geom.y() + dlt, geom.width(), geom.height() - dlt)) self.geom_anim.setEasingCurve(QEasingCurve.OutQuad) self.geom_anim.start() self.setProperty('expanded', False) self.style().unpolish(self.expand_button) self.style().polish(self.expand_button) def setupUi(self): """Set up the UI during initialization.""" self.setWindowFlags(Qt.SubWindow) self.setMinimumWidth(self.MIN_WIDTH) self.setMaximumWidth(self.MIN_WIDTH) self.setMinimumHeight(40) self.setSizeGripEnabled(False) self.setModal(False) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setContentsMargins(2, 2, 2, 2) self.verticalLayout.setSpacing(0) self.row1_widget = QWidget(self) self.row1 = QHBoxLayout(self.row1_widget) self.row1.setContentsMargins(12, 12, 12, 8) self.row1.setSpacing(4) self.severity_icon = QLabel(self.row1_widget) self.severity_icon.setObjectName("severity_icon") self.severity_icon.setMinimumWidth(30) self.severity_icon.setMaximumWidth(30) self.row1.addWidget(self.severity_icon, alignment=Qt.AlignTop) self.message = MultilineElidedLabel(self.row1_widget) self.message.setMinimumWidth(self.MIN_WIDTH - 200) self.message.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.row1.addWidget(self.message, alignment=Qt.AlignTop) self.expand_button = QPushButton(self.row1_widget) self.expand_button.setObjectName("expand_button") self.expand_button.setCursor(Qt.PointingHandCursor) self.expand_button.setMaximumWidth(20) self.expand_button.setFlat(True) self.row1.addWidget(self.expand_button, alignment=Qt.AlignTop) self.close_button = QPushButton(self.row1_widget) self.close_button.setObjectName("close_button") self.close_button.setCursor(Qt.PointingHandCursor) self.close_button.setMaximumWidth(20) self.close_button.setFlat(True) self.row1.addWidget(self.close_button, alignment=Qt.AlignTop) self.verticalLayout.addWidget(self.row1_widget, 1) self.row2_widget = QWidget(self) self.row2_widget.hide() self.row2 = QHBoxLayout(self.row2_widget) self.source_label = QLabel(self.row2_widget) self.source_label.setObjectName("source_label") self.row2.addWidget(self.source_label, alignment=Qt.AlignBottom) self.row2.addStretch() self.row2.setContentsMargins(12, 2, 16, 12) self.row2_widget.setMaximumHeight(34) self.row2_widget.setStyleSheet('QPushButton{' 'padding: 4px 12px 4px 12px; ' 'font-size: 11px;' 'min-height: 18px; border-radius: 0;}') self.verticalLayout.addWidget(self.row2_widget, 0) self.setProperty('expanded', False) self.resize(self.MIN_WIDTH, 40) def setup_buttons(self, actions: ActionSequence = ()): """Add buttons to the dialog. Parameters ---------- actions : tuple, optional A sequence of 2-tuples, where each tuple is a string and a callable. Each tuple will be used to create button in the dialog, where the text on the button is determine by the first item in the tuple, and a callback function to call when the button is pressed is the second item in the tuple. by default () """ if isinstance(actions, dict): actions = list(actions.items()) for text, callback in actions: btn = QPushButton(text) def call_back_with_self(callback, self): """ We need a higher order function this to capture the reference to self. """ def _inner(): return callback(self) return _inner btn.clicked.connect(call_back_with_self(callback, self)) btn.clicked.connect(self.close) self.row2.addWidget(btn) if actions: self.row2_widget.show() self.setMinimumHeight(self.row2_widget.maximumHeight() + self.minimumHeight()) def sizeHint(self): """Return the size required to show the entire message.""" return QSize( super().sizeHint().width(), self.row2_widget.height() + self.message.sizeHint().height(), ) @classmethod def from_notification(cls, notification: Notification) -> NapariQtNotification: from ...utils.notifications import ErrorNotification actions = notification.actions if isinstance(notification, ErrorNotification): def show_tb(parent): tbdialog = QDialog(parent=parent.parent()) tbdialog.setModal(True) # this is about the minimum width to not get rewrap # and the minimum height to not have scrollbar tbdialog.resize(650, 270) tbdialog.setLayout(QVBoxLayout()) text = QTextEdit() text.setHtml(notification.as_html()) text.setReadOnly(True) btn = QPushButton(trans._('Enter Debugger')) def _enter_debug_mode(): btn.setText( trans. _('Now Debugging. Please quit debugger in console to continue' )) _debug_tb(notification.exception.__traceback__) btn.setText(trans._('Enter Debugger')) btn.clicked.connect(_enter_debug_mode) tbdialog.layout().addWidget(text) tbdialog.layout().addWidget(btn, 0, Qt.AlignRight) tbdialog.show() actions = tuple(notification.actions) + ( (trans._('View Traceback'), show_tb), ) else: actions = notification.actions return cls( message=notification.message, severity=notification.severity, source=notification.source, actions=actions, ) @classmethod def show_notification(cls, notification: Notification): from ...utils.settings import get_settings settings = get_settings() # after https://github.com/napari/napari/issues/2370, # the os.getenv can be removed (and NAPARI_CATCH_ERRORS retired) if (os.getenv("NAPARI_CATCH_ERRORS") not in ('0', 'False') and notification.severity >= settings.application.gui_notification_level): application_instance = QApplication.instance() if application_instance: # Check if this is running from a thread if application_instance.thread() != QThread.currentThread(): dispatcher = getattr(application_instance, "_dispatcher", None) if dispatcher: dispatcher.sig_notified.emit(notification) return cls.from_notification(notification).show()
class _DownloadAPI(QObject): """ Download API based on QNetworkAccessManager """ def __init__(self, chunk_size=1024): super(_DownloadAPI, self).__init__() self._chunk_size = chunk_size self._head_requests = {} self._get_requests = {} self._paths = {} self._workers = {} self._manager = QNetworkAccessManager(self) self._timer = QTimer() # Setup self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) # Signals self._manager.finished.connect(self._request_finished) self._manager.sslErrors.connect(self._handle_ssl_errors) def _handle_ssl_errors(self, reply, errors): logger.error(str(('SSL Errors', errors))) def _clean(self): """ Periodically check for inactive workers and remove their references. """ if self._workers: for url in self._workers.copy(): w = self._workers[url] if w.is_finished(): self._workers.pop(url) self._paths.pop(url) if url in self._get_requests: self._get_requests.pop(url) else: self._timer.stop() def _request_finished(self, reply): url = to_text_string(reply.url().toEncoded(), encoding='utf-8') if url in self._paths: path = self._paths[url] if url in self._workers: worker = self._workers[url] if url in self._head_requests: self._head_requests.pop(url) start_download = True header_pairs = reply.rawHeaderPairs() headers = {} for hp in header_pairs: headers[to_text_string(hp[0]).lower()] = to_text_string(hp[1]) total_size = int(headers.get('content-length', 0)) # Check if file exists if os.path.isfile(path): file_size = os.path.getsize(path) # Check if existing file matches size of requested file start_download = file_size != total_size if start_download: # File sizes dont match, hence download file qurl = QUrl(url) request = QNetworkRequest(qurl) self._get_requests[url] = request reply = self._manager.get(request) error = reply.error() if error: logger.error(str(('Reply Error:', error))) reply.downloadProgress.connect( lambda r, t, w=worker: self._progress(r, t, w)) else: # File sizes match, dont download file worker.finished = True worker.sig_download_finished.emit(url, path) worker.sig_finished.emit(worker, path, None) elif url in self._get_requests: data = reply.readAll() self._save(url, path, data) def _save(self, url, path, data): """ """ worker = self._workers[url] path = self._paths[url] if len(data): with open(path, 'wb') as f: f.write(data) # Clean up worker.finished = True worker.sig_download_finished.emit(url, path) worker.sig_finished.emit(worker, path, None) self._get_requests.pop(url) self._workers.pop(url) self._paths.pop(url) def _progress(self, bytes_received, bytes_total, worker): """ """ worker.sig_download_progress.emit(worker.url, worker.path, bytes_received, bytes_total) def download(self, url, path): """ """ # original_url = url qurl = QUrl(url) url = to_text_string(qurl.toEncoded(), encoding='utf-8') logger.debug(str((url, path))) if url in self._workers: while not self._workers[url].finished: return self._workers[url] worker = DownloadWorker(url, path) # Check download folder exists folder = os.path.dirname(os.path.abspath(path)) if not os.path.isdir(folder): os.makedirs(folder) request = QNetworkRequest(qurl) self._head_requests[url] = request self._paths[url] = path self._workers[url] = worker self._manager.head(request) self._timer.start() return worker def terminate(self): pass
def start_client(self, language): """Start an LSP client for a given language.""" # To keep track if the client was started. started = False if language in self.clients: language_client = self.clients[language] queue = self.register_queue[language] # Don't start LSP services when testing unless we demand # them. if running_under_pytest(): if not os.environ.get('SPY_TEST_USE_INTROSPECTION'): return started started = language_client['status'] == self.RUNNING # Start client heartbeat timer = QTimer(self) self.clients_hearbeat[language] = timer timer.setInterval(self.TIME_HEARTBEAT) timer.timeout.connect(lambda: self.check_heartbeat(language)) timer.start() if language_client['status'] == self.STOPPED: config = language_client['config'] # If we're trying to connect to an external server, # verify that it's listening before creating a # client for it. if config['external']: host = config['host'] port = config['port'] response = check_connection_port(host, port) if not response: if self.show_no_external_server_warning: self.report_no_external_server( host, port, language) self.update_status(language, ClientStatus.DOWN) return False language_client['instance'] = LSPClient( parent=self, server_settings=config, folder=self.get_root_path(language), language=language ) self.register_client_instance(language_client['instance']) # Register that a client was started. logger.info("Starting LSP client for {}...".format(language)) language_client['instance'].start() language_client['status'] = self.RUNNING started = True for entry in queue: language_client['instance'].register_file(*entry) self.register_queue[language] = [] return started
class QWaitingSpinner(QWidget): def __init__(self, parent, centerOnParent=True, disableParentWhenSpinning=False, modality=Qt.NonModal): # super().__init__(parent) QWidget.__init__(self, parent) self._centerOnParent = centerOnParent self._disableParentWhenSpinning = disableParentWhenSpinning # WAS IN initialize() self._color = QColor(Qt.black) self._roundness = 100.0 self._minimumTrailOpacity = 3.14159265358979323846 self._trailFadePercentage = 80.0 self._trailSizeDecreasing = False self._revolutionsPerSecond = 1.57079632679489661923 self._numberOfLines = 20 self._lineLength = 10 self._lineWidth = 2 self._innerRadius = 10 self._currentCounter = 0 self._isSpinning = False self._timer = QTimer(self) self._timer.timeout.connect(self.rotate) self.updateSize() self.updateTimer() self.hide() # END initialize() self.setWindowModality(modality) self.setAttribute(Qt.WA_TranslucentBackground) self.show() def paintEvent(self, QPaintEvent): if not self._isSpinning: return self.updatePosition() painter = QPainter(self) painter.fillRect(self.rect(), Qt.transparent) painter.setRenderHint(QPainter.Antialiasing, True) if self._currentCounter >= self._numberOfLines: self._currentCounter = 0 painter.setPen(Qt.NoPen) for i in range(0, self._numberOfLines): painter.save() painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength) rotateAngle = float(360 * i) / float(self._numberOfLines) painter.rotate(rotateAngle) painter.translate(self._innerRadius, 0) distance = self.lineCountDistanceFromPrimary( i, self._currentCounter, self._numberOfLines) color = self.currentLineColor(distance, self._numberOfLines, self._trailFadePercentage, self._minimumTrailOpacity, self._color) # Compute the scaling factor to apply to the size and thickness # of the lines in the trail. if self._trailSizeDecreasing: sf = (self._numberOfLines - distance) / self._numberOfLines else: sf = 1 painter.setBrush(color) rect = QRect(0, round(-self._lineWidth / 2), round(sf * self._lineLength), round(sf * self._lineWidth)) painter.drawRoundedRect(rect, self._roundness, self._roundness, Qt.RelativeSize) painter.restore() def start(self): self.updatePosition() self._isSpinning = True if self.parentWidget and self._disableParentWhenSpinning: self.parentWidget().setEnabled(False) if not self._timer.isActive(): self._timer.start() self._currentCounter = 0 self.show() def stop(self): self._isSpinning = False if self.parentWidget() and self._disableParentWhenSpinning: self.parentWidget().setEnabled(True) if self._timer.isActive(): self._timer.stop() self._currentCounter = 0 self.show() self.repaint() def setNumberOfLines(self, lines): self._numberOfLines = lines self._currentCounter = 0 self.updateTimer() def setLineLength(self, length): self._lineLength = length self.updateSize() def setLineWidth(self, width): self._lineWidth = width self.updateSize() def setInnerRadius(self, radius): self._innerRadius = radius self.updateSize() def color(self): return self._color def roundness(self): return self._roundness def minimumTrailOpacity(self): return self._minimumTrailOpacity def trailFadePercentage(self): return self._trailFadePercentage def revolutionsPersSecond(self): return self._revolutionsPerSecond def numberOfLines(self): return self._numberOfLines def lineLength(self): return self._lineLength def isTrailSizeDecreasing(self): """ Return whether the length and thickness of the trailing lines are decreasing. """ return self._trailSizeDecreasing def lineWidth(self): return self._lineWidth def innerRadius(self): return self._innerRadius def isSpinning(self): return self._isSpinning def setRoundness(self, roundness): self._roundness = max(0.0, min(100.0, roundness)) def setColor(self, color=Qt.black): self._color = QColor(color) def setRevolutionsPerSecond(self, revolutionsPerSecond): self._revolutionsPerSecond = revolutionsPerSecond self.updateTimer() def setTrailFadePercentage(self, trail): self._trailFadePercentage = trail def setTrailSizeDecreasing(self, value): """ Set whether the length and thickness of the trailing lines are decreasing. """ self._trailSizeDecreasing = value def setMinimumTrailOpacity(self, minimumTrailOpacity): self._minimumTrailOpacity = minimumTrailOpacity def rotate(self): self._currentCounter += 1 if self._currentCounter >= self._numberOfLines: self._currentCounter = 0 self.update() def updateSize(self): size = int((self._innerRadius + self._lineLength) * 2) self.setFixedSize(size, size) def updateTimer(self): self._timer.setInterval( int(1000 / (self._numberOfLines * self._revolutionsPerSecond))) def updatePosition(self): if self.parentWidget() and self._centerOnParent: self.move( int(self.parentWidget().width() / 2 - self.width() / 2), int(self.parentWidget().height() / 2 - self.height() / 2)) def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines): distance = primary - current if distance < 0: distance += totalNrOfLines return distance def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput): color = QColor(colorinput) if countDistance == 0: return color minAlphaF = minOpacity / 100.0 distanceThreshold = int( math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)) if countDistance > distanceThreshold: color.setAlphaF(minAlphaF) else: alphaDiff = color.alphaF() - minAlphaF gradient = alphaDiff / float(distanceThreshold + 1) resultAlpha = color.alphaF() - gradient * countDistance # If alpha is out of bounds, clip it. resultAlpha = min(1.0, max(0.0, resultAlpha)) color.setAlphaF(resultAlpha) return color
class _RequestsDownloadAPI(QObject): """Download API based on requests.""" _sig_download_finished = Signal(str, str) _sig_download_progress = Signal(str, str, int, int) def __init__(self, load_rc_func=None): """Download API based on requests.""" super(QObject, self).__init__() self._conda_api = CondaAPI() self._queue = deque() self._threads = [] self._workers = [] self._timer = QTimer() self._load_rc_func = load_rc_func self._chunk_size = 1024 self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) @property def proxy_servers(self): """Return the proxy servers available from the conda rc config file.""" if self._load_rc_func is None: return {} else: return self._load_rc_func().get('proxy_servers', {}) def _clean(self): """Check for inactive workers and remove their references.""" if self._workers: for w in self._workers: if w.is_finished(): self._workers.remove(w) if self._threads: for t in self._threads: if t.isFinished(): self._threads.remove(t) else: self._timer.stop() def _start(self): """Start the next threaded worker in the queue.""" if len(self._queue) == 1: thread = self._queue.popleft() thread.start() self._timer.start() def _create_worker(self, method, *args, **kwargs): """Create a new worker instance.""" thread = QThread() worker = RequestsDownloadWorker(method, args, kwargs) worker.moveToThread(thread) worker.sig_finished.connect(self._start) self._sig_download_finished.connect(worker.sig_download_finished) self._sig_download_progress.connect(worker.sig_download_progress) worker.sig_finished.connect(thread.quit) thread.started.connect(worker.start) self._queue.append(thread) self._threads.append(thread) self._workers.append(worker) self._start() return worker def _download(self, url, path=None, force=False): """Callback for download.""" if path is None: path = url.split('/')[-1] # Make dir if non existent folder = os.path.dirname(os.path.abspath(path)) if not os.path.isdir(folder): os.makedirs(folder) # Start actual download try: r = requests.get(url, stream=True, proxies=self.proxy_servers) except Exception as error: print('ERROR', 'here', error) logger.error(str(error)) # Break if error found! # self._sig_download_finished.emit(url, path) # return path total_size = int(r.headers.get('Content-Length', 0)) # Check if file exists if os.path.isfile(path) and not force: file_size = os.path.getsize(path) # Check if existing file matches size of requested file if file_size == total_size: self._sig_download_finished.emit(url, path) return path # File not found or file size did not match. Download file. progress_size = 0 with open(path, 'wb') as f: for chunk in r.iter_content(chunk_size=self._chunk_size): if chunk: f.write(chunk) progress_size += len(chunk) self._sig_download_progress.emit(url, path, progress_size, total_size) self._sig_download_finished.emit(url, path) return path def _is_valid_url(self, url): """Callback for is_valid_url.""" try: r = requests.head(url, proxies=self.proxy_servers) value = r.status_code in [200] except Exception as error: logger.error(str(error)) value = False return value def _is_valid_channel(self, channel, conda_url='https://conda.anaconda.org'): """Callback for is_valid_channel.""" if channel.startswith('https://') or channel.startswith('http://'): url = channel else: url = "{0}/{1}".format(conda_url, channel) if url[-1] == '/': url = url[:-1] plat = self._conda_api.get_platform() repodata_url = "{0}/{1}/{2}".format(url, plat, 'repodata.json') try: r = requests.head(repodata_url, proxies=self.proxy_servers) value = r.status_code in [200] except Exception as error: logger.error(str(error)) value = False return value def _is_valid_api_url(self, url): """Callback for is_valid_api_url.""" # Check response is a JSON with ok: 1 data = {} try: r = requests.get(url, proxies=self.proxy_servers) content = to_text_string(r.content, encoding='utf-8') data = json.loads(content) except Exception as error: logger.error(str(error)) return data.get('ok', 0) == 1 # --- Public API # ------------------------------------------------------------------------- def download(self, url, path=None, force=False): """Download file given by url and save it to path.""" logger.debug(str((url, path, force))) method = self._download return self._create_worker(method, url, path=path, force=force) def terminate(self): """Terminate all workers and threads.""" for t in self._threads: t.quit() self._thread = [] self._workers = [] def is_valid_url(self, url, non_blocking=True): """Check if url is valid.""" logger.debug(str((url))) if non_blocking: method = self._is_valid_url return self._create_worker(method, url) else: return self._is_valid_url(url) def is_valid_api_url(self, url, non_blocking=True): """Check if anaconda api url is valid.""" logger.debug(str((url))) if non_blocking: method = self._is_valid_api_url return self._create_worker(method, url) else: return self._is_valid_api_url(url=url) def is_valid_channel(self, channel, conda_url='https://conda.anaconda.org', non_blocking=True): """Check if a conda channel is valid.""" logger.debug(str((channel, conda_url))) if non_blocking: method = self._is_valid_channel return self._create_worker(method, channel, conda_url) else: return self._is_valid_channel(channel, conda_url=conda_url) def get_api_info(self, url): """Query anaconda api info.""" data = {} try: r = requests.get(url, proxies=self.proxy_servers) content = to_text_string(r.content, encoding='utf-8') data = json.loads(content) if not data: data['api_url'] = url if 'conda_url' not in data: data['conda_url'] = 'https://conda.anaconda.org' except Exception as error: logger.error(str(error)) return data
class SiriusSpectrogramView(GraphicsLayoutWidget, PyDMWidget, PyDMColorMap, ReadingOrder): """ A SpectrogramView with support for Channels and more from PyDM. If there is no :attr:`channelWidth` it is possible to define the width of the image with the :attr:`width` property. The :attr:`normalizeData` property defines if the colors of the images are relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to the minimum and maximum values of the image. Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new image is rendered in the widget. Parameters ---------- parent : QWidget The parent widget for the Label image_channel : str, optional The channel to be used by the widget for the image data. xaxis_channel : str, optional The channel to be used by the widget to receive the image width (if ReadingOrder == Clike), and to set the xaxis values yaxis_channel : str, optional The channel to be used by the widget to receive the image width (if ReadingOrder == Fortranlike), and to set the yaxis values background : QColor, optional QColor to set the background color of the GraphicsView """ Q_ENUMS(PyDMColorMap) Q_ENUMS(ReadingOrder) color_maps = cmaps def __init__(self, parent=None, image_channel=None, xaxis_channel=None, yaxis_channel=None, roioffsetx_channel=None, roioffsety_channel=None, roiwidth_channel=None, roiheight_channel=None, title='', background='w', image_width=0, image_height=0): """Initialize widget.""" GraphicsLayoutWidget.__init__(self, parent) PyDMWidget.__init__(self) self.thread = None self._imagechannel = None self._xaxischannel = None self._yaxischannel = None self._roioffsetxchannel = None self._roioffsetychannel = None self._roiwidthchannel = None self._roiheightchannel = None self._channels = 7 * [ None, ] self.image_waveform = np.zeros(0) self._image_width = image_width if not xaxis_channel else 0 self._image_height = image_height if not yaxis_channel else 0 self._roi_offsetx = 0 self._roi_offsety = 0 self._roi_width = 0 self._roi_height = 0 self._normalize_data = False self._auto_downsample = True self._last_yaxis_data = None self._last_xaxis_data = None self._auto_colorbar_lims = True self.format_tooltip = '{0:.4g}, {1:.4g}' # ViewBox and imageItem. self._view = ViewBox() self._image_item = ImageItem() self._view.addItem(self._image_item) # ROI self.ROICurve = PlotCurveItem([0, 0, 0, 0, 0], [0, 0, 0, 0, 0]) self.ROIColor = QColor('red') pen = mkPen() pen.setColor(QColor('transparent')) pen.setWidth(1) self.ROICurve.setPen(pen) self._view.addItem(self.ROICurve) # Axis. self.xaxis = AxisItem('bottom') self.xaxis.setPen(QColor(0, 0, 0)) if not xaxis_channel: self.xaxis.setVisible(False) self.yaxis = AxisItem('left') self.yaxis.setPen(QColor(0, 0, 0)) if not yaxis_channel: self.yaxis.setVisible(False) # Colorbar legend. self.colorbar = _GradientLegend() # Title. start_row = 0 if title: self.title = LabelItem(text=title, color='#000000') self.addItem(self.title, 0, 0, 1, 3) start_row = 1 # Set layout. self.addItem(self._view, start_row, 1) self.addItem(self.yaxis, start_row, 0) self.addItem(self.colorbar, start_row, 2) self.addItem(self.xaxis, start_row + 1, 1) self.setBackground(background) self.ci.layout.setColumnSpacing(0, 0) self.ci.layout.setRowSpacing(start_row, 0) # Set color map limits. self.cm_min = 0.0 self.cm_max = 255.0 # Set default reading order of numpy array data to Clike. self._reading_order = ReadingOrder.Clike # Make a right-click menu for changing the color map. self.cm_group = QActionGroup(self) self.cmap_for_action = {} for cm in self.color_maps: action = self.cm_group.addAction(cmap_names[cm]) action.setCheckable(True) self.cmap_for_action[action] = cm # Set the default colormap. self._cm_colors = None self.colorMap = PyDMColorMap.Inferno # Setup the redraw timer. self.needs_redraw = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawImage) self._redraw_rate = 30 self.maxRedrawRate = self._redraw_rate self.newImageSignal = self._image_item.sigImageChanged # Set Channels. self.imageChannel = image_channel self.xAxisChannel = xaxis_channel self.yAxisChannel = yaxis_channel self.ROIOffsetXChannel = roioffsetx_channel self.ROIOffsetYChannel = roioffsety_channel self.ROIWidthChannel = roiwidth_channel self.ROIHeightChannel = roiheight_channel # --- Context menu --- def widget_ctx_menu(self): """ Fetch the Widget specific context menu. It will be populated with additional tools by `assemble_tools_menu`. Returns ------- QMenu or None If the return of this method is None a new QMenu will be created by `assemble_tools_menu`. """ self.menu = ViewBoxMenu(self._view) cm_menu = self.menu.addMenu("Color Map") for act in self.cmap_for_action.keys(): cm_menu.addAction(act) cm_menu.triggered.connect(self._changeColorMap) return self.menu # --- Colormap methods --- def _changeColorMap(self, action): """ Method invoked by the colormap Action Menu. Changes the current colormap used to render the image. Parameters ---------- action : QAction """ self.colorMap = self.cmap_for_action[action] @Property(float) def colorMapMin(self): """ Minimum value for the colormap. Returns ------- float """ return self.cm_min @colorMapMin.setter @Slot(float) def colorMapMin(self, new_min): """ Set the minimum value for the colormap. Parameters ---------- new_min : float """ if self.cm_min != new_min: self.cm_min = new_min if self.cm_min > self.cm_max: self.cm_max = self.cm_min @Property(float) def colorMapMax(self): """ Maximum value for the colormap. Returns ------- float """ return self.cm_max @colorMapMax.setter @Slot(float) def colorMapMax(self, new_max): """ Set the maximum value for the colormap. Parameters ---------- new_max : float """ if self.cm_max != new_max: self.cm_max = new_max if self.cm_max < self.cm_min: self.cm_min = self.cm_max def setColorMapLimits(self, mn, mx): """ Set the limit values for the colormap. Parameters ---------- mn : int The lower limit mx : int The upper limit """ if mn >= mx: return self.cm_max = mx self.cm_min = mn @Property(PyDMColorMap) def colorMap(self): """ Return the color map used by the SpectrogramView. Returns ------- PyDMColorMap """ return self._colormap @colorMap.setter def colorMap(self, new_cmap): """ Set the color map used by the SpectrogramView. Parameters ------- new_cmap : PyDMColorMap """ self._colormap = new_cmap self._cm_colors = self.color_maps[new_cmap] self.setColorMap() for action in self.cm_group.actions(): if self.cmap_for_action[action] == self._colormap: action.setChecked(True) else: action.setChecked(False) def setColorMap(self, cmap=None): """ Update the image colormap. Parameters ---------- cmap : ColorMap """ if not cmap: if not self._cm_colors.any(): return # Take default values pos = np.linspace(0.0, 1.0, num=len(self._cm_colors)) cmap = ColorMap(pos, self._cm_colors) self._view.setBackgroundColor(cmap.map(0)) lut = cmap.getLookupTable(0.0, 1.0, alpha=False) self.colorbar.setIntColorScale(colors=lut) self._image_item.setLookupTable(lut) # --- Connection Slots --- @Slot(bool) def image_connection_state_changed(self, conn): """ Callback invoked when the Image Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ if conn: self.redraw_timer.start() else: self.redraw_timer.stop() @Slot(bool) def yaxis_connection_state_changed(self, connected): """ Callback invoked when the TimeAxis Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ self._timeaxis_connected = connected @Slot(bool) def roioffsetx_connection_state_changed(self, conn): """ Run when the ROIOffsetX Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_offsetx = 0 @Slot(bool) def roioffsety_connection_state_changed(self, conn): """ Run when the ROIOffsetY Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_offsety = 0 @Slot(bool) def roiwidth_connection_state_changed(self, conn): """ Run when the ROIWidth Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_width = 0 @Slot(bool) def roiheight_connection_state_changed(self, conn): """ Run when the ROIHeight Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_height = 0 # --- Value Slots --- @Slot(np.ndarray) def image_value_changed(self, new_image): """ Callback invoked when the Image Channel value is changed. We try to do as little as possible in this method, because it gets called every time the image channel updates, which might be extremely often. Basically just store the data, and set a flag requesting that the image be redrawn. Parameters ---------- new_image : np.ndarray The new image data. This can be a flat 1D array, or a 2D array. """ if new_image is None or new_image.size == 0: return logging.debug("SpectrogramView Received New Image: Needs Redraw->True") self.image_waveform = new_image self.needs_redraw = True if not self._image_height and self._image_width: self._image_height = new_image.size / self._image_width elif not self._image_width and self._image_height: self._image_width = new_image.size / self._image_height @Slot(np.ndarray) @Slot(float) def xaxis_value_changed(self, new_array): """ Callback invoked when the Image Width Channel value is changed. Parameters ---------- new_array : np.ndarray The new x axis array """ if new_array is None: return if isinstance(new_array, float): new_array = np.array([ new_array, ]) self._last_xaxis_data = new_array if self._reading_order == self.Clike: self._image_width = new_array.size else: self._image_height = new_array.size self.needs_redraw = True @Slot(np.ndarray) @Slot(float) def yaxis_value_changed(self, new_array): """ Callback invoked when the TimeAxis Channel value is changed. Parameters ---------- new_array : np.array The new y axis array """ if new_array is None: return if isinstance(new_array, float): new_array = np.array([ new_array, ]) self._last_yaxis_data = new_array if self._reading_order == self.Fortranlike: self._image_width = new_array.size else: self._image_height = new_array.size self.needs_redraw = True @Slot(int) def roioffsetx_value_changed(self, new_offset): """ Run when the ROIOffsetX Channel value changes. Parameters ---------- new_offsetx : int The new image ROI horizontal offset """ if new_offset is None: return self._roi_offsetx = new_offset self.redrawROI() @Slot(int) def roioffsety_value_changed(self, new_offset): """ Run when the ROIOffsetY Channel value changes. Parameters ---------- new_offsety : int The new image ROI vertical offset """ if new_offset is None: return self._roi_offsety = new_offset self.redrawROI() @Slot(int) def roiwidth_value_changed(self, new_width): """ Run when the ROIWidth Channel value changes. Parameters ---------- new_width : int The new image ROI width """ if new_width is None: return self._roi_width = int(new_width) self.redrawROI() @Slot(int) def roiheight_value_changed(self, new_height): """ Run when the ROIHeight Channel value changes. Parameters ---------- new_height : int The new image ROI height """ if new_height is None: return self._roi_height = int(new_height) self.redrawROI() # --- Image update methods --- def process_image(self, image): """ Boilerplate method. To be used by applications in order to add calculations and also modify the image before it is displayed at the widget. .. warning:: This code runs in a separated QThread so it **MUST** not try to write to QWidgets. Parameters ---------- image : np.ndarray The Image Data as a 2D numpy array Returns ------- np.ndarray The Image Data as a 2D numpy array after processing. """ return image def redrawImage(self): """ Set the image data into the ImageItem, if needed. If necessary, reshape the image to 2D first. """ if self.thread is not None and not self.thread.isFinished(): logger.warning( "Image processing has taken longer than the refresh rate.") return self.thread = SpectrogramUpdateThread(self) self.thread.updateSignal.connect(self._updateDisplay) logging.debug("SpectrogramView RedrawImage Thread Launched") self.thread.start() @Slot(list) def _updateDisplay(self, data): logging.debug("SpectrogramView Update Display with new image") # Update axis if self._last_xaxis_data is not None: szx = self._last_xaxis_data.size xMin = self._last_xaxis_data.min() xMax = self._last_xaxis_data.max() else: szx = self.imageWidth if self.readingOrder == self.Clike \ else self.imageHeight xMin = 0 xMax = szx if self._last_yaxis_data is not None: szy = self._last_yaxis_data.size yMin = self._last_yaxis_data.min() yMax = self._last_yaxis_data.max() else: szy = self.imageHeight if self.readingOrder == self.Clike \ else self.imageWidth yMin = 0 yMax = szy self.xaxis.setRange(xMin, xMax) self.yaxis.setRange(yMin, yMax) self._view.setLimits(xMin=0, xMax=szx, yMin=0, yMax=szy, minXRange=szx, maxXRange=szx, minYRange=szy, maxYRange=szy) # Update image if self.autoSetColorbarLims: self.colorbar.setLimits(data) mini, maxi = data[0], data[1] img = data[2] self._image_item.setLevels([mini, maxi]) self._image_item.setImage(img, autoLevels=False, autoDownsample=self.autoDownsample) # ROI update methods def redrawROI(self): startx = self._roi_offsetx endx = self._roi_offsetx + self._roi_width starty = self._roi_offsety endy = self._roi_offsety + self._roi_height self.ROICurve.setData([startx, startx, endx, endx, startx], [starty, endy, endy, starty, starty]) def showROI(self, show): """Set ROI visibility.""" pen = mkPen() if show: pen.setColor(self.ROIColor) else: pen.setColor(QColor('transparent')) self.ROICurve.setPen(pen) # --- Properties --- @Property(bool) def autoDownsample(self): """ Return if we should or not apply the autoDownsample option. Return ------ bool """ return self._auto_downsample @autoDownsample.setter def autoDownsample(self, new_value): """ Whether we should or not apply the autoDownsample option. Parameters ---------- new_value: bool """ if new_value != self._auto_downsample: self._auto_downsample = new_value @Property(bool) def autoSetColorbarLims(self): """ Return if we should or not auto set colorbar limits. Return ------ bool """ return self._auto_colorbar_lims @autoSetColorbarLims.setter def autoSetColorbarLims(self, new_value): """ Whether we should or not auto set colorbar limits. Parameters ---------- new_value: bool """ if new_value != self._auto_colorbar_lims: self._auto_colorbar_lims = new_value @Property(int) def imageWidth(self): """ Return the width of the image. Return ------ int """ return self._image_width @imageWidth.setter def imageWidth(self, new_width): """ Set the width of the image. Can be overridden by :attr:`xAxisChannel` and :attr:`yAxisChannel`. Parameters ---------- new_width: int """ boo = self._image_width != int(new_width) boo &= not self._xaxischannel boo &= not self._yaxischannel if boo: self._image_width = int(new_width) @Property(int) def imageHeight(self): """ Return the height of the image. Return ------ int """ return self._image_height @Property(int) def ROIOffsetX(self): """ Return the ROI offset in X axis in pixels. Return ------ int """ return self._roi_offsetx @ROIOffsetX.setter def ROIOffsetX(self, new_offset): """ Set the ROI offset in X axis in pixels. Can be overridden by :attr:`ROIOffsetXChannel`. Parameters ---------- new_offset: int """ if new_offset is None: return boo = self._roi_offsetx != int(new_offset) boo &= not self._roioffsetxchannel if boo: self._roi_offsetx = int(new_offset) self.redrawROI() @Property(int) def ROIOffsetY(self): """ Return the ROI offset in Y axis in pixels. Return ------ int """ return self._roi_offsety @ROIOffsetY.setter def ROIOffsetY(self, new_offset): """ Set the ROI offset in Y axis in pixels. Can be overridden by :attr:`ROIOffsetYChannel`. Parameters ---------- new_offset: int """ if new_offset is None: return boo = self._roi_offsety != int(new_offset) boo &= not self._roioffsetychannel if boo: self._roi_offsety = int(new_offset) self.redrawROI() @Property(int) def ROIWidth(self): """ Return the ROI width in pixels. Return ------ int """ return self._roi_width @ROIWidth.setter def ROIWidth(self, new_width): """ Set the ROI width in pixels. Can be overridden by :attr:`ROIWidthChannel`. Parameters ---------- new_width: int """ if new_width is None: return boo = self._roi_width != int(new_width) boo &= not self._roiwidthchannel if boo: self._roi_width = int(new_width) self.redrawROI() @Property(int) def ROIHeight(self): """ Return the ROI height in pixels. Return ------ int """ return self._roi_height @ROIHeight.setter def ROIHeight(self, new_height): """ Set the ROI height in pixels. Can be overridden by :attr:`ROIHeightChannel`. Parameters ---------- new_height: int """ if new_height is None: return boo = self._roi_height != int(new_height) boo &= not self._roiheightchannel if boo: self._roi_height = int(new_height) self.redrawROI() @Property(bool) def normalizeData(self): """ Return True if the colors are relative to data maximum and minimum. Returns ------- bool """ return self._normalize_data @normalizeData.setter @Slot(bool) def normalizeData(self, new_norm): """ Define if the colors are relative to minimum and maximum of the data. Parameters ---------- new_norm: bool """ if self._normalize_data != new_norm: self._normalize_data = new_norm @Property(ReadingOrder) def readingOrder(self): """ Return the reading order of the :attr:`imageChannel` array. Returns ------- ReadingOrder """ return self._reading_order @readingOrder.setter def readingOrder(self, order): """ Set reading order of the :attr:`imageChannel` array. Parameters ---------- order: ReadingOrder """ if self._reading_order != order: self._reading_order = order if order == self.Clike: if self._last_xaxis_data is not None: self._image_width = self._last_xaxis_data.size if self._last_yaxis_data is not None: self._image_height = self._last_yaxis_data.size elif order == self.Fortranlike: if self._last_yaxis_data is not None: self._image_width = self._last_yaxis_data.size if self._last_xaxis_data is not None: self._image_height = self._last_xaxis_data.size @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000)) # --- Events rederivations --- def keyPressEvent(self, ev): """Handle keypress events.""" return def mouseMoveEvent(self, ev): if not self._image_item.width() or not self._image_item.height(): super().mouseMoveEvent(ev) return pos = ev.pos() posaux = self._image_item.mapFromDevice(ev.pos()) if posaux.x() < 0 or posaux.x() >= self._image_item.width() or \ posaux.y() < 0 or posaux.y() >= self._image_item.height(): super().mouseMoveEvent(ev) return pos_scene = self._view.mapSceneToView(pos) x = round(pos_scene.x()) y = round(pos_scene.y()) if self.xAxisChannel and self._last_xaxis_data is not None: maxx = len(self._last_xaxis_data) - 1 x = x if x < maxx else maxx valx = self._last_xaxis_data[x] else: valx = x if self.yAxisChannel and self._last_yaxis_data is not None: maxy = len(self._last_yaxis_data) - 1 y = y if y < maxy else maxy valy = self._last_yaxis_data[y] else: valy = y txt = self.format_tooltip.format(valx, valy) QToolTip.showText(self.mapToGlobal(pos), txt, self, self.geometry(), 5000) super().mouseMoveEvent(ev) # --- Channels --- @Property(str) def imageChannel(self): """ The channel address in use for the image data . Returns ------- str Channel address """ if self._imagechannel: return str(self._imagechannel.address) else: return '' @imageChannel.setter def imageChannel(self, value): """ The channel address in use for the image data . Parameters ---------- value : str Channel address """ if self._imagechannel != value: # Disconnect old channel if self._imagechannel: self._imagechannel.disconnect() # Create and connect new channel self._imagechannel = PyDMChannel( address=value, connection_slot=self.image_connection_state_changed, value_slot=self.image_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[0] = self._imagechannel self._imagechannel.connect() @Property(str) def xAxisChannel(self): """ The channel address in use for the x-axis of image. Returns ------- str Channel address """ if self._xaxischannel: return str(self._xaxischannel.address) else: return '' @xAxisChannel.setter def xAxisChannel(self, value): """ The channel address in use for the x-axis of image. Parameters ---------- value : str Channel address """ if self._xaxischannel != value: # Disconnect old channel if self._xaxischannel: self._xaxischannel.disconnect() # Create and connect new channel self._xaxischannel = PyDMChannel( address=value, connection_slot=self.connectionStateChanged, value_slot=self.xaxis_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[1] = self._xaxischannel self._xaxischannel.connect() @Property(str) def yAxisChannel(self): """ The channel address in use for the time axis. Returns ------- str Channel address """ if self._yaxischannel: return str(self._yaxischannel.address) else: return '' @yAxisChannel.setter def yAxisChannel(self, value): """ The channel address in use for the time axis. Parameters ---------- value : str Channel address """ if self._yaxischannel != value: # Disconnect old channel if self._yaxischannel: self._yaxischannel.disconnect() # Create and connect new channel self._yaxischannel = PyDMChannel( address=value, connection_slot=self.yaxis_connection_state_changed, value_slot=self.yaxis_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[2] = self._yaxischannel self._yaxischannel.connect() @Property(str) def ROIOffsetXChannel(self): """ Return the channel address in use for the image ROI horizontal offset. Returns ------- str Channel address """ if self._roioffsetxchannel: return str(self._roioffsetxchannel.address) else: return '' @ROIOffsetXChannel.setter def ROIOffsetXChannel(self, value): """ Return the channel address in use for the image ROI horizontal offset. Parameters ---------- value : str Channel address """ if self._roioffsetxchannel != value: # Disconnect old channel if self._roioffsetxchannel: self._roioffsetxchannel.disconnect() # Create and connect new channel self._roioffsetxchannel = PyDMChannel( address=value, connection_slot=self.roioffsetx_connection_state_changed, value_slot=self.roioffsetx_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[3] = self._roioffsetxchannel self._roioffsetxchannel.connect() @Property(str) def ROIOffsetYChannel(self): """ Return the channel address in use for the image ROI vertical offset. Returns ------- str Channel address """ if self._roioffsetychannel: return str(self._roioffsetychannel.address) else: return '' @ROIOffsetYChannel.setter def ROIOffsetYChannel(self, value): """ Return the channel address in use for the image ROI vertical offset. Parameters ---------- value : str Channel address """ if self._roioffsetychannel != value: # Disconnect old channel if self._roioffsetychannel: self._roioffsetychannel.disconnect() # Create and connect new channel self._roioffsetychannel = PyDMChannel( address=value, connection_slot=self.roioffsety_connection_state_changed, value_slot=self.roioffsety_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[4] = self._roioffsetychannel self._roioffsetychannel.connect() @Property(str) def ROIWidthChannel(self): """ Return the channel address in use for the image ROI width. Returns ------- str Channel address """ if self._roiwidthchannel: return str(self._roiwidthchannel.address) else: return '' @ROIWidthChannel.setter def ROIWidthChannel(self, value): """ Return the channel address in use for the image ROI width. Parameters ---------- value : str Channel address """ if self._roiwidthchannel != value: # Disconnect old channel if self._roiwidthchannel: self._roiwidthchannel.disconnect() # Create and connect new channel self._roiwidthchannel = PyDMChannel( address=value, connection_slot=self.roiwidth_connection_state_changed, value_slot=self.roiwidth_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[5] = self._roiwidthchannel self._roiwidthchannel.connect() @Property(str) def ROIHeightChannel(self): """ Return the channel address in use for the image ROI height. Returns ------- str Channel address """ if self._roiheightchannel: return str(self._roiheightchannel.address) else: return '' @ROIHeightChannel.setter def ROIHeightChannel(self, value): """ Return the channel address in use for the image ROI height. Parameters ---------- value : str Channel address """ if self._roiheightchannel != value: # Disconnect old channel if self._roiheightchannel: self._roiheightchannel.disconnect() # Create and connect new channel self._roiheightchannel = PyDMChannel( address=value, connection_slot=self.roiheight_connection_state_changed, value_slot=self.roiheight_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[6] = self._roiheightchannel self._roiheightchannel.connect() def channels(self): """ Return the channels being used for this Widget. Returns ------- channels : list List of PyDMChannel objects """ return self._channels def channels_for_tools(self): """Return channels for tools.""" return [self._imagechannel]
class ProcessWorker(QObject): """Conda worker based on a QProcess for non blocking UI.""" sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs=None): """Conda worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. parse : bool (optional) Parse json from output. pip : bool (optional) Define as a pip command. callback : func (optional) If the process has a callback to process output from comd_list. extra_kwargs : dict Arguments for the callback. """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs if extra_kwargs else {} self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda' not in stderr.lower(): if stderr.strip() and self._conda: logger.error('{0}:\nSTDERR:\n{1}\nEND'.format( ' '.join(self._cmd_list), stderr)) elif stderr.strip() and self._pip: logger.error("pip error: {}".format(self._cmd_list)) result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except Exception as error: result = stdout, str(error) if 'error' in result[0]: if not isinstance(result[0], dict): result = {'error': str(result[0])}, None error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def start(self): """Start process.""" logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
def create_app(datafiles=[], data_configs=[], data_configs_show=False, interactive=True): """ Create and initialize a cubeviz application instance Parameters ---------- datafiles : `list` A list of filenames representing data files to be loaded data_configs : `list` A list of filenames representing data configuration files to be used data_configs_show : `bool` Display matching info about data configuration files interactive : `bool` Flag to indicate whether session is interactive or not (i.e. for testing) """ app = get_qapp() # Splash screen if interactive: splash = get_splash() splash.image = QtGui.QPixmap(CUBEVIZ_LOGO_PATH) splash.show() else: splash = None # Start off by loading plugins. We need to do this before restoring # the session or loading the configuration since these may use existing # plugins. load_plugins(splash=splash) dfc_kwargs = dict(remove_defaults=True, check_ifu_valid=interactive) DataFactoryConfiguration(data_configs, data_configs_show, **dfc_kwargs) # Check to make sure each file exists and raise an Exception # that will show in the popup if it does not exist. _check_datafiles_exist(datafiles) # Show the splash screen for 1 second if interactive: timer = QTimer() timer.setInterval(1000) timer.setSingleShot(True) timer.timeout.connect(splash.close) timer.start() data_collection = glue.core.DataCollection() hub = data_collection.hub ga = _create_glue_app(data_collection, hub) ga.run_startup_action('cubeviz') # Load the data files. if datafiles: datasets = load_data_files(datafiles) ga.add_datasets(data_collection, datasets, auto_merge=False) if interactive: splash.set_progress(100) return ga