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())
class DelayJobRunner(object): """ Utility class for running job after a certain delay. If a new request is made during this delay, the previous request is dropped and the timer is restarted for the new request. We use this to implement a cooldown effect that prevents jobs from being executed while the IDE is not idle. A job is a simple callable. """ def __init__(self, delay=500): """ :param delay: Delay to wait before running the job. This delay applies to all requests and cannot be changed afterwards. """ self._timer = QTimer() self.delay = delay self._timer.timeout.connect(self._exec_requested_job) self._args = [] self._kwargs = {} self._job = lambda x: None def request_job(self, job, *args, **kwargs): """ Request a job execution. The job will be executed after the delay specified in the DelayJobRunner contructor elapsed if no other job is requested until then. :param job: job. :type job: callable :param args: job's position arguments :param kwargs: job's keyworded arguments """ self.cancel_requests() self._job = job self._args = args self._kwargs = kwargs self._timer.start(self.delay) def cancel_requests(self): """Cancels pending requests.""" self._timer.stop() self._job = None self._args = None self._kwargs = None def _exec_requested_job(self): """Execute the requested job after the timer has timeout.""" self._timer.stop() self._job(*self._args, **self._kwargs)
class EventEngine(object): """ 事件驱动引擎 事件驱动引擎中所有的变量都设置为了私有,这是为了防止不小心 从外部修改了这些变量的值或状态,导致bug。 变量说明 __queue:私有变量,事件队列 __active:私有变量,事件引擎开关 __thread:私有变量,事件处理线程 __timer:私有变量,计时器 __handlers:私有变量,事件处理函数字典 方法说明 __run: 私有方法,事件处理线程连续运行用 __process: 私有方法,处理事件,调用注册在引擎中的监听函数 __onTimer:私有方法,计时器固定事件间隔触发后,向事件队列中存入计时器事件 start: 公共方法,启动引擎 stop:公共方法,停止引擎 register:公共方法,向引擎中注册监听函数 unregister:公共方法,向引擎中注销监听函数 put:公共方法,向事件队列中存入新的事件 事件监听函数必须定义为输入参数仅为一个event对象,即: 函数 def func(event) ... 对象方法 def method(self, event) ... """ #---------------------------------------------------------------------- def __init__(self): """初始化事件引擎""" # 事件队列 self.__queue = Queue() # 事件引擎开关 self.__active = False # 事件处理线程 self.__thread = Thread(target=self.__run) # 计时器,用于触发计时器事件 self.__timer = QTimer() self.__timer.timeout.connect(self.__onTimer) # 这里的__handlers是一个字典,用来保存对应的事件调用关系 # 其中每个键对应的值是一个列表,列表中保存了对该事件进行监听的函数功能 self.__handlers = defaultdict(list) # __generalHandlers是一个列表,用来保存通用回调函数(所有事件均调用) self.__generalHandlers = [] #---------------------------------------------------------------------- def __run(self): """引擎运行""" while self.__active == True: try: event = self.__queue.get(block=True, timeout=1) # 获取事件的阻塞时间设为1秒 self.__process(event) except Empty: pass #---------------------------------------------------------------------- def __process(self, event): """处理事件""" # 检查是否存在对该事件进行监听的处理函数 if event.type_ in self.__handlers: # 若存在,则按顺序将事件传递给处理函数执行 [handler(event) for handler in self.__handlers[event.type_]] # 以上语句为Python列表解析方式的写法,对应的常规循环写法为: #for handler in self.__handlers[event.type_]: #handler(event) # 调用通用处理函数进行处理 if self.__generalHandlers: [handler(event) for handler in self.__generalHandlers] #---------------------------------------------------------------------- def __onTimer(self): """向事件队列中存入计时器事件""" # 创建计时器事件 event = Event(type_=EVENT_TIMER) # 向队列中存入计时器事件 self.put(event) #---------------------------------------------------------------------- def start(self, timer=True): """ 引擎启动 timer:是否要启动计时器 """ # 将引擎设为启动 self.__active = True # 启动事件处理线程 self.__thread.start() # 启动计时器,计时器事件间隔默认设定为1秒 if timer: self.__timer.start(1000) #---------------------------------------------------------------------- def stop(self): """停止引擎""" # 将引擎设为停止 self.__active = False # 停止计时器 self.__timer.stop() # 等待事件处理线程退出 self.__thread.join() #---------------------------------------------------------------------- def register(self, type_, handler): """注册事件处理函数监听""" # 尝试获取该事件类型对应的处理函数列表,若无defaultDict会自动创建新的list handlerList = self.__handlers[type_] # 若要注册的处理器不在该事件的处理器列表中,则注册该事件 if handler not in handlerList: handlerList.append(handler) #---------------------------------------------------------------------- """注销事件处理函数监听""" # 尝试获取该事件类型对应的处理函数列表,若无则忽略该次注销请求 handlerList = self.__handlers[type_] # 如果该函数存在于列表中,则移除 if handler in handlerList: handlerList.remove(handler) # 如果函数列表为空,则从引擎中移除该事件类型 if not handlerList: del self.__handlers[type_] #---------------------------------------------------------------------- def put(self, event): """向事件队列中存入事件""" self.__queue.put(event) #---------------------------------------------------------------------- def registerGeneralHandler(self, handler): """注册通用事件处理函数监听""" if handler not in self.__generalHandlers: self.__generalHandlers.append(handler) #---------------------------------------------------------------------- def unregisterGeneralHandler(self, handler): """注销通用事件处理函数监听""" if handler in self.__generalHandlers: self.__generalHandlers.remove(handler)
class ChronoTimer(QObject): def __init__(self, parent, duration=None): """ :param parent: :param Nteams: :param duration: (dict) containing optional keys : days, minutes, seconds, hours, weeks, milliseconds, microseconds """ super().__init__() self.area = parent if duration is None: self.type = 'chrono' self.duration = timedelta() else: self.type = 'timer' self.duration = timedelta(**duration) # seconds self.displayed_time = 0 # in seconds self.started = False self.timer = QTimer() self.timer.setInterval(100) self.timer.timeout.connect(self.display_time) self.setup_ui() def setup_ui(self): self.dock_chrono_timer = Dock(self.type.capitalize()) self.area.addDock(self.dock_chrono_timer) self.dock_chrono_timer.float() widget_chrono_timer = QtWidgets.QWidget() self.dock_chrono_timer.addWidget(widget_chrono_timer) self.layout_lcd = QtWidgets.QVBoxLayout() widget_chrono_timer.setLayout(self.layout_lcd) self.dock_chrono_timer.setAutoFillBackground(True) palette = self.dock_chrono_timer.palette() palette.setColor(palette.Background, QtGui.QColor(0, 0, 0)) self.dock_chrono_timer.setPalette(palette) self.time_lcd = QtWidgets.QLCDNumber(8) self.set_lcd_color(self.time_lcd, 'red') self.layout_lcd.addWidget(self.time_lcd) hours, minutes, seconds = self.get_times(self.duration) self.time_lcd.display('{:02d}:{:02d}:{:02d}'.format( hours, minutes, seconds)) self.dock_controls = Dock('Chrono/Timer Controls') self.area.addDock(self.dock_controls) self.dock_controls.setOrientation('vertical', True) self.dock_controls.setMaximumHeight(150) self.widget_controls = QtWidgets.QWidget() self.controls_layout = QtWidgets.QVBoxLayout() self.widget_controls.setLayout(self.controls_layout) hor_layout = QtWidgets.QHBoxLayout() hor_widget = QtWidgets.QWidget() hor_widget.setLayout(hor_layout) self.controls_layout.addWidget(hor_widget) icon = QtGui.QIcon() icon.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/run2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.start_pb = PushButtonShortcut(icon, 'Start', shortcut='Home', shortcut_widget=self.area) self.start_pb.clicked.connect(self.start) self.start_pb.setToolTip('home ("début") key shorcut') hor_layout.addWidget(self.start_pb) icon = QtGui.QIcon() icon.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.pause_pb = PushButtonShortcut(icon, 'Pause', shortcut='Ctrl+p', shortcut_widget=self.area) self.pause_pb.setCheckable(True) self.pause_pb.setToolTip("Ctrl+p key shortcut") self.pause_pb.clicked.connect(self.pause) hor_layout.addWidget(self.pause_pb) icon = QtGui.QIcon() icon.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/Refresh2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.reset_pb = PushButtonShortcut(icon, 'Reset', shortcut='F5', shortcut_widget=self.area) self.reset_pb.setToolTip('F5 key shortcut') self.reset_pb.clicked.connect(self.reset) hor_layout.addWidget(self.reset_pb) self.dock_controls.addWidget(self.widget_controls) def get_times(self, duration): seconds = int(duration.total_seconds() % 60) total_minutes = duration.total_seconds() // 60 minutes = int(total_minutes % 60) hours = int(total_minutes // 60) return hours, minutes, seconds def get_elapsed_time(self): return time.perf_counter() - self.ini_time def display_time(self): elapsed_time = self.get_elapsed_time() if self.type == 'timer': display_timedelta = self.duration - timedelta(seconds=elapsed_time) else: display_timedelta = self.duration + timedelta(seconds=elapsed_time) self.displayed_time = display_timedelta.total_seconds() if display_timedelta.total_seconds() <= 0: self.reset() return else: hours, minutes, seconds = self.get_times(display_timedelta) self.time_lcd.display('{:02d}:{:02d}:{:02d}'.format( hours, minutes, seconds)) QtWidgets.QApplication.processEvents() def start(self): self.ini_time = time.perf_counter() self.timer.start() self.started = True self.start_pb.setEnabled(False) def pause(self): if self.pause_pb.isChecked(): self.started = False self.timer.stop() self.paused_time = time.perf_counter() else: elapsed_pause_time = time.perf_counter() - self.paused_time self.ini_time += elapsed_pause_time self.timer.start() self.started = True def reset(self): if self.pause_pb.isChecked(): self.pause_pb.setChecked(False) QtWidgets.QApplication.processEvents() self.timer.stop() self.start_pb.setEnabled(True) self.started = False hours, minutes, seconds = self.get_times(self.duration) self.time_lcd.display('{:02d}:{:02d}:{:02d}'.format( hours, minutes, seconds)) def set_lcd_color(self, lcd, color): palette = lcd.palette() # lcd.setPalette(QtGui.QPalette(Qt.red)) if hasattr(Qt, color): palette.setBrush(palette.WindowText, getattr(Qt, color)) palette.setColor(palette.Background, QtGui.QColor(0, 0, 0)) lcd.setPalette(palette)
class _ClientAPI(QObject): """Anaconda Client API wrapper.""" def __init__(self): """Anaconda Client API wrapper.""" 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): """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): """Take avalaible worker from the queue and start it.""" 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 @staticmethod def _load_repodata(filepaths, extra_data=None, metadata=None): """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. """ extra_data = extra_data if extra_data else {} metadata = metadata if metadata else {} 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'): all_packages[name]['type'][version] = data.get('type') all_packages[name]['app_entry'][version] = data.get( 'app_entry') all_packages[name]['app_type'][version] = data.get( 'app_type') 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') # 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 @staticmethod def _prepare_model_data(packages, linked, pip=None, private_packages=None): """Prepare model data for the packages table model.""" pip = pip if pip else [] private_packages = private_packages if private_packages else {} data = [] if private_packages is not None: for pkg in private_packages: if pkg in packages: p_data = packages.get(pkg) 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) 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 load_repodata(self, filepaths, extra_data=None, metadata=None): """ Load all the available pacakges information for downloaded repodata. Files include 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=None, private_packages=None): """Prepare downloaded package info along with pip pacakges info.""" logger.debug('') return self._prepare_model_data(packages, linked, pip=pip, private_packages=private_packages) def set_domain(self, domain='https://api.anaconda.org'): """Reset current api domain.""" 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() @staticmethod def store_token(token): """Store authentication user token.""" class Args: """Enum.""" site = None binstar_client.utils.store_token(token, Args) @staticmethod def remove_token(): """Remove authentication user token.""" class Args: """Enum.""" site = None binstar_client.utils.remove_token(Args) def user(self): """Return current logged user information.""" try: user = self._anaconda_client_api.user() except Exception: user = {} return user def domain(self): """Return current domain.""" return self._anaconda_client_api.domain def packages(self, login=None, platform=None, package_type=None, type_=None, access=None): """Return all the available packages for a given user. Parameters ---------- type_: Optional[str] Only find packages that have this conda `type`, (i.e. 'app'). access : Optional[str] Only find packages that have this access level (e.g. 'private', 'authenticated', 'public'). """ 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): """Return the private packages for a given set of usernames/logins.""" 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): """Return 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) @staticmethod def load_token(url): """Load saved token for a given url api site.""" token = binstar_client.utils.load_token(url) return token @staticmethod def get_api_url(): """Get the anaconda client url configuration.""" return get_config().get('url', 'https://api.anaconda.org') @staticmethod def set_api_url(url): """Set the anaconda client url configuration.""" data = get_config() data['url'] = url set_config(data)
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 AutosaveForPlugin(object): """Component of editor plugin implementing autosave functionality.""" # Interval (in ms) between two autosaves DEFAULT_AUTOSAVE_INTERVAL = 60 * 1000 def __init__(self, editor): """ Constructor. Autosave is disabled after construction and needs to be enabled explicitly if required. Args: editor (Editor): editor plugin. """ self.editor = editor self.timer = QTimer(self.editor) self.timer.setSingleShot(True) self.timer.timeout.connect(self.do_autosave) self._enabled = False # Can't use setter here self._interval = self.DEFAULT_AUTOSAVE_INTERVAL @property def enabled(self): """ Get or set whether autosave component is enabled. The setter will start or stop the autosave component if appropriate. """ return self._enabled @enabled.setter def enabled(self, new_enabled): if new_enabled == self.enabled: return self.stop_autosave_timer() self._enabled = new_enabled self.start_autosave_timer() @property def interval(self): """ Interval between two autosaves, in milliseconds. The setter will perform an autosave if the interval is changed and autosave is enabled. """ return self._interval @interval.setter def interval(self, new_interval): if new_interval == self.interval: return self.stop_autosave_timer() self._interval = new_interval if self.enabled: self.do_autosave() def start_autosave_timer(self): """ Start a timer which calls do_autosave() after `self.interval`. The autosave timer is only started if autosave is enabled. """ if self.enabled: self.timer.start(self.interval) def stop_autosave_timer(self): """Stop the autosave timer.""" self.timer.stop() def do_autosave(self): """Instruct current editorstack to autosave files where necessary.""" logger.debug('Autosave triggered') stack = self.editor.get_current_editorstack() stack.autosave.autosave_all() self.start_autosave_timer() def try_recover_from_autosave(self): """Offer to recover files from autosave.""" autosave_dir = get_conf_path('autosave') autosave_mapping = CONF.get('editor', 'autosave_mapping', {}) dialog = RecoveryDialog(autosave_dir, autosave_mapping, parent=self.editor) dialog.exec_if_nonempty() self.recover_files_to_open = dialog.files_to_open[:]
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 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 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, designable=False) 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 _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 Status(DataPlugin): stat = STAT def __init__(self, cycle_time=100): super(Status, self).__init__() self.no_force_homing = INFO.noForceHoming() self.max_recent_files = PREFS.getPref("STATUS", "MAX_RECENT_FILES", 10, int) files = PREFS.getPref("STATUS", "RECENT_FILES", [], list) self.recent_files = [file for file in files if os.path.exists(file)] self.jog_increment = 0 # jog self.step_jog_increment = INFO.getIncrements()[0] self.jog_mode = True self.linear_jog_velocity = INFO.getJogVelocity() self.angular_jog_velocity = INFO.getJogVelocity() try: STAT.poll() except: pass excluded_items = [ 'axis', 'joint', 'spindle', 'poll', 'command', 'debug' ] self.old = {} # initialize data channels for item in dir(STAT): if item in self.channels: self.old[item] = getattr(STAT, item) self.channels[item].setValue(getattr(STAT, item)) elif item not in excluded_items and not item.startswith('_'): self.old[item] = getattr(STAT, item) chan = DataChannel(doc=item) chan.setValue(getattr(STAT, item)) self.channels[item] = chan setattr(self, item, chan) # add joint status channels self.joint = tuple(JointStatus(jnum) for jnum in range(9)) for joint in self.joint: for chan, obj in joint.channels.items(): self.channels['joint.{}.{}'.format(joint.jnum, chan)] = obj # add spindle status channels self.spindle = tuple(SpindleStatus(snum) for snum in range(8)) for spindle in self.spindle: for chan, obj in spindle.channels.items(): self.channels['spindle.{}.{}'.format(spindle.snum, chan)] = obj self.all_axes_homed.setValue(STAT.homed) self.homed.notify(self.all_axes_homed.setValue) # Set up the periodic update timer self.timer = QTimer() self._cycle_time = cycle_time self.timer.timeout.connect(self._periodic) self.on.settable = True self.task_state.notify( lambda ts: self.on.setValue(ts == linuxcnc.STATE_ON)) recent_files = DataChannel(doc='List of recently loaded files', settable=True, data=[]) @DataChannel def on(self): """True if machine power is ON.""" return STAT.task_state == linuxcnc.STATE_ON @DataChannel def file(self, chan): """Currently loaded file including path""" return chan.value or 'No file loaded' @file.setter def file(self, chan, fname): if STAT.interp_state == linuxcnc.INTERP_IDLE \ and STAT.call_level == 0: chan.value = fname chan.signal.emit(fname) @DataChannel def state(self, chan): """Current command execution status 1) Done 2) Exec 3) Error To return the string in a status label:: status:state?string :returns: current command execution state :rtype: int, str """ return STAT.state @state.tostring def state(self, chan): states = { 0: "N/A", linuxcnc.RCS_DONE: "Done", linuxcnc.RCS_EXEC: "Exec", linuxcnc.RCS_ERROR: "Error" } return states[STAT.state] @DataChannel def exec_state(self, chan): """Current task execution state 1) Error 2) Done 3) Waiting for Motion 4) Waiting for Motion Queue 5) Waiting for Pause 6) -- 7) Waiting for Motion and IO 8) Waiting for Delay 9) Waiting for system CMD 10) Waiting for spindle orient To return the string in a status label:: status:exec_state?string :returns: current task execution error :rtype: int, str """ return STAT.exec_state @exec_state.tostring def exec_state(self, chan): exec_states = { 0: "N/A", linuxcnc.EXEC_ERROR: "Error", linuxcnc.EXEC_DONE: "Done", linuxcnc.EXEC_WAITING_FOR_MOTION: "Waiting for Motion", linuxcnc.EXEC_WAITING_FOR_MOTION_QUEUE: "Waiting for Motion Queue", linuxcnc.EXEC_WAITING_FOR_IO: "Waiting for Pause", linuxcnc.EXEC_WAITING_FOR_MOTION_AND_IO: "Waiting for Motion and IO", linuxcnc.EXEC_WAITING_FOR_DELAY: "Waiting for Delay", linuxcnc.EXEC_WAITING_FOR_SYSTEM_CMD: "Waiting for system CMD", linuxcnc.EXEC_WAITING_FOR_SPINDLE_ORIENTED: "Waiting for spindle orient" } return exec_states[STAT.exec_state] @DataChannel def interp_state(self, chan): """Current state of RS274NGC interpreter 1) Idle 2) Reading 3) Paused 4) Waiting To return the string in a status label:: status:interp_state?string :returns: RS274 interpreter state :rtype: int, str """ return STAT.interp_state @interp_state.tostring def interp_state(self, chan): interp_states = { 0: "N/A", linuxcnc.INTERP_IDLE: "Idle", linuxcnc.INTERP_READING: "Reading", linuxcnc.INTERP_PAUSED: "Paused", linuxcnc.INTERP_WAITING: "Waiting" } return interp_states[STAT.interp_state] @DataChannel def interpreter_errcode(self, chan): """Current RS274NGC interpreter return code 0) Ok 1) Exit 2) Finished 3) Endfile 4) File not open 5) Error To return the string in a status label:: status:interpreter_errcode?string :returns: interp error code :rtype: int, str """ return STAT.interpreter_errcode @interpreter_errcode.tostring def interpreter_errcode(self, chan): interpreter_errcodes = { 0: "Ok", 1: "Exit", 2: "Finished", 3: "Endfile", 4: "File not open", 5: "Error" } return interpreter_errcodes[STAT.interpreter_errcode] @DataChannel def task_state(self, chan, query=None): """Current status of task 1) E-Stop 2) Reset 3) Off 4) On To return the string in a status label:: status:task_state?string :returns: current task state :rtype: int, str """ return STAT.task_state @task_state.tostring def task_state(self, chan): task_states = { 0: "N/A", linuxcnc.STATE_ESTOP: "E-Stop", linuxcnc.STATE_ESTOP_RESET: "Reset", linuxcnc.STATE_ON: "On", linuxcnc.STATE_OFF: "Off" } return task_states[STAT.task_state] @DataChannel def task_mode(self, chan): """Current task mode 1) Manual 2) Auto 3) MDI To return the string in a status label:: status:task_mode?string :returns: current task mode :rtype: int, str """ return STAT.task_mode @task_mode.tostring def task_mode(self, chan): task_modes = { 0: "N/A", linuxcnc.MODE_MANUAL: "Manual", linuxcnc.MODE_AUTO: "Auto", linuxcnc.MODE_MDI: "MDI" } return task_modes[STAT.task_mode] @DataChannel def motion_mode(self, chan): """Current motion controller mode 1) Free 2) Coord 3) Teleop To return the string in a status label:: status:motion_mode?string :returns: current motion mode :rtype: int, str """ return STAT.motion_mode @motion_mode.tostring def motion_mode(self, chan): modes = { 0: "N/A", linuxcnc.TRAJ_MODE_COORD: "Coord", linuxcnc.TRAJ_MODE_FREE: "Free", linuxcnc.TRAJ_MODE_TELEOP: "Teleop" } return modes[STAT.motion_mode] @DataChannel def motion_type(self, chan, query=None): """Motion type 0) None 1) Traverse 2) Linear Feed 3) Arc Feed 4) Tool Change 5) Probing 6) Rotary Index To return the string in a status label:: status:motion_type?string :returns: current motion type :rtype: int, str """ return STAT.motion_type @motion_type.tostring def motion_type(self, chan): motion_types = { 0: "None", linuxcnc.MOTION_TYPE_TRAVERSE: "Traverse", linuxcnc.MOTION_TYPE_FEED: "Linear Feed", linuxcnc.MOTION_TYPE_ARC: "Arc Feed", linuxcnc.MOTION_TYPE_TOOLCHANGE: "Tool Change", linuxcnc.MOTION_TYPE_PROBING: "Probing", linuxcnc.MOTION_TYPE_INDEXROTARY: "Rotary Index" } return motion_types[STAT.motion_type] @DataChannel def program_units(self, chan): """Program units Available as an integer, or in short or long string formats. 1) in, Inches 2) mm, Millimeters 3) cm, Centimeters To return the string in a status label:: status:program_units status:program_units?string status:program_units?string&format=long :returns: current program units :rtype: int, str """ return STAT.program_units @program_units.tostring def program_units(self, chan, format='short'): if format == 'short': return ["N/A", "in", "mm", "cm"][STAT.program_units] else: return ["N/A", "Inches", "Millimeters", "Centimeters"][STAT.program_units] @DataChannel def linear_units(self, chan): """Machine linear units Available as float (units/mm), or in short or long string formats. To return the string in a status label:: status:linear_units status:linear_units?string status:linear_units?string&format=long :returns: machine linear units :rtype: float, str """ return STAT.linear_units @linear_units.tostring def linear_units(self, chan, format='short'): if format == 'short': return {0.0: "N/A", 1.0: "mm", 1 / 25.4: "in"}[STAT.linear_units] else: return { 0.0: "N/A", 1.0: "Millimeters", 1 / 25.4: "Inches" }[STAT.linear_units] @DataChannel def gcodes(self, chan, fmt=None): """G-codes active G-codes for each modal group | syntax ``status:gcodes`` returns tuple of strings | syntax ``status:gcodes?raw`` returns tuple of integers | syntax ``status:gcodes?string`` returns str """ if fmt == 'raw': return STAT.gcodes return chan.value @gcodes.tostring def gcodes(self, chan): return " ".join(chan.value) @gcodes.setter def gcodes(self, chan, gcodes): chan.value = tuple( ["G%g" % (c / 10.) for c in sorted(gcodes[1:]) if c != -1]) chan.signal.emit(self.gcodes.value) @DataChannel def mcodes(self, chan, fmt=None): """M-codes active M-codes for each modal group | syntax ``status:mcodes`` returns tuple of strings | syntax ``status:mcodes?raw`` returns tuple of integers | syntax ``status:mcodes?string`` returns str """ if fmt == 'raw': return STAT.mcodes return chan.value @mcodes.tostring def mcodes(self, chan): return " ".join(chan.value) @mcodes.setter def mcodes(self, chan, gcodes): chan.value = tuple( ["M%g" % gcode for gcode in sorted(gcodes[1:]) if gcode != -1]) chan.signal.emit(chan.value) @DataChannel def g5x_index(self, chan): """Current G5x work coord system | syntax ``status:g5x_index`` returns int | syntax ``status:g5x_index?string`` returns str """ return STAT.g5x_index @g5x_index.tostring def g5x_index(self, chan): return [ "G53", "G54", "G55", "G56", "G57", "G58", "G59", "G59.1", "G59.2", "G59.3" ][STAT.g5x_index] @DataChannel def settings(self, chan, item=None): """Interpreter Settings Available Items: 0) sequence_number 1) feed 2) speed :return: interpreter settings :rtype: tuple, int, float """ if item is None: return STAT.settings return STAT.settings[{ 'sequence_number': 0, 'feed': 1, 'speed': 2 }[item]] @DataChannel def homed(self, chan, anum=None): """Axis homed status If no axis number is specified returns a tuple of integers. If ``anum`` is specified returns True if the axis is homed, else False. Rules syntax:: status:homed status:homed?anum=0 Args: anum (int, optional) : the axis number to return the homed state of. :returns: axis homed states :rtype: tuple, bool """ if anum is None: return STAT.homed return bool(STAT.homed[int(anum)]) @DataChannel def all_axes_homed(self, chan): """All axes homed status True if all axes are homed or if [TRAJ]NO_FORCE_HOMING set in INI. :returns: all homed :rtype: bool """ return chan.value @all_axes_homed.setter def all_axes_homed(self, chan, homed): if self.no_force_homing: chan.value = True else: for anum in INFO.AXIS_NUMBER_LIST: if homed[anum] is not 1: chan.value = False break else: chan.value = True chan.signal.emit(chan.value) # this is used by File "qtpyvcp/qtpyvcp/actions/program_actions.py", # line 83, in _run_ok elif not STATUS.allHomed(): def allHomed(self): if self.no_force_homing: return True for jnum in range(STAT.joints): if not STAT.joint[jnum]['homed']: return False return True def initialise(self): """Start the periodic update timer.""" LOG.debug("Starting periodic updates with %ims cycle time", self._cycle_time) self.timer.start(self._cycle_time) def terminate(self): """Save persistent settings on terminate.""" PREFS.setPref("STATUS", "RECENT_FILES", self.recent_files.value) PREFS.setPref("STATUS", "MAX_RECENT_FILES", self.max_recent_files) def _periodic(self): # s = time.time() try: STAT.poll() except Exception: LOG.warning("Status polling failed, is LinuxCNC running?", exc_info=True) self.timer.stop() return # status updates for item, old_val in self.old.iteritems(): new_val = getattr(STAT, item) if new_val != old_val: self.old[item] = new_val self.channels[item].setValue(new_val) # joint status updates for joint in self.joint: joint._update() # spindle status updates for spindle in self.spindle: spindle._update()
class CondaDependenciesModel(QAbstractTableModel): """ """ def __init__(self, parent, dic, packages_sizes=None): super(CondaDependenciesModel, self).__init__(parent) self._parent = parent self._packages = dic self._packages_sizes = packages_sizes self._rows = [] self._bold_rows = [] self._timer = QTimer() self._timer.timeout.connect(self._timer_update) self._timer_dots = ['. ', '.. ', '...', ' '] self._timer_counter = 0 if len(dic) == 0: self._timer.start(650) self._rows = [[_(u'Resolving dependencies '), u'', u'', u'']] self._bold_rows.append(0) else: if 'actions' in dic: dic = dic['actions'] titles = [_('Name'), _('Unlink'), _('Link'), _('Download')] order = ['UNLINK', 'LINK', 'FETCH'] packages_dic = self._build_packages_table(dic) total_size = packages_dic.pop('TOTAL##', '') packages_order = sorted(list(packages_dic)) rows = [titles] self._bold_rows.append(0) for package in packages_order: row = [package] item = packages_dic[package] for section in order: row.append(item.get(section, u'-')) rows.append(row) if total_size: rows.append([u'', u'', u'', u'TOTAL']) rows.append([u'', u'', u'', total_size]) self._bold_rows.append(len(rows) - 2) self._bold_rows.append(len(rows) - 1) for row in rows: self._rows.append(row) def _timer_update(self): """Add some moving points to the dependency resolution text.""" self._timer_counter += 1 dot = self._timer_dots.pop(0) self._timer_dots = self._timer_dots + [dot] self._rows = [[_(u'Resolving dependencies') + dot, u'', u'', u'']] index = self.createIndex(0, 0) self.dataChanged.emit(index, index) if self._timer_counter > 150: self._timer.stop() self._timer_counter = 0 def _build_packages_table(self, dic): """ """ self._timer.stop() sections = {'FETCH': None, 'EXTRACT': None, 'LINK': None, 'UNLINK': None} packages = {} for section in sections: sections[section] = dic.get(section, ()) if sections[section]: for item in sections[section]: i = item.split(' ')[0] name, version, build = split_canonical_name(i) packages[name] = {} for section in sections: pkgs = sections[section] for item in pkgs: i = item.split(' ')[0] name, version, build = split_canonical_name(i) packages[name][section] = version total = 0 for pkg in packages: val = packages[pkg] if u'FETCH' in val: v = val['FETCH'] if pkg in self._packages_sizes: size = self._packages_sizes[pkg].get(v, '-') if size != '-': total += size size = human_bytes(size) packages[pkg]['FETCH'] = size packages['TOTAL##'] = human_bytes(total) return packages def flags(self, index): """Override Qt method""" if not index.isValid(): return Qt.ItemIsEnabled column = index.column() if column in [0, 1, 2, 3]: return Qt.ItemFlags(Qt.ItemIsEnabled) else: return Qt.ItemFlags(Qt.NoItemFlags) def data(self, index, role=Qt.DisplayRole): """Override Qt method""" if not index.isValid() or not 0 <= index.row() < len(self._rows): return to_qvariant() row = index.row() column = index.column() # Carefull here with the order, this has to be adjusted manually if self._rows[row] == row: name, unlink, link, fetch = [u'', u'', u'', u''] else: name, unlink, link, fetch = self._rows[row] if role == Qt.DisplayRole: if column == 0: return to_qvariant(name) elif column == 1: return to_qvariant(unlink) elif column == 2: return to_qvariant(link) elif column == 3: return to_qvariant(fetch) elif role == Qt.TextAlignmentRole: if column in [0]: return to_qvariant(int(Qt.AlignLeft | Qt.AlignVCenter)) elif column in [1, 2, 3]: return to_qvariant(int(Qt.AlignHCenter | Qt.AlignVCenter)) elif role == Qt.ForegroundRole: return to_qvariant() elif role == Qt.FontRole: font = QFont() if row in self._bold_rows: font.setBold(True) return to_qvariant(font) else: font.setBold(False) return to_qvariant(font) return to_qvariant() def rowCount(self, index=QModelIndex()): """Override Qt method.""" return len(self._rows) def columnCount(self, index=QModelIndex()): """Override Qt method.""" return 4 def row(self, row_index): """Get a row by row index.""" return self._rows[row_index]
class AutosaveForPlugin(object): """ Component of editor plugin implementing autosave functionality. Attributes: name_mapping (dict): map between names of opened and autosave files. file_hashes (dict): map between file names and hash of their contents. This is used for both files opened in the editor and their corresponding autosave files. """ # Interval (in ms) between two autosaves DEFAULT_AUTOSAVE_INTERVAL = 60 * 1000 def __init__(self, editor): """ Constructor. Autosave is disabled after construction and needs to be enabled explicitly if required. Args: editor (Editor): editor plugin. """ self.editor = editor self.name_mapping = {} self.file_hashes = {} self.timer = QTimer(self.editor) self.timer.setSingleShot(True) self.timer.timeout.connect(self.do_autosave) self._enabled = False # Can't use setter here self._interval = self.DEFAULT_AUTOSAVE_INTERVAL @property def enabled(self): """ Get or set whether autosave component is enabled. The setter will start or stop the autosave component if appropriate. """ return self._enabled @enabled.setter def enabled(self, new_enabled): if new_enabled == self.enabled: return self.stop_autosave_timer() self._enabled = new_enabled self.start_autosave_timer() @property def interval(self): """ Interval between two autosaves, in milliseconds. The setter will perform an autosave if the interval is changed and autosave is enabled. """ return self._interval @interval.setter def interval(self, new_interval): if new_interval == self.interval: return self.stop_autosave_timer() self._interval = new_interval if self.enabled: self.do_autosave() def start_autosave_timer(self): """ Start a timer which calls do_autosave() after `self.interval`. The autosave timer is only started if autosave is enabled. """ if self.enabled: self.timer.start(self.interval) def stop_autosave_timer(self): """Stop the autosave timer.""" self.timer.stop() def single_instance(self): """Return whether Spyder is running in single instance mode.""" single_instance = CONF.get('main', 'single_instance') new_instance = self.editor.main.new_instance return single_instance and not new_instance def do_autosave(self): """Instruct current editorstack to autosave files where necessary.""" if not self.single_instance(): logger.debug('Autosave disabled because not single instance') return logger.debug('Autosave triggered') stack = self.editor.get_current_editorstack() stack.autosave.autosave_all() self.start_autosave_timer() def try_recover_from_autosave(self): """Offer to recover files from autosave.""" if not self.single_instance(): self.recover_files_to_open = [] return autosave_dir = get_conf_path('autosave') autosave_mapping = CONF.get('editor', 'autosave_mapping', {}) dialog = RecoveryDialog(autosave_dir, autosave_mapping, parent=self.editor) dialog.exec_if_nonempty() self.recover_files_to_open = dialog.files_to_open[:] def register_autosave_for_stack(self, autosave_for_stack): """ Register an AutosaveForStack object. This replaces the `name_mapping` and `file_hashes` attributes in `autosave_for_stack` with references to the corresponding attributes of `self`, so that all AutosaveForStack objects share the same data. """ autosave_for_stack.name_mapping = self.name_mapping autosave_for_stack.file_hashes = self.file_hashes
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 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 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 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(150) 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 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.')
class AsyncClient(QObject): """ A class which handles a connection to a client through a QProcess. """ # Emitted when the client has initialized. initialized = Signal() # Emitted when the client errors. errored = Signal() # Emitted when a request response is received. received = Signal(object) def __init__(self, target, executable=None, name=None, extra_args=None, libs=None, cwd=None, env=None, extra_path=None): super(AsyncClient, self).__init__() self.executable = executable or sys.executable self.extra_args = extra_args self.target = target self.name = name or self self.libs = libs self.cwd = cwd self.env = env self.extra_path = extra_path self.is_initialized = False self.closing = False self.notifier = None self.process = None self.context = zmq.Context() QApplication.instance().aboutToQuit.connect(self.close) # Set up the heartbeat timer. self.timer = QTimer(self) self.timer.timeout.connect(self._heartbeat) def run(self): """Handle the connection with the server. """ # Set up the zmq port. self.socket = self.context.socket(zmq.PAIR) self.port = self.socket.bind_to_random_port('tcp://*') # Set up the process. self.process = QProcess(self) if self.cwd: self.process.setWorkingDirectory(self.cwd) p_args = ['-u', self.target, str(self.port)] if self.extra_args is not None: p_args += self.extra_args # Set up environment variables. processEnvironment = QProcessEnvironment() env = self.process.systemEnvironment() if (self.env and 'PYTHONPATH' not in self.env) or self.env is None: python_path = osp.dirname(get_module_path('trex')) # Add the libs to the python path. for lib in self.libs: try: path = osp.dirname(imp.find_module(lib)[1]) python_path = osp.pathsep.join([python_path, path]) except ImportError: pass if self.extra_path: try: python_path = osp.pathsep.join([python_path] + self.extra_path) except Exception as e: debug_print("Error when adding extra_path to plugin env") debug_print(e) env.append("PYTHONPATH=%s" % python_path) if self.env: env.update(self.env) for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) # Start the process and wait for started. self.process.start(self.executable, p_args) self.process.finished.connect(self._on_finished) running = self.process.waitForStarted() if not running: raise IOError('Could not start %s' % self) # Set up the socket notifer. fid = self.socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self._on_msg_received) def request(self, func_name, *args, **kwargs): """Send a request to the server. The response will be a dictionary the 'request_id' and the 'func_name' as well as a 'result' field with the object returned by the function call or or an 'error' field with a traceback. """ if not self.is_initialized: return request_id = uuid.uuid4().hex request = dict(func_name=func_name, args=args, kwargs=kwargs, request_id=request_id) self._send(request) return request_id def close(self): """Cleanly close the connection to the server. """ self.closing = True self.is_initialized = False self.timer.stop() if self.notifier is not None: self.notifier.activated.disconnect(self._on_msg_received) self.notifier.setEnabled(False) self.notifier = None self.request('server_quit') if self.process is not None: self.process.waitForFinished(1000) self.process.close() self.context.destroy() def _on_finished(self): """Handle a finished signal from the process. """ if self.closing: return if self.is_initialized: debug_print('Restarting %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.is_initialized = False self.notifier.setEnabled(False) self.run() else: debug_print('Errored %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.errored.emit() def _on_msg_received(self): """Handle a message trigger from the socket. """ self.notifier.setEnabled(False) while 1: try: resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) except zmq.ZMQError: self.notifier.setEnabled(True) return if not self.is_initialized: self.is_initialized = True debug_print('Initialized %s' % self.name) self.initialized.emit() self.timer.start(HEARTBEAT) continue resp['name'] = self.name self.received.emit(resp) def _heartbeat(self): """Send a heartbeat to keep the server alive. """ self._send(dict(func_name='server_heartbeat')) def _send(self, obj): """Send an object to the server. """ try: self.socket.send_pyobj(obj, zmq.NOBLOCK) except Exception as e: debug_print(e) self.is_initialized = False self._on_finished()
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 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 message: MultilineElidedLabel source_label: QLabel severity_icon: QLabel def __init__( self, message: str, severity: Union[str, 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( 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() 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) tbdialog.layout().addWidget(text) 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 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): cls.from_notification(notification).show()
class CommunityTab(WidgetBase): """Community tab.""" # Qt Signals sig_video_started = Signal(str, int) sig_status_updated = Signal(object, int, int, int) sig_ready = Signal(object) # Sender # Class variables instances = [] # Maximum item count for different content type VIDEOS_LIMIT = 25 WEBINARS_LIMIT = 25 EVENTS_LIMIT = 25 # Google analytics campaigns UTM_MEDIUM = 'in-app' UTM_SOURCE = 'navigator' def __init__(self, parent=None, tags=None, content_urls=None, content_path=CONTENT_PATH, image_path=IMAGE_DATA_PATH, config=CONF, bundle_path=LINKS_INFO_PATH, saved_content_path=CONTENT_JSON_PATH, tab_name=''): """Community tab.""" super(CommunityTab, self).__init__(parent=parent) self._tab_name = '' self.content_path = content_path self.image_path = image_path self.bundle_path = bundle_path self.saved_content_path = saved_content_path self.config = config self._parent = parent self._downloaded_thumbnail_urls = [] self._downloaded_urls = [] self._downloaded_filepaths = [] self.api = AnacondaAPI() self.content_urls = content_urls self.content_info = [] self.step = 0 self.step_size = 1 self.tags = tags self.timer_load = QTimer() self.pixmaps = {} self.filter_widgets = [] self.default_pixmap = QPixmap(VIDEO_ICON_PATH).scaled( 100, 60, Qt.KeepAspectRatio, Qt.FastTransformation) # Widgets self.text_filter = LineEditSearch() self.list = ListWidgetContent() self.frame_header = FrameTabHeader() self.frame_content = FrameTabContent() # Widget setup self.timer_load.setInterval(333) self.list.setAttribute(Qt.WA_MacShowFocusRect, False) self.text_filter.setPlaceholderText('Search') self.text_filter.setAttribute(Qt.WA_MacShowFocusRect, False) self.setObjectName("Tab") self.list.setMinimumHeight(200) fm = self.text_filter.fontMetrics() self.text_filter.setMaximumWidth(fm.width('M' * 23)) # Layouts self.filters_layout = QHBoxLayout() layout_header = QHBoxLayout() layout_header.addLayout(self.filters_layout) layout_header.addStretch() layout_header.addWidget(self.text_filter) self.frame_header.setLayout(layout_header) layout_content = QHBoxLayout() layout_content.addWidget(self.list) self.frame_content.setLayout(layout_content) layout = QVBoxLayout() layout.addWidget(self.frame_header) layout.addWidget(self.frame_content) self.setLayout(layout) # Signals self.timer_load.timeout.connect(self.set_content_list) self.text_filter.textChanged.connect(self.filter_content) def setup(self): """Setup tab content.""" self.download_content() def _json_downloaded(self, worker, output, error): """Callbacl for download_content.""" url = worker.url if url in self._downloaded_urls: self._downloaded_urls.remove(url) if not self._downloaded_urls: self.load_content() def download_content(self): """Download content to display in cards.""" self._downloaded_urls = [] self._downloaded_filepaths = [] if self.content_urls: for url in self.content_urls: url = url.lower() # Enforce lowecase... just in case fname = url.split('/')[-1] + '.json' filepath = os.sep.join([self.content_path, fname]) self._downloaded_urls.append(url) self._downloaded_filepaths.append(filepath) worker = self.api.download(url, filepath) worker.url = url worker.sig_finished.connect(self._json_downloaded) else: self.load_content() def load_content(self, paths=None): """Load downloaded and bundled content.""" content = [] # Load downloaded content for filepath in self._downloaded_filepaths: fname = filepath.split(os.sep)[-1] items = [] if os.path.isfile(filepath): with open(filepath, 'r') as f: data = f.read() try: items = json.loads(data) except Exception as error: logger.error(str((filepath, error))) else: items = [] if 'video' in fname: for item in items: try: item['tags'] = ['video'] item['uri'] = item.get('video', '') if item['uri']: item['banner'] = item.get('thumbnail') image_path = item['banner'].split('/')[-1] item['image_file'] = image_path else: url = '' item['image_file'] = '' item['banner'] = url item['date'] = item.get('date_start', '') except Exception: logger.debug("Video parse failed: {0}".format(item)) items = items[:self.VIDEOS_LIMIT] elif 'event' in fname: for item in items: try: item['tags'] = ['event'] item['uri'] = item.get('url', '') if item['banner']: image_path = item['banner'].split('/')[-1] item['image_file'] = image_path else: item['banner'] = '' except Exception: logger.debug('Event parse failed: {0}'.format(item)) items = items[:self.EVENTS_LIMIT] elif 'webinar' in fname: for item in items: try: item['tags'] = ['webinar'] uri = item.get('url', '') utm_campaign = item.get('utm_campaign', '') item['uri'] = self.add_campaign(uri, utm_campaign) image = item.get('image', '') if image and isinstance(image, dict): item['banner'] = image.get('src', '') if item['banner']: image_path = item['banner'].split('/')[-1] item['image_file'] = image_path else: item['image_file'] = '' else: item['banner'] = '' item['image_file_path'] = '' except Exception: logger.debug('Webinar parse failed: {0}'.format(item)) items = items[:self.WEBINARS_LIMIT] if items: content.extend(items) # Load bundled content with open(self.bundle_path, 'r') as f: data = f.read() items = [] try: items = json.loads(data) except Exception as error: logger.error(str((filepath, error))) content.extend(items) # Add the image path to get the full path for i, item in enumerate(content): uri = item['uri'] uri = uri.replace('<p>', '').replace('</p>', '') item['uri'] = uri.replace(' ', '%20') filename = item.get('image_file', '') item['image_file_path'] = os.path.sep.join( [self.image_path, filename]) # if 'video' in item['tags']: # print(i, item['uri']) # print(item['banner']) # print(item['image_file_path']) # print('') # Make sure items of the same type/tag are contiguous in the list content = sorted(content, key=lambda i: i.get('tags')) # But also make sure sticky content appears first sticky_content = [] for i, item in enumerate(content[:]): sticky = item.get('sticky') if isinstance(sticky, str): is_sticky = sticky == 'true' elif sticky is None: is_sticky = False # print(i, sticky, is_sticky, item.get('title')) if is_sticky: sticky_content.append(item) content.remove(item) content = sticky_content + content self.content_info = content # Save loaded data in a single file with open(self.saved_content_path, 'w') as f: json.dump(content, f) self.make_tag_filters() self.timer_load.start(random.randint(25, 35)) def add_campaign(self, uri, utm_campaign): """Add tracking analytics campaing to url in content items.""" if uri and utm_campaign: parameters = parse.urlencode({ 'utm_source': self.UTM_SOURCE, 'utm_medium': self.UTM_MEDIUM, 'utm_campaign': utm_campaign }) uri = '{0}?{1}'.format(uri, parameters) return uri def make_tag_filters(self): """Create tag filtering checkboxes based on available content tags.""" if not self.tags: self.tags = set() for content_item in self.content_info: tags = content_item.get('tags', []) for tag in tags: if tag: self.tags.add(tag) # Get count tag_count = {tag: 0 for tag in self.tags} for tag in self.tags: for content_item in self.content_info: item_tags = content_item.get('tags', []) if tag in item_tags: tag_count[tag] += 1 logger.debug("TAGS: {0}".format(self.tags)) self.filter_widgets = [] for tag in sorted(self.tags): count = tag_count[tag] tag_text = "{0} ({1})".format(tag.capitalize(), count).strip() item = ButtonToggle(tag_text) item.setObjectName(tag.lower()) item.setChecked(self.config.get('checkboxes', tag.lower(), True)) item.clicked.connect(self.filter_content) self.filter_widgets.append(item) self.filters_layout.addWidget(item) self.filters_layout.addWidget(SpacerHorizontal()) def filter_content(self, text=None): """ Filter content by a search string on all the fields of the item. Using comma allows the use of several keywords, e.g. Peter,2015. """ text = self.text_filter.text().lower() text = [t for t in re.split('\W', text) if t] selected_tags = [] for item in self.filter_widgets: tag_parts = item.text().lower().split() tag = tag_parts[0] # tag_count = tag_parts[-1] if item.isChecked(): selected_tags.append(tag) self.config.set('checkboxes', tag, True) else: self.config.set('checkboxes', tag, False) for i in range(self.list.count()): item = self.list.item(i) all_checks = [] for t in text: t = t.strip() checks = (t in item.title.lower() or t in item.venue.lower() or t in ' '.join(item.authors).lower() or t in item.summary.lower()) all_checks.append(checks) all_checks.append( any(tag.lower() in selected_tags for tag in item.tags)) if all(all_checks): item.setHidden(False) else: item.setHidden(True) def set_content_list(self): """ Add items to the list, gradually. Called by a timer. """ for i in range(self.step, self.step + self.step_size): if i < len(self.content_info): item = self.content_info[i] banner = item.get('banner', '') path = item.get('image_file_path', '') content_item = ListItemContent( title=item['title'], subtitle=item.get('subtitle', "") or "", uri=item['uri'], date=item.get('date', '') or "", summary=item.get('summary', '') or "", tags=item.get('tags', []), banner=banner, path=path, pixmap=self.default_pixmap, ) self.list.addItem(content_item) # self.update_style_sheet(self.style_sheet) # This allows the content to look for the pixmap content_item.pixmaps = self.pixmaps # Use images shipped with Navigator, if no image try the # download image_file = item.get('image_file', 'NaN') local_image = os.path.join(LOGO_PATH, image_file) if os.path.isfile(local_image): self.pixmaps[path] = QPixmap(local_image) else: self.download_thumbnail(content_item, banner, path) else: self.timer_load.stop() self.sig_ready.emit(self._tab_name) break self.step += self.step_size self.filter_content() def download_thumbnail(self, item, url, path): """Download all the video thumbnails.""" # Check url is not an empty string or not already downloaded if url and url not in self._downloaded_thumbnail_urls: self._downloaded_thumbnail_urls.append(url) # For some content the app segfaults (with big files) so # we dont use chunks worker = self.api.download(url, path, chunked=True) worker.url = url worker.item = item worker.path = path worker.sig_finished.connect(self.convert_image) logger.debug('Fetching thumbnail {}'.format(url)) def convert_image(self, worker, output, error): """ Load an image using PIL, and converts it to a QPixmap. This was needed as some image libraries are not found in some OS. """ path = output if path in self.pixmaps: return try: if sys.platform == 'darwin' and PYQT4: from PIL.ImageQt import ImageQt from PIL import Image if path: image = Image.open(path) image = ImageQt(image) qt_image = QImage(image) pixmap = QPixmap.fromImage(qt_image) else: pixmap = QPixmap() else: if path and os.path.isfile(path): extension = path.split('.')[-1].upper() if extension in ['PNG', 'JPEG', 'JPG']: # This might be producing an error message on windows # for some of the images pixmap = QPixmap(path, format=extension) else: pixmap = QPixmap(path) else: pixmap = QPixmap() self.pixmaps[path] = pixmap except (IOError, OSError) as error: logger.error(str(error)) def update_style_sheet(self, style_sheet=None): """Update custom CSS stylesheet.""" if style_sheet is None: self.style_sheet = load_style_sheet() else: self.style_sheet = style_sheet self.setStyleSheet(self.style_sheet) self.list.update_style_sheet(self.style_sheet) def ordered_widgets(self, next_widget=None): """Fix tab order of UI widgets.""" ordered_widgets = [] ordered_widgets += self.filter_widgets ordered_widgets += [self.text_filter] ordered_widgets += self.list.ordered_widgets() return ordered_widgets
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) 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() 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() 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() 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 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 ToolTipWidget(QLabel): """ Shows tooltips that can be styled with the different themes. """ sig_completion_help_requested = Signal(str, str) sig_help_requested = Signal(str) def __init__(self, parent=None, as_tooltip=False): """ Shows tooltips that can be styled with the different themes. """ super(ToolTipWidget, self).__init__(parent, Qt.ToolTip) # Variables self.completion_doc = None self._url = '' self.app = QCoreApplication.instance() self.as_tooltip = as_tooltip self.tip = None self._timer_hide = QTimer() self._text_edit = parent # Setup # This keeps the hints below other applications if sys.platform == 'darwin': self.setWindowFlags(Qt.SplashScreen) else: self.setWindowFlags(Qt.ToolTip | Qt.FramelessWindowHint) self._timer_hide.setInterval(500) self.setTextInteractionFlags(Qt.TextSelectableByMouse) self.setTextInteractionFlags(Qt.TextBrowserInteraction) self.setOpenExternalLinks(False) self.setForegroundRole(QPalette.ToolTipText) self.setBackgroundRole(QPalette.ToolTipBase) self.setPalette(QToolTip.palette()) self.setAlignment(Qt.AlignLeft) self.setIndent(1) self.setFrameStyle(QFrame.NoFrame) style = self.style() delta_margin = style.pixelMetric(QStyle.PM_ToolTipLabelFrameWidth, None, self) self.setMargin(1 + delta_margin) # Signals self.linkHovered.connect(self._update_hover_html_link_style) self._timer_hide.timeout.connect(self.hide) def paintEvent(self, event): """Reimplemented to paint the background panel.""" painter = QStylePainter(self) option = QStyleOptionFrame() option.initFrom(self) painter.drawPrimitive(QStyle.PE_PanelTipLabel, option) painter.end() super(ToolTipWidget, self).paintEvent(event) def _update_hover_html_link_style(self, url): """Update style of labels that include rich text and html links.""" link = 'text-decoration:none;' link_hovered = 'text-decoration:underline;' self._url = url if url: QApplication.setOverrideCursor(QCursor(Qt.PointingHandCursor)) new_text, old_text = link_hovered, link else: new_text, old_text = link, link_hovered QApplication.restoreOverrideCursor() text = self.text() new_text = text.replace(old_text, new_text) self.setText(new_text) # ------------------------------------------------------------------------ # --- 'ToolTipWidget' interface # ------------------------------------------------------------------------ def show_basic_tip(self, point, tip): """Show basic tip.""" self.tip = tip self.setText(tip) self.resize(self.sizeHint()) y = point.y() - self.height() self.move(point.x(), y) self.show() return True def show_tip(self, point, tip, cursor=None, completion_doc=None): """ Attempts to show the specified tip at the current cursor location. """ # Don't attempt to show it if it's already visible and the text # to be displayed is the same as the one displayed before. if self.tip == tip: if not self.isVisible(): self.show() return # Set the text and resize the widget accordingly. self.tip = tip self.setText(tip) self.resize(self.sizeHint()) self.completion_doc = completion_doc padding = 0 text_edit = self._text_edit if cursor is None: cursor_rect = text_edit.cursorRect() else: cursor_rect = text_edit.cursorRect(cursor) screen_rect = self.app.desktop().screenGeometry(text_edit) point.setY(point.y() + padding) tip_height = self.size().height() tip_width = self.size().width() vertical = 'bottom' horizontal = 'Right' if point.y() + tip_height > screen_rect.height() + screen_rect.y(): point_ = text_edit.mapToGlobal(cursor_rect.topRight()) # If tip is still off screen, check if point is in top or bottom # half of screen. if point_.y() - tip_height < padding: # If point is in upper half of screen, show tip below it. # otherwise above it. if 2 * point.y() < screen_rect.height(): vertical = 'bottom' else: vertical = 'top' else: vertical = 'top' if point.x() + tip_width > screen_rect.width() + screen_rect.x(): point_ = text_edit.mapToGlobal(cursor_rect.topRight()) # If tip is still off-screen, check if point is in the right or # left half of the screen. if point_.x() - tip_width < padding: if 2 * point.x() < screen_rect.width(): horizontal = 'Right' else: horizontal = 'Left' else: horizontal = 'Left' pos = getattr(cursor_rect, '%s%s' % (vertical, horizontal)) adjusted_point = text_edit.mapToGlobal(pos()) if vertical == 'top': if os.name == 'nt': padding = -7 else: padding = -4.5 point.setY(adjusted_point.y() - tip_height - padding) if horizontal == 'Left': point.setX(adjusted_point.x() - tip_width - padding) self.move(point) if not self.isVisible(): self.show() return True def mousePressEvent(self, event): """ Reimplemented to hide it when focus goes out of the main window. """ QApplication.restoreOverrideCursor() if self.completion_doc: name = self.completion_doc.get('name', '') signature = self.completion_doc.get('signature', '') self.sig_completion_help_requested.emit(name, signature) else: self.sig_help_requested.emit(self._url) super(ToolTipWidget, self).mousePressEvent(event) self._hide() def focusOutEvent(self, event): """ Reimplemented to hide it when focus goes out of the main window. """ self.hide() def leaveEvent(self, event): """Override Qt method to hide the tooltip on leave.""" super(ToolTipWidget, self).leaveEvent(event) self.hide() def _hide(self): """Hide method helper.""" QApplication.restoreOverrideCursor() self._timer_hide.start() def hide(self): """Override Qt method to add timer and restore cursor.""" super(ToolTipWidget, self).hide() self._timer_hide.stop()
class TasksInProcess(TasksBase): """Implements a queue containing jobs (Python methods of a base class specified in `cls`).""" def init_queues(self): self._qin = pQueue() self._qout = pQueue() self._qout_sync = pQueue() def instanciate_task(self): self.task_obj = ToInstanciate(self.task_class, *self.initargs, **self.initkwargs) # This object resides on the master process. self.task_obj_master = self.task_class(*self.initargs, **self.initkwargs) def start(self): self.start_worker() self.start_master() def start_worker(self): """Start the worker thread or process.""" self._process_worker = Process(target=worker_loop, args=(self.task_obj, self._qin, self._qout, self._qout_sync, self.impatient)) self._process_worker.start() def _retrieve(self): """Master main function.""" if self.use_master_thread: master_loop(self.task_class, self._qin, self._qout, self.results, task_obj=self.task_obj_master) else: master_iteration(self.task_class, self._qin, self._qout, self.results, task_obj=self.task_obj_master) def start_master(self): """Start the master thread, used to retrieve the results.""" if self.use_master_thread: self._thread_master = _start_thread(self._retrieve) else: self._timer_master = QTimer(self) self._timer_master.setInterval(int(TIMER_MASTER_DELAY * 1000)) self._timer_master.timeout.connect(self._retrieve) self._timer_master.start() def join(self): """Stop the worker and master as soon as all tasks have finished.""" self._qin.put(FINISHED) if self.use_master_thread: self._thread_master.join() else: self._timer_master.stop() self._process_worker.terminate() self._process_worker.join() def terminate(self): self._process_worker.terminate() self._process_worker.join() self._qout.put(FINISHED) if self.use_master_thread: self._thread_master.join() else: self._timer_master.stop() def __getattr__(self, name): # execute a method on the task object locally task_obj = self.task_obj_master # if hasattr(self.task_class, name): if hasattr(task_obj, name): v = getattr(task_obj, name) # wrap the task object's method in the Job Queue so that it # is pushed in the queue instead of executed immediately if inspect.ismethod(v): return lambda *args, **kwargs: self._put(name, *args, **kwargs) # if the attribute is a task object's property, just return it else: return v # or, if the requested name is a task object attribute, try obtaining # remotely else: result = self._put('__getattribute__', name, _sync=True) return result[2]['_result']
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): """Download API based on requests.""" super(QObject, self).__init__() self._conda_api = CondaAPI() self._client_api = ClientAPI() 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() @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=True, chunked=True, ): """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) # Get headers try: r = requests.head( url, proxies=self.proxy_servers, verify=verify, 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, 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): """Callback for is_valid_url.""" try: r = requests.head( url, proxies=self.proxy_servers, 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' ): """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, verify=self._client_api.get_ssl(), 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.""" # Check response is a JSON with ok: 1 data = {} if verify is None: verify_value = self._client_api.get_ssl() else: verify_value = verify 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 = {} if verify is None: verify_value = self._client_api.get_ssl() else: verify_value = verify 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 # --- Public API # ------------------------------------------------------------------------- def download(self, url, path=None, force=False, verify=True, 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=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, 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=True, 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): """Callback.""" 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", } try: r = requests.get( url, proxies=self.proxy_servers, verify=self._client_api.get_ssl(), 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 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 RunDialog(QDialog): simulation_done = Signal(bool, str) simulation_termination_request = Signal() def __init__(self, config_file, run_model, simulation_arguments, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setModal(True) self.setWindowModality(Qt.WindowModal) self.setWindowTitle("Simulations - {}".format(config_file)) self._snapshot_model = SnapshotModel(self) self._run_model = run_model self._isDetailedDialog = False self._minimum_width = 1200 ert = None if isinstance(run_model, BaseRunModel): ert = run_model.ert() self._simulations_argments = simulation_arguments self._ticker = QTimer(self) self._ticker.timeout.connect(self._on_ticker) progress_proxy_model = ProgressProxyModel(self._snapshot_model, parent=self) self._total_progress_label = QLabel( _TOTAL_PROGRESS_TEMPLATE.format( total_progress=0, phase_name=run_model.getPhaseName()), self, ) self._total_progress_bar = QProgressBar(self) self._total_progress_bar.setRange(0, 100) self._total_progress_bar.setTextVisible(False) self._iteration_progress_label = QLabel(self) self._progress_view = ProgressView(self) self._progress_view.setModel(progress_proxy_model) self._progress_view.setIndeterminate(True) legend_view = LegendView(self) legend_view.setModel(progress_proxy_model) self._tab_widget = QTabWidget(self) self._tab_widget.currentChanged.connect(self._current_tab_changed) self._snapshot_model.rowsInserted.connect(self.on_new_iteration) self._job_label = QLabel(self) self._job_model = JobListProxyModel(self, 0, 0, 0, 0) self._job_model.setSourceModel(self._snapshot_model) self._job_view = QTableView(self) self._job_view.setVerticalScrollMode(QAbstractItemView.ScrollPerItem) self._job_view.setSelectionBehavior(QAbstractItemView.SelectRows) self._job_view.setSelectionMode(QAbstractItemView.SingleSelection) self._job_view.clicked.connect(self._job_clicked) self._open_files = {} self._job_view.setModel(self._job_model) self.running_time = QLabel("") self.plot_tool = PlotTool(config_file) self.plot_tool.setParent(self) self.plot_button = QPushButton(self.plot_tool.getName()) self.plot_button.clicked.connect(self.plot_tool.trigger) self.plot_button.setEnabled(ert is not None) self.kill_button = QPushButton("Kill Simulations") self.done_button = QPushButton("Done") self.done_button.setHidden(True) self.restart_button = QPushButton("Restart") self.restart_button.setHidden(True) self.show_details_button = QPushButton("Show Details") self.show_details_button.setCheckable(True) size = 20 spin_movie = resourceMovie("ide/loading.gif") spin_movie.setSpeed(60) spin_movie.setScaledSize(QSize(size, size)) spin_movie.start() self.processing_animation = QLabel() self.processing_animation.setMaximumSize(QSize(size, size)) self.processing_animation.setMinimumSize(QSize(size, size)) self.processing_animation.setMovie(spin_movie) button_layout = QHBoxLayout() button_layout.addWidget(self.processing_animation) button_layout.addWidget(self.running_time) button_layout.addStretch() button_layout.addWidget(self.show_details_button) button_layout.addWidget(self.plot_button) button_layout.addWidget(self.kill_button) button_layout.addWidget(self.done_button) button_layout.addWidget(self.restart_button) button_widget_container = QWidget() button_widget_container.setLayout(button_layout) layout = QVBoxLayout() layout.addWidget(self._total_progress_label) layout.addWidget(self._total_progress_bar) layout.addWidget(self._iteration_progress_label) layout.addWidget(self._progress_view) layout.addWidget(legend_view) layout.addWidget(self._tab_widget) layout.addWidget(self._job_label) layout.addWidget(self._job_view) layout.addWidget(button_widget_container) self.setLayout(layout) self.kill_button.clicked.connect(self.killJobs) self.done_button.clicked.connect(self.accept) self.restart_button.clicked.connect(self.restart_failed_realizations) self.show_details_button.clicked.connect(self.toggle_detailed_progress) self.simulation_done.connect(self._on_simulation_done) self.setMinimumWidth(self._minimum_width) self._setSimpleDialog() def _current_tab_changed(self, index: int): # Clear the selection in the other tabs for i in range(0, self._tab_widget.count()): if i != index: self._tab_widget.widget(i).clearSelection() def _setSimpleDialog(self) -> None: self._isDetailedDialog = False self._tab_widget.setVisible(False) self._job_label.setVisible(False) self._job_view.setVisible(False) self.show_details_button.setText("Show Details") def _setDetailedDialog(self) -> None: self._isDetailedDialog = True self._tab_widget.setVisible(True) self._job_label.setVisible(True) self._job_view.setVisible(True) self.show_details_button.setText("Hide Details") @Slot(QModelIndex, int, int) def on_new_iteration(self, parent: QModelIndex, start: int, end: int) -> None: if not parent.isValid(): index = self._snapshot_model.index(start, 0, parent) iter_row = start self._iteration_progress_label.setText( f"Progress for iteration {index.internalPointer().id}") widget = RealizationWidget(iter_row) widget.setSnapshotModel(self._snapshot_model) widget.currentChanged.connect(self._select_real) self._tab_widget.addTab( widget, f"Realizations for iteration {index.internalPointer().id}") @Slot(QModelIndex) def _job_clicked(self, index): if not index.isValid(): return selected_file = index.data(FileRole) if selected_file and selected_file not in self._open_files: job_name = index.siblingAtColumn(0).data() viewer = FileDialog( selected_file, job_name, index.row(), index.model().get_real(), index.model().get_iter(), self, ) self._open_files[selected_file] = viewer def remove_file(): """ We have sometimes seen this fail because the selected file is not in open file, without being able to reproduce the exception. """ try: self._open_files.pop(selected_file) except KeyError: logger = logging.getLogger(__name__) logger.exception( f"Failed to pop: {selected_file} from {self._open_files}" ) viewer.finished.connect(remove_file) elif selected_file in self._open_files: self._open_files[selected_file].raise_() @Slot(QModelIndex) def _select_real(self, index): step = 0 stage = 0 real = index.row() iter_ = index.model().get_iter() self._job_model.set_step(iter_, real, stage, step) self._job_label.setText( f"Realization id {index.data(RealIens)} in iteration {iter_}") self._job_view.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) def reject(self): return def closeEvent(self, QCloseEvent): if self._run_model.isFinished(): self.simulation_done.emit(self._run_model.hasRunFailed(), self._run_model.getFailMessage()) else: # Kill jobs if dialog is closed if self.killJobs() != QMessageBox.Yes: QCloseEvent.ignore() def startSimulation(self): self._run_model.reset() self._snapshot_model.reset() self._tab_widget.clear() def run(): asyncio.set_event_loop(asyncio.new_event_loop()) self._run_model.startSimulations(self._simulations_argments) simulation_thread = Thread(name="ert_gui_simulation_thread") simulation_thread.setDaemon(True) simulation_thread.run = run simulation_thread.start() self._ticker.start(1000) tracker = create_tracker( self._run_model, num_realizations=self._simulations_argments["active_realizations"]. count(), ee_config=self._simulations_argments.get("ee_config", None), ) worker = TrackerWorker(tracker) worker_thread = QThread() worker.done.connect(worker_thread.quit) worker.consumed_event.connect(self._on_tracker_event) worker.moveToThread(worker_thread) self.simulation_done.connect(worker.stop) self._worker = worker self._worker_thread = worker_thread worker_thread.started.connect(worker.consume_and_emit) self._worker_thread.start() def killJobs(self): msg = "Are you sure you want to kill the currently running simulations?" if self._run_model.getQueueStatus().get( JobStatusType.JOB_QUEUE_UNKNOWN, 0) > 0: msg += "\n\nKilling a simulation with unknown status will not kill the realizations already submitted!" kill_job = QMessageBox.question(self, "Kill simulations?", msg, QMessageBox.Yes | QMessageBox.No) if kill_job == QMessageBox.Yes: # Normally this slot would be invoked by the signal/slot system, # but the worker is busy tracking the evaluation. self._worker.request_termination() self.reject() return kill_job @Slot(bool, str) def _on_simulation_done(self, failed, failed_msg): self.processing_animation.hide() self.kill_button.setHidden(True) self.done_button.setHidden(False) self.restart_button.setVisible(self.has_failed_realizations()) self.restart_button.setEnabled(self._run_model.support_restart) self._total_progress_bar.setValue(100) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format( total_progress=100, phase_name=self._run_model.getPhaseName())) if failed: QMessageBox.critical( self, "Simulations failed!", f"The simulation failed with the following error:\n\n{failed_msg}", ) @Slot() def _on_ticker(self): runtime = self._run_model.get_runtime() self.running_time.setText(format_running_time(runtime)) @Slot(object) def _on_tracker_event(self, event): if isinstance(event, EndEvent): self.simulation_done.emit(event.failed, event.failed_msg) self._worker.stop() self._ticker.stop() elif isinstance(event, FullSnapshotEvent): if event.snapshot is not None: self._snapshot_model._add_snapshot(event.snapshot, event.iteration) self._progress_view.setIndeterminate(event.indeterminate) progress = int(event.progress * 100) self._total_progress_bar.setValue(progress) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(total_progress=progress, phase_name=event.phase_name)) elif isinstance(event, SnapshotUpdateEvent): if event.partial_snapshot is not None: self._snapshot_model._add_partial_snapshot( event.partial_snapshot, event.iteration) self._progress_view.setIndeterminate(event.indeterminate) progress = int(event.progress * 100) self._total_progress_bar.setValue(progress) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(total_progress=progress, phase_name=event.phase_name)) def has_failed_realizations(self): completed = self._run_model.completed_realizations_mask initial = self._run_model.initial_realizations_mask for (index, successful) in enumerate(completed): if initial[index] and not successful: return True return False def count_successful_realizations(self): """ Counts the realizations completed in the prevoius ensemble run :return: """ completed = self._run_model.completed_realizations_mask return completed.count(True) def create_mask_from_failed_realizations(self): """ Creates a BoolVector mask representing the failed realizations :return: Type BoolVector """ completed = self._run_model.completed_realizations_mask initial = self._run_model.initial_realizations_mask inverted_mask = BoolVector(default_value=False) for (index, successful) in enumerate(completed): inverted_mask[index] = initial[index] and not successful return inverted_mask def restart_failed_realizations(self): msg = QMessageBox(self) msg.setIcon(QMessageBox.Information) msg.setText( "Note that workflows will only be executed on the restarted realizations and that this might have unexpected consequences." ) msg.setWindowTitle("Restart Failed Realizations") msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) result = msg.exec_() if result == QMessageBox.Ok: self.restart_button.setVisible(False) self.kill_button.setVisible(True) self.done_button.setVisible(False) active_realizations = self.create_mask_from_failed_realizations() self._simulations_argments[ "active_realizations"] = active_realizations self._simulations_argments[ "prev_successful_realizations"] = self._simulations_argments.get( "prev_successful_realizations", 0) self._simulations_argments[ "prev_successful_realizations"] += self.count_successful_realizations( ) self.startSimulation() @Slot() def toggle_detailed_progress(self): if self._isDetailedDialog: self._setSimpleDialog() else: self._setDetailedDialog() self.adjustSize()
class ListItemContent(ListWidgetItemBase): """Widget to build an item for the content listing.""" def __init__(self, title='', description='', uri='', authors=None, venue='', path='', year='', summary='', banner='', tags='', subtitle='', date='', pixmap=None): """Widget to build an item for the content listing.""" super(ListItemContent, self).__init__() self.title = title self.uri = uri self.authors = authors if authors else [] self.venue = venue self.banner = banner self.year = year self.path = path self.tags = tags self.subtitle = subtitle self.date = date self.summary = summary self.timer_pixmap = QTimer() self.pixmaps = {} self.pixmap = pixmap self.label = None # Widgets self.widget = FrameContent() self.frame_hover = FrameContentHover(parent=self.widget) self.frame_body = FrameContentBody(parent=self.widget) self.frame_icon = FrameContentIcon(parent=self.widget) self.label_icon = LabelContentIcon(parent=self.widget) self.label_text = LabelContentTitle(parent=self.widget) self.button_text = ButtonContentText() # Widget setup self.button_text.setDefault(True) self.button_text.setAutoDefault(True) self.frame_hover.move(QPoint(5, 5)) self.frame_hover.label_icon = self.label_icon self.frame_hover.label_text = self.label_text self.frame_hover.button_text = self.button_text valid_tags = { 'documentation': 'Read', 'webinar': "Explore", 'event': "Learn More", 'video': "View", 'training': "Explore", 'forum': "Explore", 'social': "Engage" } self.tag = 'notag' filter_tags = [] if len(tags) >= 1: filter_tags = [t.lower() for t in tags if t.lower() in valid_tags] if filter_tags: self.tag = filter_tags[0].lower() self.widget.setObjectName(self.tag) self.button_text.setObjectName(self.tag) self.button_text.setText(valid_tags.get(self.tag, '')) self.label_icon.setAlignment(Qt.AlignHCenter) self.timer_pixmap.setInterval(random.randint(950, 1050)) if pixmap: self.update_thumbnail(pixmap=pixmap) # Layout if title: layout_icon = QVBoxLayout() layout_icon_h = QHBoxLayout() layout_icon_h.addWidget(self.label_icon) layout_icon.addStretch() layout_icon.addLayout(layout_icon_h) layout_icon.addStretch() self.frame_icon.setLayout(layout_icon) layout_frame = QVBoxLayout() layout_frame.addWidget(self.frame_icon) layout_frame.addStretch() layout_frame.addWidget(self.label_text) layout_frame.addStretch() layout_frame.addWidget(self.button_text) self.frame_body.setLayout(layout_frame) layout = QVBoxLayout() layout.addWidget(self.frame_body) self.widget.setLayout(layout) self.setSizeHint(self.widget_size()) self.widget.setMinimumSize(self.widget_size()) if summary: date = '<small>' + date + '</small><br>' if date else '' sub = '<small>' + subtitle + '</small><br>' if subtitle else '' tt = ('<p><b>' + title + '</b><br>' + sub + date + summary + '</p>') self.frame_hover.summary = tt else: self.frame_hover.summary = '<p><b>' + title + '</b><br>' # Signals self.frame_hover.sig_entered.connect( lambda: self.label_text.setProperty('active', True)) self.frame_hover.sig_left.connect( lambda: self.label_text.setProperty('active', False)) self.frame_hover.sig_entered.connect( lambda: self.button_text.setProperty('active', True)) self.frame_hover.sig_left.connect( lambda: self.button_text.setProperty('active', False)) self.timer_pixmap.timeout.connect(self.update_thumbnail) # Setup self.timer_pixmap.start() def ordered_widgets(self): """Return a list of the ordered widgets.""" return [self.button_text] def show_information(self): """Display additional information of item.""" if self.label: self.label.move(-1000, 0) self.label.show() app = QApplication.instance() geo = app.desktop().screenGeometry(self.button_information) w, h = geo.right(), geo.bottom() pos = self.button_information.mapToGlobal(QPoint(0, 0)) x, y = pos.x() + 10, pos.y() + 10 x = min(x, w - self.label.width()) y = min(y, h - self.label.height()) self.label.move(x, y) @staticmethod def widget_size(): """Return the size defined in the SASS file.""" return QSize(SASS_VARIABLES.WIDGET_CONTENT_TOTAL_WIDTH, SASS_VARIABLES.WIDGET_CONTENT_TOTAL_HEIGHT) def update_thumbnail(self, pixmap=None): """Update thumbnails image.""" height = SASS_VARIABLES.WIDGET_CONTENT_TOTAL_HEIGHT / 2 image_width = (SASS_VARIABLES.WIDGET_CONTENT_TOTAL_WIDTH - 2 * SASS_VARIABLES.WIDGET_CONTENT_PADDING - 2 * SASS_VARIABLES.WIDGET_CONTENT_MARGIN) # image_height = height * 1.666 if 'video' in self.tag else height pixmap = self.pixmaps.get(self.path) if pixmap and not pixmap.isNull(): self.pixmap = pixmap pix_width = self.pixmap.width() pix_height = self.pixmap.height() if pix_width * 1.0 / pix_height < image_width * 1.0 / height: max_height = height max_width = height * (pix_width / pix_height) else: max_height = image_width * (pix_height / pix_width) max_width = image_width self.label_icon.setScaledContents(True) self.label_icon.setMaximumWidth(max_width) self.label_icon.setMaximumHeight(max_height) self.label_icon.setPixmap(self.pixmap) self.timer_pixmap.stop()
class ProgressView(QWidget): """ :type batch_manager: CalculationManager """ def __init__(self, parent, batch_manager): super().__init__(parent) self.task_count = 0 self.calculation_manager = batch_manager self.whole_progress = QProgressBar(self) self.whole_progress.setMinimum(0) self.whole_progress.setMaximum(1) self.whole_progress.setFormat("%v of %m") self.whole_progress.setTextVisible(True) self.part_progress = QProgressBar(self) self.part_progress.setMinimum(0) self.part_progress.setMaximum(1) self.part_progress.setFormat("%v of %m") self.whole_label = QLabel("All batch progress:", self) self.part_label = QLabel("Single batch progress:", self) self.cancel_remove_btn = QPushButton("Remove task") self.cancel_remove_btn.setDisabled(True) self.logs = ExceptionList(self) self.logs.setToolTip("Logs") self.task_view = QListView() self.task_que = QStandardItemModel(self) self.task_view.setModel(self.task_que) self.process_num_timer = QTimer() self.process_num_timer.setInterval(1000) self.process_num_timer.setSingleShot(True) self.process_num_timer.timeout.connect(self.change_number_of_workers) self.number_of_process = QSpinBox(self) self.number_of_process.setRange(1, multiprocessing.cpu_count()) self.number_of_process.setValue(1) self.number_of_process.setToolTip( "Number of process used in batch calculation") self.number_of_process.valueChanged.connect( self.process_num_timer_start) self.progress_item_dict = {} layout = QGridLayout() layout.addWidget(self.whole_label, 0, 0, Qt.AlignRight) layout.addWidget(self.whole_progress, 0, 1, 1, 2) layout.addWidget(self.part_label, 1, 0, Qt.AlignRight) layout.addWidget(self.part_progress, 1, 1, 1, 2) lab = QLabel("Number of process:") lab.setToolTip("Number of process used in batch calculation") layout.addWidget(lab, 2, 0) layout.addWidget(self.number_of_process, 2, 1) layout.addWidget(self.logs, 3, 0, 2, 3) layout.addWidget(self.task_view, 0, 4, 4, 1) layout.addWidget(self.cancel_remove_btn, 4, 4, 1, 1) layout.setColumnMinimumWidth(2, 10) layout.setColumnStretch(2, 1) self.setLayout(layout) self.preview_timer = QTimer() self.preview_timer.setInterval(1000) self.preview_timer.timeout.connect(self.update_info) self.task_view.selectionModel().currentChanged.connect( self.task_selection_change) self.cancel_remove_btn.clicked.connect(self.task_cancel_remove) def task_selection_change(self, new, old): task: CalculationProcessItem = self.task_que.item( new.row(), new.column()) if task is None: self.cancel_remove_btn.setDisabled(True) return self.cancel_remove_btn.setEnabled(True) if task.is_finished(): self.cancel_remove_btn.setText(f"Remove task {task.num}") else: self.cancel_remove_btn.setText(f"Cancel task {task.num}") def task_cancel_remove(self): index = self.task_view.selectionModel().currentIndex() task: CalculationProcessItem = self.task_que.item( index.row(), index.column()) if task.is_finished(): self.calculation_manager.remove_calculation(task.calculation) self.task_que.takeRow(index.row()) else: self.calculation_manager.cancel_calculation(task.calculation) print(task) def new_task(self): self.whole_progress.setMaximum( self.calculation_manager.calculation_size) if not self.preview_timer.isActive(): self.update_info() self.preview_timer.start() def update_info(self): res = self.calculation_manager.get_results() for el in res.errors: if el[0]: QListWidgetItem(el[0], self.logs) ExceptionListItem(el[1], self.logs) if (state_store.report_errors and parsed_version.is_devrelease and not isinstance(el[1][0], SegmentationLimitException) and isinstance(el[1][1], tuple)): with sentry_sdk.push_scope() as scope: scope.set_tag("auto_report", "true") sentry_sdk.capture_event(el[1][1][0]) self.whole_progress.setValue(res.global_counter) working_search = True for uuid, progress in res.jobs_status.items(): calculation = self.calculation_manager.calculation_dict[uuid] total = len(calculation.file_list) if uuid in self.progress_item_dict: item = self.progress_item_dict[uuid] item.update_count(progress) else: item = CalculationProcessItem(calculation, self.task_count, progress) self.task_count += 1 self.task_que.appendRow(item) self.progress_item_dict[uuid] = item if working_search and progress != total: self.part_progress.setMaximum(total) self.part_progress.setValue(progress) working_search = False if not self.calculation_manager.has_work: self.part_progress.setValue(self.part_progress.maximum()) self.preview_timer.stop() logging.info("Progress stop") def process_num_timer_start(self): self.process_num_timer.start() def update_progress(self, total_progress, part_progress): self.whole_progress.setValue(total_progress) self.part_progress.setValue(part_progress) def set_total_size(self, size): self.whole_progress.setMaximum(size) def set_part_size(self, size): self.part_progress.setMaximum(size) def change_number_of_workers(self): self.calculation_manager.set_number_of_workers( self.number_of_process.value())
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)
class PluginManager(QObject): introspection_complete = Signal(object) def __init__(self, executable): super(PluginManager, self).__init__() plugins = OrderedDict() for name in PLUGINS: try: plugin = PluginClient(name, executable) plugin.run() except Exception as e: debug_print('Introspection Plugin Failed: %s' % name) debug_print(str(e)) continue debug_print('Introspection Plugin Loaded: %s' % name) plugins[name] = plugin plugin.received.connect(self.handle_response) self.plugins = plugins self.timer = QTimer() self.desired = [] self.ids = dict() self.info = None self.request = None self.pending = None self.pending_request = None self.waiting = False def send_request(self, info): """Handle an incoming request from the user.""" if self.waiting: if info.serialize() != self.info.serialize(): self.pending_request = info else: debug_print('skipping duplicate request') return debug_print('%s request' % info.name) desired = None self.info = info editor = info.editor if (info.name == 'completion' and 'jedi' not in self.plugins and info.line.lstrip().startswith(('import ', 'from '))): desired = 'fallback' if ((not editor.is_python_like()) or sourcecode.is_keyword(info.obj) or (editor.in_comment_or_string() and info.name != 'info')): desired = 'fallback' plugins = self.plugins.values() if desired: plugins = [self.plugins[desired]] self.desired = [desired] elif (info.name == 'definition' and not info.editor.is_python() or info.name == 'info'): self.desired = list(self.plugins.keys()) else: # Use all but the fallback plugins = list(self.plugins.values())[:-1] self.desired = list(self.plugins.keys())[:-1] self._start_time = time.time() self.waiting = True method = 'get_%s' % info.name value = info.serialize() self.ids = dict() for plugin in plugins: request_id = plugin.request(method, value) self.ids[request_id] = plugin.name self.timer.stop() self.timer.singleShot(LEAD_TIME_SEC * 1000, self._handle_timeout) def validate(self): for plugin in self.plugins.values(): plugin.request('validate') def handle_response(self, response): name = self.ids.get(response['request_id'], None) if not name: return if response.get('error', None): debug_print('Response error:', response['error']) return if name == self.desired[0] or not self.waiting: if response.get('result', None): self._finalize(response) else: self.pending = response def close(self): [plugin.close() for plugin in self.plugins] def _finalize(self, response): self.waiting = False self.pending = None if self.info: delta = time.time() - self._start_time debug_print('%s request from %s finished: "%s" in %.1f sec' % (self.info.name, response['name'], str(response['result'])[:100], delta)) response['info'] = self.info self.introspection_complete.emit(response) self.info = None if self.pending_request: info = self.pending_request self.pending_request = None self.send_request(info) def _handle_timeout(self): self.waiting = False if self.pending: self._finalize(self.pending) else: debug_print('No valid responses acquired')
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 QtLayerList(QScrollArea): def __init__(self, layers): super().__init__() self.layers = layers 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.dragTimer = QTimer() self.dragTimer.setSingleShot(False) self.dragTimer.setInterval(20) self.dragTimer.timeout.connect(self._force_scroll) self._scroll_up = True self._min_scroll_region = 24 self.setAcceptDrops(True) self.setToolTip('Layer list') self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.layers.events.added.connect(self._add) self.layers.events.removed.connect(self._remove) self.layers.events.reordered.connect(lambda e: self._reorder()) self.drag_start_position = np.zeros(2) self.drag_name = None def _add(self, event): """Insert widget for layer `event.item` at index `event.index`.""" layer = event.item 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`.""" 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) widget.deleteLater() self.vbox_layout.removeWidget(divider) divider.deleteLater() def _reorder(self): """Reorders list of layer widgets by looping through all widgets in list sequentially removing them and inserting them into the correct place in final list. """ 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.""" layer = event.source self._ensure_visible(layer) def _ensure_visible(self, layer): """Ensure layer widget for at particular layer is visible.""" 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): event.ignore() def keyReleaseEvent(self, event): event.ignore() def mousePressEvent(self, event): # Check if mouse press happens on a layer properties widget or # a child of such a widget. If not, the press has happended on the # Layers Widget itself and should be ignored. 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): 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): 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_() if self.drag_name is not None: index = self.layers.index(self.drag_name) layer = self.layers[index] self._ensure_visible(layer) def dragLeaveEvent(self, event): """Unselects layer dividers.""" event.ignore() self.dragTimer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) def dragEnterEvent(self, event): 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): """Set the appropriate layers list divider to be highlighted when dragging a layer to a new position in the layers list. """ max_height = self.frameGeometry().height() if (event.pos().y() < self._min_scroll_region and not self.dragTimer.isActive()): self._scroll_up = True self.dragTimer.start() elif (event.pos().y() > max_height - self._min_scroll_region and not self.dragTimer.isActive()): self._scroll_up = False self.dragTimer.start() elif (self.dragTimer.isActive() and event.pos().y() >= self._min_scroll_region and event.pos().y() <= max_height - self._min_scroll_region): self.dragTimer.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): if self.dragTimer.isActive(): self.dragTimer.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 EventEngine(object): """ 事件驱动引擎 事件驱动引擎中所有的变量都设置为了私有,这是为了防止不小心 从外部修改了这些变量的值或状态,导致bug。 变量说明 __queue:私有变量,事件队列 __active:私有变量,事件引擎开关 __thread:私有变量,事件处理线程 __timer:私有变量,计时器 __handlers:私有变量,事件处理函数字典 方法说明 __run: 私有方法,事件处理线程连续运行用 __process: 私有方法,处理事件,调用注册在引擎中的监听函数 __onTimer:私有方法,计时器固定事件间隔触发后,向事件队列中存入计时器事件 start: 公共方法,启动引擎 stop:公共方法,停止引擎 register:公共方法,向引擎中注册监听函数 unregister:公共方法,向引擎中注销监听函数 put:公共方法,向事件队列中存入新的事件 事件监听函数必须定义为输入参数仅为一个event对象,即: 函数 def func(event) ... 对象方法 def method(self, event) ... """ #---------------------------------------------------------------------- def __init__(self): """初始化事件引擎""" # 事件队列 self.__queue = Queue() # 事件引擎开关 self.__active = False # 事件处理线程 self.__thread = Thread(target = self.__run) # 计时器,用于触发计时器事件 self.__timer = QTimer() self.__timer.timeout.connect(self.__onTimer) # 这里的__handlers是一个字典,用来保存对应的事件调用关系 # 其中每个键对应的值是一个列表,列表中保存了对该事件进行监听的函数功能 self.__handlers = defaultdict(list) # __generalHandlers是一个列表,用来保存通用回调函数(所有事件均调用) self.__generalHandlers = [] #---------------------------------------------------------------------- def __run(self): """引擎运行""" while self.__active == True: try: event = self.__queue.get(block = True, timeout = 1) # 获取事件的阻塞时间设为1秒 self.__process(event) except Empty: pass #---------------------------------------------------------------------- def __process(self, event): """处理事件""" # 检查是否存在对该事件进行监听的处理函数 if event.type_ in self.__handlers: # 若存在,则按顺序将事件传递给处理函数执行 [handler(event) for handler in self.__handlers[event.type_]] # 以上语句为Python列表解析方式的写法,对应的常规循环写法为: #for handler in self.__handlers[event.type_]: #handler(event) # 调用通用处理函数进行处理 if self.__generalHandlers: [handler(event) for handler in self.__generalHandlers] #---------------------------------------------------------------------- def __onTimer(self): """向事件队列中存入计时器事件""" # 创建计时器事件 event = Event(type_=EVENT_TIMER) # 向队列中存入计时器事件 self.put(event) #---------------------------------------------------------------------- def start(self, timer=True): """ 引擎启动 timer:是否要启动计时器 """ # 将引擎设为启动 self.__active = True # 启动事件处理线程 self.__thread.start() # 启动计时器,计时器事件间隔默认设定为1秒 if timer: self.__timer.start(1000) #---------------------------------------------------------------------- def stop(self): """停止引擎""" # 将引擎设为停止 self.__active = False # 停止计时器 self.__timer.stop() # 等待事件处理线程退出 self.__thread.join() #---------------------------------------------------------------------- def register(self, type_, handler): """注册事件处理函数监听""" # 尝试获取该事件类型对应的处理函数列表,若无defaultDict会自动创建新的list handlerList = self.__handlers[type_] # 若要注册的处理器不在该事件的处理器列表中,则注册该事件 if handler not in handlerList: handlerList.append(handler) #---------------------------------------------------------------------- def unregister(self, type_, handler): """注销事件处理函数监听""" # 尝试获取该事件类型对应的处理函数列表,若无则忽略该次注销请求 handlerList = self.__handlers[type_] # 如果该函数存在于列表中,则移除 if handler in handlerList: handlerList.remove(handler) # 如果函数列表为空,则从引擎中移除该事件类型 if not handlerList: del self.__handlers[type_] #---------------------------------------------------------------------- def put(self, event): """向事件队列中存入事件""" self.__queue.put(event) #---------------------------------------------------------------------- def registerGeneralHandler(self, handler): """注册通用事件处理函数监听""" if handler not in self.__generalHandlers: self.__generalHandlers.append(handler) #---------------------------------------------------------------------- def unregisterGeneralHandler(self, handler): """注销通用事件处理函数监听""" if handler in self.__generalHandlers: self.__generalHandlers.remove(handler)
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 AsyncClient(QObject): """ A class which handles a connection to a client through a QProcess. """ # Emitted when the client has initialized. initialized = Signal() # Emitted when the client errors. errored = Signal() # Emitted when a request response is received. received = Signal(object) def __init__(self, target, executable=None, name=None, extra_args=None, libs=None, cwd=None, env=None): super(AsyncClient, self).__init__() self.executable = executable or sys.executable self.extra_args = extra_args self.target = target self.name = name or self self.libs = libs self.cwd = cwd self.env = env self.is_initialized = False self.closing = False self.context = zmq.Context() QApplication.instance().aboutToQuit.connect(self.close) # Set up the heartbeat timer. self.timer = QTimer(self) self.timer.timeout.connect(self._heartbeat) def run(self): """Handle the connection with the server. """ # Set up the zmq port. self.socket = self.context.socket(zmq.PAIR) self.port = self.socket.bind_to_random_port('tcp://*') # Set up the process. self.process = QProcess(self) if self.cwd: self.process.setWorkingDirectory(self.cwd) p_args = ['-u', self.target, str(self.port)] if self.extra_args is not None: p_args += self.extra_args # Set up environment variables. processEnvironment = QProcessEnvironment() env = self.process.systemEnvironment() if (self.env and 'PYTHONPATH' not in self.env) or DEV: python_path = osp.dirname(get_module_path('spyderlib')) # Add the libs to the python path. for lib in self.libs: try: path = osp.dirname(imp.find_module(lib)[1]) python_path = osp.pathsep.join([python_path, path]) except ImportError: pass env.append("PYTHONPATH=%s" % python_path) if self.env: env.update(self.env) for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) # Start the process and wait for started. self.process.start(self.executable, p_args) self.process.finished.connect(self._on_finished) running = self.process.waitForStarted() if not running: raise IOError('Could not start %s' % self) # Set up the socket notifer. fid = self.socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self._on_msg_received) def request(self, func_name, *args, **kwargs): """Send a request to the server. The response will be a dictionary the 'request_id' and the 'func_name' as well as a 'result' field with the object returned by the function call or or an 'error' field with a traceback. """ if not self.is_initialized: return request_id = uuid.uuid4().hex request = dict(func_name=func_name, args=args, kwargs=kwargs, request_id=request_id) self._send(request) return request_id def close(self): """Cleanly close the connection to the server. """ self.closing = True self.is_initialized = False self.timer.stop() self.notifier.activated.disconnect(self._on_msg_received) self.notifier.setEnabled(False) del self.notifier self.request('server_quit') self.process.waitForFinished(1000) self.process.close() self.context.destroy() def _on_finished(self): """Handle a finished signal from the process. """ if self.closing: return if self.is_initialized: debug_print('Restarting %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.is_initialized = False self.notifier.setEnabled(False) self.run() else: debug_print('Errored %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.errored.emit() def _on_msg_received(self): """Handle a message trigger from the socket. """ self.notifier.setEnabled(False) while 1: try: resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) except zmq.ZMQError: self.notifier.setEnabled(True) return if not self.is_initialized: self.is_initialized = True debug_print('Initialized %s' % self.name) self.initialized.emit() self.timer.start(HEARTBEAT) continue resp['name'] = self.name self.received.emit(resp) def _heartbeat(self): """Send a heartbeat to keep the server alive. """ self._send(dict(func_name='server_heartbeat')) def _send(self, obj): """Send an object to the server. """ try: self.socket.send_pyobj(obj) except Exception as e: debug_print(e) self.is_initialized = False self._on_finished()
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 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 _ClientAPI(QObject): """Anaconda Client API wrapper.""" DEFAULT_TIMEOUT = 6 def __init__(self, config=None): """Anaconda Client API wrapper.""" super(QObject, self).__init__() self._conda_api = CondaAPI() self._anaconda_client_api = None self._config = config self._queue = deque() self._threads = [] self._workers = [] self._timer = QTimer() self.config = CONF self._timer.setInterval(1000) self._timer.timeout.connect(self._clean) # Setup self.reload_binstar_client() 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): """Take avalaible worker from the queue and start it.""" 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 _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 # --- Callbacks # ------------------------------------------------------------------------- @staticmethod def _load_repodata(repodata, metadata=None, python_version=None): """ Load all the available package information. See load_repadata for full documentation. """ metadata = metadata if metadata else {} # python_version = '.'.join(python_version.split('.')[:2]) all_packages = {} for channel_url, data in repodata.items(): packages = data.get('packages', {}) for canonical_name in packages: data = packages[canonical_name] # Do not filter based on python version # if (python_version and not is_dependency_met( # data['depends'], python_version, 'python')): # continue 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'): all_packages[name]['type'][version] = data.get('type') all_packages[name]['app_entry'][version] = data.get( 'app_entry') all_packages[name]['app_type'][version] = data.get( 'app_type') # Calculate the correct latest_version for name in all_packages: versions = tuple( sorted(all_packages[name]['versions'], reverse=True)) all_packages[name]['latest_version'] = versions[0] 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') # 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 @staticmethod def _prepare_model_data(packages, linked, pip=None): """Prepare model data for the packages table model.""" pip = pip if pip else [] data = [] 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())), )) for name in packages_names: p_data = packages.get(name) 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 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_ACTION_VERSION: None, } data.append(row) return data def _get_user_licenses(self, products=None): """Get user trial/paid licenses from anaconda.org.""" license_data = [] try: res = self._anaconda_client_api.user_licenses() license_data = res.get('data', []) # This should be returning a dict or list not a json string! if is_text_string(license_data): license_data = json.loads(license_data) except Exception: time.sleep(0.3) return license_data # --- Public API # ------------------------------------------------------------------------- def reload_binstar_client(self): """ Recreate the binstar client with new updated values. Notes: ------ The Client needs to be restarted because on domain change it will not validate the user since it will check against the old domain, which was used to create the original client. See: https://github.com/ContinuumIO/navigator/issues/1325 """ config = binstar_client.utils.get_config() token = self.load_token() binstar = binstar_client.utils.get_server_api(token=token, site=None, cls=None, config=config, log_level=logging.NOTSET) self._anaconda_client_api = binstar return binstar def token(self): """Return the current token registered with authenticate.""" return self._anaconda_client_api.token def load_token(self): """Load current authenticated token.""" token = None try: token = binstar_client.utils.load_token(self.get_api_url()) except Exception: pass return token def _login(self, username, password, application, application_url): """Login callback.""" new_token = self._anaconda_client_api.authenticate( username, password, application, application_url) args = Args() args.site = None args.token = new_token binstar_client.utils.store_token(new_token, args) return new_token def login(self, username, password, application, application_url): """Login to anaconda server.""" logger.debug(str((username, application, application_url))) method = self._login return self._create_worker(method, username, password, application, application_url) def logout(self): """ Logout from anaconda.org. This method removes the authentication and removes the token. """ error = None args = Args() args.site = None args.token = self.token binstar_client.utils.remove_token(args) if self.token: try: self._anaconda_client_api.remove_authentication() except binstar_client.errors.Unauthorized as e: error = e logger.debug("The token that you are trying to remove may " "not be valid {}".format(e)) except Exception as e: error = e logger.debug("The certificate might be invalid. {}".format(e)) logger.info("logout successful") return error def load_repodata(self, repodata, metadata=None, python_version=None): """ Load all the available packages information for downloaded repodata. For downloaded repodata files (repo.anaconda.com), additional data provided (anaconda cloud), and additional metadata and merge into a single set of packages and apps. If python_version is not none, exclude all package/versions which require an incompatible version of python. Parameters ---------- repodata: dict of dicts Data loaded from the conda cache directories. metadata: dict Metadata info form different sources. For now only from repo.anaconda.com python_version: str Python version used in preprocessing. """ logger.debug('') method = self._load_repodata return self._create_worker( method, repodata, metadata=metadata, python_version=python_version, ) def prepare_model_data(self, packages, linked, pip=None): """Prepare downloaded package info along with pip pacakges info.""" logger.debug('') method = self._prepare_model_data return self._create_worker( method, packages, linked, pip=pip, ) def set_domain(self, domain='https://api.anaconda.org'): """Reset current api domain.""" logger.debug('Setting domain {}'.format(domain)) config = binstar_client.utils.get_config() config['url'] = domain try: binstar_client.utils.set_config(config) except binstar_client.errors.BinstarError: logger.error('Could not write anaconda client configuation') traceback = format_exc() msg_box = MessageBoxError( title='Anaconda Client configuration error', text='Anaconda Client domain could not be updated.<br><br>' 'This may result in Navigator not working properly.<br>', error='<pre>' + traceback + '</pre>', report=False, learn_more=None, ) msg_box.exec_() self._anaconda_client_api = binstar_client.utils.get_server_api( token=None, log_level=logging.NOTSET, ) def user(self): """Return current logged user information.""" return self.organizations(login=None) def domain(self): """Return current domain.""" return self._anaconda_client_api.domain def packages(self, login=None, platform=None, package_type=None, type_=None, access=None): """Return all the available packages for a given user. Parameters ---------- type_: Optional[str] Only find packages that have this conda `type`, (i.e. 'app'). access : Optional[str] Only find packages that have this access level (e.g. 'private', 'authenticated', 'public'). """ 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 organizations(self, login): """List all the organizations a user has access to.""" try: user = self._anaconda_client_api.user(login=login) except Exception: user = {} return user @staticmethod def get_api_url(): """Get the anaconda client url configuration.""" config_data = binstar_client.utils.get_config() return config_data.get('url', 'https://api.anaconda.org') @staticmethod def set_api_url(url): """Set the anaconda client url configuration.""" config_data = binstar_client.utils.get_config() config_data['url'] = url try: binstar_client.utils.set_config(config_data) except Exception as e: logger.error('Could not write anaconda client configuration') msg_box = MessageBoxError( title='Anaconda Client configuration error', text='Anaconda Client configuration could not be updated.<br>' 'This may result in Navigator not working properly.<br>', error=e, report=False, learn_more=None, ) msg_box.exec_() def get_ssl(self, set_conda_ssl=True): """ Get the anaconda client url configuration and set conda accordingly. """ config = binstar_client.utils.get_config() value = config.get('ssl_verify', config.get('ssl_verify', True)) # ssl_verify = self._conda_api.config_get('ssl_verify').communicate() # if ssl_verify != value: # FIXME: Conda rstricted acces to the key # self._conda_api.config_set('ssl_verify', value).communicate() if set_conda_ssl: self._conda_api.config_set('ssl_verify', value).communicate() return value def set_ssl(self, value): """Set the anaconda client url configuration.""" config_data = binstar_client.utils.get_config() config_data['verify_ssl'] = value config_data['ssl_verify'] = value try: binstar_client.utils.set_config(config_data) self._conda_api.config_set('ssl_verify', value).communicate() except Exception as e: logger.error('Could not write anaconda client configuration') msg_box = MessageBoxError( title='Anaconda Client configuration error', text='Anaconda Client configuration could not be updated.<br>' 'This may result in Navigator not working properly.<br>', error=e, report=False, learn_more=None, ) msg_box.exec_() def get_user_licenses(self, products=None): """Get user trial/paid licenses from anaconda.org.""" logger.debug(str((products))) method = self._get_user_licenses return self._create_worker(method, products=products) def _get_api_info(self, url, proxy_servers=None, verify=True): """Callback.""" proxy_servers = proxy_servers or {} data = { "api_url": url, "api_docs_url": "https://api.anaconda.org/docs", "brand": DEFAULT_BRAND, "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=proxy_servers, verify=verify, timeout=self.DEFAULT_TIMEOUT, ) content = to_text_string(r.content, encoding='utf-8') new_data = json.loads(content) # Enforce no trailing slash for key, value in new_data.items(): if is_text_string(value): data[key] = value[:-1] if value[-1] == '/' else value except Exception as error: logger.error(str(error)) return data def get_api_info(self, url, proxy_servers=None, verify=True): """Query anaconda api info.""" logger.debug(str((url))) proxy_servers = proxy_servers or {} method = self._get_api_info return self._create_worker(method, url, proxy_servers=proxy_servers, verify=verify)
class PyDMEmbeddedDisplay(QFrame, PyDMPrimitiveWidget): """ A QFrame capable of rendering a PyDM Display Parameters ---------- parent : QWidget The parent widget for the Label """ def __init__(self, parent=None): QFrame.__init__(self, parent) PyDMPrimitiveWidget.__init__(self) self.app = QApplication.instance() self._filename = None self._macros = None self._embedded_widget = None self._disconnect_when_hidden = True self._is_connected = False self._only_load_when_shown = True self._needs_load = True self._load_error_timer = None self._load_error = None self.layout = QVBoxLayout(self) self.err_label = QLabel(self) self.err_label.setAlignment(Qt.AlignHCenter) self.layout.addWidget(self.err_label) self.layout.setContentsMargins(0, 0, 0, 0) self.err_label.hide() def init_for_designer(self): self.setFrameShape(QFrame.Box) def minimumSizeHint(self): """ This property holds the recommended minimum size for the widget. Returns ------- QSize """ # This is totally arbitrary, I just want *some* visible nonzero size return QSize(100, 100) @Property(str) def macros(self): """ JSON-formatted string containing macro variables to pass to the embedded file. Returns ------- str """ if self._macros is None: return "" return self._macros @macros.setter def macros(self, new_macros): """ JSON-formatted string containing macro variables to pass to the embedded file. .. warning:: If the macros property is not defined before the filename property, The widget will not have any macros defined when it loads the embedded file. This behavior will be fixed soon. Parameters ---------- new_macros : str """ new_macros = str(new_macros) if new_macros != self._macros: self._macros = new_macros self._needs_load = True self.load_if_needed() @Property(str) def filename(self): """ Filename of the display to embed. Returns ------- str """ if self._filename is None: return "" return self._filename @filename.setter def filename(self, filename): """ Filename of the display to embed. Parameters ---------- filename : str """ filename = str(filename) if filename != self._filename: self._filename = filename self._needs_load = True if is_qt_designer(): if self._load_error_timer: # Kill the timer here. If new filename still causes the problem, it will be restarted self._load_error_timer.stop() self._load_error_timer = None self.clear_error_text() self.load_if_needed() def parsed_macros(self): """ Dictionary containing the key value pair for each macro specified. Returns -------- dict """ parent_display = self.find_parent_display() parent_macros = {} if parent_display: parent_macros = copy.copy(parent_display.macros()) widget_macros = macro.parse_macro_string(self.macros) parent_macros.update(widget_macros) return parent_macros def load_if_needed(self): if self._needs_load and (not self._only_load_when_shown or self.isVisible() or is_qt_designer()): self.embedded_widget = self.open_file() def open_file(self, force=False): """ Opens the widget specified in the widget's filename property. Returns ------- display : QWidget """ if (not force) and (not self._needs_load): return if not self.filename: return try: parent_display = self.find_parent_display() base_path = "" if parent_display: base_path = os.path.dirname(parent_display.loaded_file()) fname = find_file(self.filename, base_path=base_path) w = load_file(fname, macros=self.parsed_macros(), target=None) self._needs_load = False self.clear_error_text() return w except Exception as e: self._load_error = e if self._load_error_timer: self._load_error_timer.stop() self._load_error_timer = QTimer(self) self._load_error_timer.setSingleShot(True) self._load_error_timer.setTimerType(Qt.VeryCoarseTimer) self._load_error_timer.timeout.connect( self._display_designer_load_error) self._load_error_timer.start(1000) return None def clear_error_text(self): if self._load_error_timer: self._load_error_timer.stop() self.err_label.clear() self.err_label.hide() def display_error_text(self, e): self.err_label.setText( "Could not open {filename}.\nError: {err}".format( filename=self._filename, err=e)) self.err_label.show() @property def embedded_widget(self): """ The embedded widget being displayed. Returns ------- QWidget """ return self._embedded_widget @embedded_widget.setter def embedded_widget(self, new_widget): """ Defines the embedded widget to display inside the QFrame Parameters ---------- new_widget : QWidget """ should_reconnect = False if new_widget is self._embedded_widget: return if self._embedded_widget is not None: self.layout.removeWidget(self._embedded_widget) self._embedded_widget.deleteLater() self._embedded_widget = None if new_widget is not None: self._embedded_widget = new_widget self._embedded_widget.setParent(self) self.layout.addWidget(self._embedded_widget) self.err_label.hide() self._embedded_widget.show() self._is_connected = True def connect(self): """ Establish the connection between the embedded widget and the channels associated with it. """ if self._is_connected or self.embedded_widget is None: return establish_widget_connections(self.embedded_widget) self._is_connected = True def disconnect(self): """ Disconnects the embedded widget from the channels associated with it. """ if not self._is_connected or self.embedded_widget is None: return close_widget_connections(self.embedded_widget) self._is_connected = False @Property(bool) def loadWhenShown(self): """ If True, only load and display the file once the PyDMEmbeddedDisplayWidget is visible on screen. This is very useful if you have many different PyDMEmbeddedWidgets in different tabs of a QTabBar or PyDMTabBar: only the tab that the user is looking at will be loaded, which can greatly speed up the launch time of a display. If this property is changed from 'True' to 'False', and the file has not been loaded yet, it will be loaded immediately. Returns ------- bool """ return self._only_load_when_shown @loadWhenShown.setter def loadWhenShown(self, val): self._only_load_when_shown = val self.load_if_needed() @Property(bool) def disconnectWhenHidden(self): """ Disconnect from PVs when this widget is not visible. Returns ------- bool """ return self._disconnect_when_hidden @disconnectWhenHidden.setter def disconnectWhenHidden(self, disconnect_when_hidden): """ Disconnect from PVs when this widget is not visible. Parameters ---------- disconnect_when_hidden : bool """ self._disconnect_when_hidden = disconnect_when_hidden def showEvent(self, e): """ Show events are sent to widgets that become visible on the screen. Parameters ---------- event : QShowEvent """ if self._only_load_when_shown: w = self.open_file() if w: self.embedded_widget = w if self.disconnectWhenHidden: self.connect() def hideEvent(self, e): """ Hide events are sent to widgets that become invisible on the screen. Parameters ---------- event : QHideEvent """ if self.disconnectWhenHidden: self.disconnect() def _display_designer_load_error(self): self._load_error_timer = None logger.exception("Exception while opening embedded display file.", exc_info=self._load_error) if self._load_error: self.display_error_text(self._load_error)
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 ProcessWorker(QObject): """Process worker based on a QProcess for non blocking UI.""" sig_started = Signal(object) sig_partial = Signal(object, object, object) sig_finished = 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._partial_stderr = 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()) # raw_stderr = self._process.readAllStandardError() # stderr = handle_qbytearray(raw_stderr, self._get_encoding()) if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout # if self._partial_stderr is None: # self._partial_stderr = stderr # else: # self._partial_stderr += stderr # FIXME: use the piece or the cummulative? 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 if self._partial_stderr is None: raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, enco) else: stderr = self._partial_stderr if PY2: stdout = stdout.decode() stderr = stderr.decode() result = [stdout, stderr] 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: # print(self._cmd_list) self._partial_ouput = None if self._cmd_list: 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 write(self, data): if self._started: self._process.write(data) def start(self): """Start worker.""" if not self._started: self.sig_started.emit(self) self._started = True
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 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 avoid 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): """Dereference workers in workers bag periodically.""" 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 QtFrameRate(QLabel): """A frame rate label with green/yellow/red LEDs. The LED values are logarithmic, so a lot of detail between 60Hz and 10Hz but then the highest LED is many seconds long. """ def __init__(self): super().__init__() self.leds = LedState() # The per-LED config and state. # The last time we were updated, either from mouse move or our # timer. We update _last_time in both cases. self._last_time: Optional[float] = None # The bitmap image we draw into. self._image = np.zeros(BITMAP_SHAPE, dtype=np.uint8) # We animate on camera movements, but then we use a timer to # animate the display. When all the LEDs go off, we stop the timer # so that we use zero CPU until another camera movement. self._timer = QTimer() self._timer.setSingleShot(False) self._timer.setInterval(33) self._timer.timeout.connect(self._on_timer) # _print_calibration() # Debugging. def _on_timer(self) -> None: """Animate the LEDs.""" now = time.time() self._draw(now) # Just animate and draw, no new peak. # Stop timer if nothing more to animation, save CPU. if self.leds.all_off(): self._timer.stop() def _draw(self, now: float) -> None: """Animate the LEDs. Parameters ---------- now : float The current time in seconds. """ self.leds.update(now) # Animates the LEDs. self._update_image(now) # Draws our internal self._image self._update_bitmap() # Writes self._image into the QLabel bitmap. # We always update _last_time whether this was from a camera move # or the timer. This is the right thing to do since in either case # it means a frame was drawn. self._last_time = now def on_camera_move(self) -> None: """Update our display to show the new framerate.""" # Only count this frame if the timer is active. This avoids # displaying a potentially super long frame since it might have # been many seconds or minutes since the last camera movement. # # Ideally we should display the draw time of even that first frame, # but there's no easy/obvious way to do that today. And this will # show everthing except one frame. first_time = self._last_time is None use_delta = self._timer.isActive() and not first_time now = time.time() if use_delta: delta_seconds = now - self._last_time self.leds.set_peak(now, delta_seconds) self._draw(now) # Draw the whole meter. # Since there was activity, we need to start the timer so we can # animate the decay of the LEDs. The timer will be shut off when # all the LEDs go idle. self._timer.start() def _update_image(self, now: float) -> None: """Update our self._image with the latest meter display. Parameters ---------- now : float The current time in seconds. """ self._image.fill(0) # Start fresh each time. # Get colors with latest alpha values accord to decay. colors = self.leds.get_colors(now) # Draw each segment with the right color and alpha (due to decay). for index in range(NUM_SEGMENTS): x0 = int(index * SEGMENT_SPACING) x1 = int(x0 + SEGMENT_WIDTH) y0, y1 = 0, BITMAP_SHAPE[0] # The whole height of the bitmap. self._image[y0:y1, x0:x1] = colors[index] def _update_bitmap(self) -> None: """Update the bitmap with latest image data.""" height, width = BITMAP_SHAPE[:2] image = QImage(self._image, width, height, QImage.Format_RGBA8888) self.setPixmap(QPixmap.fromImage(image))
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 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'), tip=_("Find previous")) self.next_button = create_toolbutton(self, triggered=self.find_next, icon=ima.icon('ArrowDown'), tip=_("Find next")) 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('regex'), tip=_("Regular expression")) self.re_button.setCheckable(True) self.re_button.toggled.connect(lambda state: self.find()) self.case_button = create_toolbutton( self, icon=ima.icon("format_letter_case"), 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() case = self.case_button.isChecked() word = self.words_button.isChecked() regexp = self.re_button.isChecked() self.editor.highlight_found_results(text, word=word, regexp=regexp, case=case) 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(): sel_text = self.editor.get_selected_text() if len(to_text_string(sel_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() word = self.words_button.isChecked() regexp = self.re_button.isChecked() found = self.editor.find_text(text, changed, forward, case=case, word=word, 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, word=word) if hasattr(self.editor, 'get_match_number'): match_number = self.editor.get_match_number(text, case=case, regexp=regexp, word=word) else: match_number = 0 self.change_number_matches(current_match=match_number, total_matches=number_matches) return found
class XicamSplashScreen(QSplashScreen): minsplashtime = 5000 def __init__(self, log_path: str, initial_length: int, f: int = Qt.WindowStaysOnTopHint | Qt.SplashScreen): """ A QSplashScreen customized to display an animated gif. The splash triggers launch when clicked. After minsplashtime, this splash waits until the animation finishes before triggering the launch. Parameters ---------- log_path : str Path to the Xi-CAM log file to reflect initial_length: int Length in bytes to seek forward before reading f : int Extra flags (see base class) """ # Get logo movie from relative path self.movie = QMovie(str(static.path("images/animated_logo.gif"))) # Setup drawing self.movie.frameChanged.connect(self.paintFrame) self.movie.jumpToFrame(1) self.pixmap = QPixmap(self.movie.frameRect().size()) super(XicamSplashScreen, self).__init__(self.pixmap, f) self.setMask(self.pixmap.mask()) self.movie.finished.connect(self.restartmovie) self.showMessage('Starting Xi-CAM...') self._launching = False self._launchready = False self.timer = QTimer(self) self.log_file = open(log_path, 'r') self.log_file.seek(initial_length) # Start splashing self.setAttribute(Qt.WA_DeleteOnClose) self.show() self.raise_() self.activateWindow() QApplication.instance().setActiveWindow(self) # Setup timed triggers for launching the QMainWindow self.timer.singleShot(self.minsplashtime, self.launchwindow) def showMessage(self, message: str, color=Qt.darkGray): # TODO: Make this work. super(XicamSplashScreen, self).showMessage(message, color=color, alignment=Qt.AlignBottom) def mousePressEvent(self, *args, **kwargs): # TODO: Apparently this doesn't work? self.timer.stop() self.execlaunch() def show(self): """ Start the animation when shown """ super(XicamSplashScreen, self).show() self.movie.start() def paintFrame(self): """ Paint the current frame """ self.pixmap = self.movie.currentPixmap() self.setMask(self.pixmap.mask()) self.setPixmap(self.pixmap) self.movie.setSpeed(self.movie.speed() + 20) line = self.log_file.readline().strip() if line: self.showMessage(elide(line.split('>')[-1])) def sizeHint(self): return self.movie.scaledSize() def restartmovie(self): """ Once the animation reaches the end, check if its time to launch, otherwise restart animation """ if self._launchready: self.execlaunch() return self.movie.start() def launchwindow(self): """ Save state, defer launch until animation finishes """ self._launchready = True def execlaunch(self): """ Launch the mainwindow """ if not self._launching: self._launching = True self.timer.stop() # Stop splashing self.hide() self.movie.stop() self.close() QApplication.instance().quit()
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): super().__init__(parent) self.events = EmitterGroup(source=self, poll=None) self.timer = QTimer() self.timer.setInterval(POLL_INTERVAL_MS) self.timer.timeout.connect(self._on_timer) self._interval = IntervalTimer() def on_camera(self) -> None: """Called when camera view changes.""" # When the mouse button is down and the camera is being zoomed # or panned, timer events are starved out. So we call poll # explicitly here. It will start the timer if needed so that # polling can continue even after the camera stops moving. self._poll() def wake_up(self) -> None: """Wake up QtPoll so it starts polling.""" # Start the timer so that we start polling. We used to poll once # right away here, but it led to crashes. Because we polled during # a paintGL event? if not self.timer.isActive(): 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.""" # Between timers and camera and wake_up() we might be called multiple # times in quick succession. Use an IntervalTimer to ignore these # near-duplicate calls. if self._interval.elapsed_ms < IGNORE_INTERVAL_MS: return # Poll all listeners. 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 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.""" ImageView.__init__(self, parent) PyDMWidget.__init__(self) self._channels = [None, None] self.thread = None self.axes = dict({'t': None, "x": 0, "y": 1, "c": 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 # 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() # Set color map limits. self.cm_min = 0.0 self.cm_max = 255.0 # Set default reading order of numpy array data to Fortranlike. self._reading_order = ReadingOrder.Fortranlike # 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._colormap = PyDMColorMap.Inferno self._cm_colors = None 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._redraw_rate = 30 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): 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()) 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().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))