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 shutdown(self, shutdown_kernel=True): """Shutdown connection and kernel.""" if self.shutting_down: return self.shutting_down = True if shutdown_kernel: if not self.kernel_manager: return self.interrupt_kernel() if self.kernel_manager: self.kernel_manager.stop_restarter() self.spyder_kernel_comm.close() if self.kernel_client is not None: self.kernel_client.stop_channels() if self.kernel_manager: shutdown_thread = QThread(None) shutdown_thread.kernel_manager = self.kernel_manager shutdown_thread.run = self.shutdown_kernel self.shutdown_thread_list.append(shutdown_thread) shutdown_thread.start() else: self.spyder_kernel_comm.close(shutdown_channel=False) if self.kernel_client is not None: self.kernel_client.stop_channels() self.prune_shutdown_thread_list() super(ShellWidget, self).shutdown()
class PMQThreadManager(QObject): signal_server_message_received = Signal(dict) signal_data_changed = Signal(str) def __init__(self, parent=None, work_fcn: Callable = None, loop_fcn: Callable = None, exit_fcn: Callable = None): super().__init__(parent) self.thread_recv = QThread() self.worker_recv = PMGThreadWorker() if work_fcn is not None and loop_fcn is not None: raise ValueError( 'work_fcn and loop_fcn cannot be both not None at the same time!' ) if work_fcn is not None: self.worker_recv.work = work_fcn else: self.worker_recv.work_loop_fcn = loop_fcn self.worker_recv.moveToThread(self.thread_recv) self.thread_recv.started.connect( self.worker_recv.work) # self.worker_recv.work) self.worker_recv.on_exit_fcn = exit_fcn self.thread_recv.start() def shut_down(self): logger.info('client quit') self.worker_recv.on_exit() # self.worker_recv.close_socket() self.thread_recv.quit() self.thread_recv.wait(500)
def on_incoming_connection(self, socket_descriptor: int) -> None: """Create new client thread. Args: socket_descriptor (int): Socket descriptor. """ client_id = self.get_free_id() thread = QThread() thread.setObjectName(str(client_id)) client = _SocketClient(socket_descriptor, client_id) client.moveToThread(thread) thread.started.connect(client.run) # noqa client.connected.connect(self.connected.emit) client.message.connect(self.message.emit) client.error.connect(self.error.emit) client.disconnected.connect(self.on_client_disconnected) client.disconnected.connect(thread.quit) client.disconnected.connect(thread.wait) thread.finished.connect(self.on_thread_finished) self.clients.append(client) self.threads.append(thread) thread.start() self._logger.info("Started new client thread!") self._logger.debug("Active clients: {}".format(len([x for x in self.threads if x.isRunning()])))
class DamTerm(QObject): #Main Class def __init__(self, addr): super(self.__class__, self).__init__() self.start() @pyqtSlot(str) def take_info(self, _str): print("TAKE::INFO:: %s" % _str) with open("test", "w") as f: f.write("Test") self.info.write(_str) def test(self): print("TESTETT!") def start(self): print("Start!") self.GenUids = GenerateUids(self) self.th = QThread(self) self.th.setObjectName("GenerateUids") self.GenUids.moveToThread(self.th) self.GenUids.pasInfo.connect(self.take_info) self.th.started.connect(self.GenUids.run) self.th.start() while not self.th.isFinished(): time.sleep(1) print("while") print("Koniec startu")
class Main(QMainWindow): def __init__(self, tracker: Tracker) -> None: super().__init__() self.qttracker = QtTracker(tracker) self.bg_thread = QThread(self) self.qttracker.moveToThread(self.bg_thread) self.bg_thread.start() self.qttracker.start_recording() self.setCentralWidget(Controls(self.qttracker)) toolbar = QToolBar() self.addToolBar(toolbar) toolbar.setMovable(False) toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.new_action = QAction("New", self) self.new_action.setToolTip("Create a new file for tracking.") icon = self.style().standardIcon(QStyle.SP_FileIcon) self.new_action.setIcon(icon) self.new_action.triggered.connect(self.new_db) toolbar.addAction(self.new_action) self.load_action = QAction("Load", self) self.load_action.setToolTip("Load a existing file for tracking.") icon = self.style().standardIcon(QStyle.SP_DialogOpenButton) self.load_action.setIcon(icon) self.load_action.triggered.connect(self.load_db) toolbar.addAction(self.load_action) @Slot() def load_db(self): fname, ftype = QFileDialog.getOpenFileName( self, "Tracking files", str(Path.home()), "CamTrack Files (*.camtrack)", options=QFileDialog.DontUseNativeDialog) if fname != "": self.qttracker.tracker.open_db(fname) @Slot() def new_db(self): fname, ftype = QFileDialog.getSaveFileName( self, "New file", str(Path.home()), "CamTrack Files (*.camtrack)", options=QFileDialog.DontUseNativeDialog) if fname != 0: if (p := Path(fname).with_suffix('.camtrack')).exists(): p.unlink() self.qttracker.tracker.open_db(fname + '.camtrack')
def run(self): """ 运行代码 """ call_id_list = self.scene.topo_sort() self.scene.call_id_list = call_id_list thread = QThread() for node in self.scene.nodes: node.reset() node.content.moveToThread(thread) worker = self.scene.find_node(call_id_list[0]).content worker.moveToThread(thread) thread.started.connect(worker._process) thread.start() self.worker = worker self.thread = thread
def create_worker(self): """Creates new socket worker in thread.""" thread = QThread() worker = _SocketWorker() worker.moveToThread(thread) worker.connected.connect(self.connected.emit) # noqa worker.message.connect(self.message.emit) # noqa worker.disconnected.connect(self.disconnected.emit) # noqa worker.error.connect(self.error.emit) # noqa thread.started.connect(worker.start) # noqa worker.closed.connect(thread.quit) # noqa worker.closed.connect(thread.wait) # noqa self.workers.append(worker) self.threads.append(thread) thread.start()
class PMClient(QObject, BaseClient): signal_server_message_received = Signal(dict) signal_data_changed = Signal(str) def __init__(self, parent=None, name='Anonymous QtClient'): super().__init__(parent) self.name = name self.client = self.init_socket(12306) self.thread_recv = QThread() self.worker_recv = RecvWork(self.client, self.name) self.signal_received = self.worker_recv.signal_received self.worker_recv.signal_received.connect(self.on_server_message_received) self.worker_recv.moveToThread(self.thread_recv) self.thread_recv.started.connect(self.worker_recv.work) self.thread_recv.start() def shut_down(self): logger.info('client quit') self.worker_recv.close_socket() self.thread_recv.quit() self.thread_recv.wait(500) def on_server_message_received(self, packet: bytes): try: dic = json.loads(packet) logger.info(dic) msg = dic.get('message') if msg == 'data_changed': data_name = dic.get('data_name') if data_name is not None: self.signal_data_changed.emit(data_name) self.signal_server_message_received.emit(dic) except: import traceback traceback.print_exc() pass def send_bytes(self, packet: bytes): self.client.sendall(packet) @timeit def compress(self, byte_str): return zlib.compress(byte_str)
class PMGServer(QObject): def __init__(self, address: Tuple[str, int], parent=None): super().__init__(parent) self.dispatcher_dic = {} self.long_conn_sockets: Dict[str, socket.socket] = {} self.socket = init_socket(address) self.server_loop_thread = QThread() self.loop_worker = LoopWork(self, self.socket) self.loop_worker.moveToThread(self.server_loop_thread) self.server_loop_thread.started.connect(self.loop_worker.work) self.server_loop_thread.start() def broadcast_message(self, message_dic: dict = None): """ 广播信息 message:传递信息 # 'DATA_CHANGED' # 'SHUT_DOWN' :param message_dic:要发送的信息,应该是一个可以json序列化的字典。 :return: """ if message_dic is None: message_dic = {'name': 'broadcast', 'message': 'Are you alive?'} ids = [] logger.info('broadcast message:' + repr(message_dic)) for k in self.long_conn_sockets.keys(): try: self.long_conn_sockets[k].sendall( json.dumps(message_dic).encode('utf8') + b'PMEND') except ConnectionResetError: ids.append(k) logger.info('Connection \'%s\' closed!' % k) except: import traceback traceback.print_exc() ids.append(k) logger.info('died connections:' + repr(ids)) for not_used_socket_name in ids: sock = self.long_conn_sockets.pop(not_used_socket_name) sock.close()
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(None) 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() thread.wait() 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()
class PMQThreadObject(QObject): signal_server_message_received = Signal(dict) signal_data_changed = Signal(str) def __init__(self, parent=None, worker: QObject = None): super().__init__(parent) self.thread = QThread() self.worker = worker self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.work) # self.worker_recv.work) self.thread.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.thread.start() def terminate(self): logger.info('client quit') self.worker.on_exit() self.thread.quit() self.thread.wait(500)
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()
class PMGQThreadManager(QObject): signal_server_message_received = Signal(dict) signal_data_changed = Signal(str) signal_finished = Signal() def __init__(self, parent=None, worker: QObject = None): super().__init__(parent) self.thread_recv = QThread() self.worker_recv = worker self.worker_recv.moveToThread(self.thread_recv) self.thread_recv.started.connect(self.worker_recv.work) self.thread_recv.start() self.thread_recv.finished.connect(self.signal_finished.emit) def shut_down(self): """ 关闭线程,并且退出。 :return: """ self.worker_recv.on_exit() self.thread_recv.quit() self.thread_recv.wait(500)
class TimerThread(QObject): signal = Signal(str) def __init__(self, callback, milis): super().__init__() self.milis = milis self.signal.connect(callback) self.thread = QThread() self.timer = QTimer() #self.timer.timeout.connect(self.timeout) #self.timer.start(milis) self.thread.started.connect(self.init) def Start(self): self.thread.start() @Slot() def init(self): self.timer.timeout.connect(self.timeout) self.timer.start(self.milis) def timeout(self): self.signal.emit("tick")
class Runner: """ init -> register -> start init -> start -> register """ def __init__(self, task_cls: Type[Task], *args, **kwargs): super().__init__() self.task: Optional[Task] = None self.thread: Optional[QThread] = None self._started = False self.task_cls = task_cls self._args = args self._kwargs = kwargs def init(self) -> None: if self._started: raise RuntimeError("Cannot init runner when task already started!") self.thread = QThread() self.task = self.task_cls(*self._args, **self._kwargs) self.task.moveToThread(self.thread) self.task.finished.connect(self.task.deleteLater) self.task.finished.connect(self.thread.quit) self.task.failed.connect(self.fail) self.thread.started.connect( self.task.execute) # lambdas don't work here hm self.thread.finished.connect(self.thread.deleteLater) def register(self, *, started: Callable = None, finished: Callable = None, progress: Callable = None, failed: Callable = None, result: Callable = None) -> None: if started is not None: self.task.started.connect(started) if finished is not None: self.task.finished.connect(finished) if progress is not None: self.task._progressThrottled.connect(progress) if failed is not None: self.task.failed.connect(failed) if result is not None: self.task.result.connect(result) def start(self) -> None: self._started = True self.thread.start() def stop(self) -> None: if self.task is not None: self.task.stop() if self.thread is not None: try: self.thread.quit() self.thread.wait() except RuntimeError: pass self._started = False def fail(self, e: Exception) -> None: raise e @property def is_running(self) -> bool: """ Without this try-except block, Qt throws an error saying that the thread has already been deleted, even if `self.thread` is not None. """ is_running = False try: if self.thread is not None: is_running = self.thread.isRunning() except RuntimeError: pass return is_running
class KiteClient(QObject, KiteMethodProviderMixIn): sig_response_ready = Signal(int, dict) sig_client_started = Signal(list) sig_client_not_responding = Signal() sig_perform_request = Signal(int, str, object) sig_perform_status_request = Signal(str) sig_status_response_ready = Signal((str, ), (dict, )) sig_perform_onboarding_request = Signal() sig_onboarding_response_ready = Signal(str) def __init__(self, parent, enable_code_snippets=True): QObject.__init__(self, parent) self.endpoint = None self.requests = {} self.languages = [] self.mutex = QMutex() self.opened_files = {} self.opened_files_status = {} self.thread_started = False self.enable_code_snippets = enable_code_snippets self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_perform_request.connect(self.perform_request) self.sig_perform_status_request.connect(self.get_status) self.sig_perform_onboarding_request.connect(self.get_onboarding_file) def start(self): if not self.thread_started: self.thread.start() logger.debug('Starting Kite HTTP session...') self.endpoint = requests.Session() self.languages = self.get_languages() self.sig_client_started.emit(self.languages) def started(self): self.thread_started = True def stop(self): if self.thread_started: logger.debug('Closing Kite HTTP session...') self.endpoint.close() self.thread.quit() def get_languages(self): verb, url = KITE_ENDPOINTS.LANGUAGES_ENDPOINT success, response = self.perform_http_request(verb, url) if response is None: response = ['python'] return response def _get_onboarding_file(self): """Perform a request to get kite's onboarding file.""" verb, url = KITE_ENDPOINTS.ONBOARDING_ENDPOINT success, response = self.perform_http_request(verb, url) return response def get_onboarding_file(self): """Get onboarding file.""" onboarding_file = self._get_onboarding_file() self.sig_onboarding_response_ready.emit(onboarding_file) def _get_status(self, filename): """Perform a request to get kite status for a file.""" verb, url = KITE_ENDPOINTS.STATUS_ENDPOINT if filename: url_params = {'filename': filename} else: url_params = {'filetype': 'python'} success, response = self.perform_http_request(verb, url, url_params=url_params) return response def get_status(self, filename): """Get kite status for a given filename.""" kite_status = self._get_status(filename) if not filename or kite_status is None: kite_status = status() self.sig_status_response_ready[str].emit(kite_status) else: self.sig_status_response_ready[dict].emit(kite_status) def perform_http_request(self, verb, url, url_params=None, params=None): response = None http_method = getattr(self.endpoint, verb) try: http_response = http_method(url, params=url_params, json=params) except Exception as error: return False, None success = http_response.status_code == 200 if success: try: response = http_response.json() except Exception: response = http_response.text response = None if response == '' else response return success, response def send(self, method, params, url_params): response = None if self.endpoint is not None and method in KITE_REQUEST_MAPPING: http_verb, path = KITE_REQUEST_MAPPING[method] encoded_url_params = { key: quote(value) if isinstance(value, TEXT_TYPES) else value for (key, value) in url_params.items() } path = path.format(**encoded_url_params) try: success, response = self.perform_http_request(http_verb, path, params=params) except (ConnectionRefusedError, ConnectionError): return response return response def perform_request(self, req_id, method, params): response = None if method in self.sender_registry: logger.debug('Perform {0} request with id {1}'.format( method, req_id)) handler_name = self.sender_registry[method] handler = getattr(self, handler_name) response = handler(params) if method in self.handler_registry: converter_name = self.handler_registry[method] converter = getattr(self, converter_name) if response is not None: response = converter(response) self.sig_response_ready.emit(req_id, response or {})
class QBaseServer(QObject): """Server base for QtPyNetwork.""" connected = Signal(Device, str, int) disconnected = Signal(Device) message = Signal(Device, bytes) error = Signal(Device, str) server_error = Signal(str) closed = Signal() def __init__(self, loggerName=None): super(QBaseServer, self).__init__() if loggerName: self.__logger = logging.getLogger(loggerName) else: self.__logger = logging.getLogger(self.__class__.__name__) self.__ip = None self.__port = None self.__devices = [] self.__deviceModel = Device self.__handler = None self.__handler_thread = None self.__handlerClass = None self.__server = None @Slot(str, int, bytes) def start(self, ip: str, port: int): """Start server.""" if self.__handlerClass: ip = QHostAddress(ip) self.__ip = ip self.__port = port self.__handler = self.__handlerClass() self.__handler_thread = QThread() self.__handler.moveToThread(self.__handler_thread) self.__handler.connected.connect( self.__on_handler_successful_connection) self.__handler.message.connect(self.__on_handler_device_message) self.__handler.error.connect(self.__on_handler_device_error) self.__handler.disconnected.connect( self.__on_handler_device_disconnected) self.__handler.closed.connect(self.on_closed) self.__handler_thread.started.connect(self.__handler.start) self.__handler.started.connect(self.__setup_server) self.__handler_thread.start() else: raise Exception("Handler class not set!") @Slot() def __setup_server(self): """Create QTCPServer, start listening for connections.""" self.__server = TCPServer() self.__server.connection.connect(self.__handler.on_incoming_connection) if self.__server.listen(self.__ip, self.__port): self.__logger.info("Started listening for connections") else: e = self.__server.errorString() self.__logger.error(e) self.server_error.emit(e) @Slot(int, str, int) def __on_handler_successful_connection(self, device_id, ip, port): """When client connects to server successfully.""" device = self.__deviceModel(self, device_id, ip, port) self.__devices.append(device) self.__logger.info("Added new CLIENT-{} with address {}:{}".format( device_id, ip, port)) self.on_connected(device, ip, port) @Slot(int, bytes) def __on_handler_device_message(self, device_id: int, message: bytes): """When server receives message from bot.""" self.on_message(self.get_device_by_id(device_id), message) @Slot(int) def __on_handler_device_disconnected(self, device_id): """When bot disconnects from server.""" device = self.get_device_by_id(device_id) device.set_connected(False) if device in self.__devices: self.__devices.remove(device) self.on_disconnected(device) @Slot(int, str) def __on_handler_device_error(self, device_id, error): self.on_error(self.get_device_by_id(device_id), error) @Slot(Device, str, int) def on_connected(self, device: Device, ip: str, port: int): """Called when new client connects to server. Emits connected signal. Args: device (Device): Device object. ip (str): Client ip address. port (int): Client port. """ self.connected.emit(device, ip, port) @Slot(Device, bytes) def on_message(self, device: Device, message: bytes): """Called when server receives message from client. Emits message signal. Args: device (Device): Message sender. message (bytes): Message. """ self.message.emit(device, message) @Slot(Device) def on_disconnected(self, device: Device): """Called when device disconnects from server. Emits disconnected signal. Args: device (Device): Disconnected device. """ self.disconnected.emit(device) @Slot(Device, str) def on_error(self, device: Device, error: str): """Called when a socket error occurs. Emits error signal. Args: device (Device): Device object. error (str): Error string. """ self.error.emit(device, error) @Slot() def on_closed(self): self.closed.emit() @Slot(Device, bytes) def write(self, device: Device, data: bytes): """Write data to device.""" if not self.__server or not self.__handler: raise ServerNotRunning("Server is not running") if not device.is_connected(): raise NotConnectedError("Client is not connected") self.__handler.write.emit(device.id(), data) @Slot(bytes) def write_all(self, data: bytes): """Write data to all devices.""" if not self.__server or not self.__handler: raise ServerNotRunning("Server is not running") self.__handler.write_all.emit(data) @Slot() def kick(self, device: Device): """Disconnect device from server.""" if not self.__server or not self.__handler: raise ServerNotRunning("Server is not running") if not device.is_connected(): raise NotConnectedError("Client is not connected") self.__handler.kick.emit(device.id()) @Slot() def close(self): """Disconnect clients and close server.""" self.__logger.info("Closing server...") if self.__server: self.__server.close() if self.__handler: self.__handler.close() self.__handler_thread.quit() def set_device_model(self, model): """Set model to use for device when client connects. Note: Model should be subclassing Device. """ if self.is_running(): raise Exception("Set device model before starting server!") if not issubclass(model, Device): raise ValueError("Model should be subclassing Device class.") try: model(QBaseServer(), 0, "127.0.0.1", 5000) except TypeError as e: raise TypeError( "Model is not valid class! Exception: {}".format(e)) self.__deviceModel = model def is_running(self): """Check if server is running.""" if self.__handler_thread: return self.__handler_thread.isRunning() return False def wait(self): """Wait for server thread to close.""" if self.__handler_thread: return self.__handler_thread.wait() return True @Slot(int) def get_device_by_id(self, device_id: int) -> Device: """Returns device with associated ID. Args: device_id (int): Device ID. """ for device in self.__devices: if device.id() == device_id: return device raise Exception("CLIENT-{} not found".format(device_id)) def get_devices(self): """Returns list with devices.""" return self.__devices def set_handler_class(self, handler): """Set handler to use. This should not be used outside this library.""" if self.is_running(): raise Exception("Set socket handler before starting server!") try: handler() except TypeError as e: raise TypeError( "Handler is not valid class! Exception: {}".format(e)) self.__handlerClass = handler
class KiteClient(QObject, KiteMethodProviderMixIn): sig_response_ready = Signal(int, dict) sig_client_started = Signal(list) sig_client_not_responding = Signal() sig_perform_request = Signal(int, str, object) MAX_SERVER_CONTACT_RETRIES = 40 def __init__(self, parent): QObject.__init__(self, parent) self.contact_retries = 0 self.endpoint = None self.requests = {} self.languages = [] self.mutex = QMutex() self.opened_files = {} self.alive = False self.thread_started = False self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_perform_request.connect(self.perform_request) def __wait_http_session_to_start(self): logger.debug('Waiting Kite HTTP endpoint to be available...') _, url = KITE_ENDPOINTS.ALIVE_ENDPOINT try: code = requests.get(url).status_code except Exception: code = 500 if self.contact_retries == self.MAX_SERVER_CONTACT_RETRIES: logger.debug('Kite server is not answering') self.sig_client_not_responding.emit() elif code != 200: self.contact_retries += 1 QTimer.singleShot(250, self.__wait_http_session_to_start) elif code == 200: self.alive = True self.start_client() def start(self): if not self.thread_started: self.thread.start() self.__wait_http_session_to_start() def start_client(self): logger.debug('Starting Kite HTTP session...') self.endpoint = requests.Session() self.languages = self.get_languages() self.sig_client_started.emit(self.languages) def started(self): self.thread_started = True def stop(self): if self.thread_started: logger.debug('Closing Kite HTTP session...') self.endpoint.close() self.thread.quit() def get_languages(self): verb, url = KITE_ENDPOINTS.LANGUAGES_ENDPOINT success, response = self.perform_http_request(verb, url) return response def perform_http_request(self, verb, url, params=None): response = None success = False http_method = getattr(self.endpoint, verb) http_response = http_method(url, json=params) success = http_response.status_code == 200 if success: try: response = http_response.json() except Exception: response = http_response.text response = None if response == '' else response return success, response def send(self, method, params, url_params): response = None if self.endpoint is not None and method in KITE_REQUEST_MAPPING: if self.alive: http_verb, path = KITE_REQUEST_MAPPING[method] path = path.format(**url_params) try: success, response = self.perform_http_request( http_verb, path, params) except (ConnectionRefusedError, ConnectionError): self.alive = False self.endpoint = None self.contact_retries = 0 self.__wait_http_session_to_start() return response return response def perform_request(self, req_id, method, params): if method in self.sender_registry: logger.debug('Perform {0} request with id {1}'.format( method, req_id)) handler_name = self.sender_registry[method] handler = getattr(self, handler_name) response = handler(params) if method in self.handler_registry: converter_name = self.handler_registry[method] converter = getattr(self, converter_name) response = converter(response) if response is not None: self.sig_response_ready.emit(req_id, response)
class FileDialog(QDialog): def __init__(self, file_name, job_name, job_number, realization, iteration, parent=None): super(FileDialog, self).__init__(parent) self.setWindowTitle("{} # {} Realization: {} Iteration: {}" \ .format(job_name, job_number, realization, iteration)) self._file_name = file_name try: self._file = open(file_name, "r") except OSError as error: self._mb = QMessageBox(QMessageBox.Critical, "Error opening file", error.strerror, QMessageBox.Ok, self) self._mb.finished.connect(self.accept) self._mb.show() return self._model = FileModel() self._view = FileView() self._view.setModel(self._model) self._init_layout() self._init_thread() self.show() @Slot() def _stop_thread(self): self._thread.quit() self._thread.wait() def _init_layout(self): self.setMinimumWidth(400) self.setMinimumHeight(200) dialog_buttons = QDialogButtonBox(QDialogButtonBox.Ok) self._follow = dialog_buttons.addButton("Follow", QDialogButtonBox.ActionRole) self._copy_all = dialog_buttons.addButton("Copy All", QDialogButtonBox.ActionRole) dialog_buttons.accepted.connect(self.accept) self._follow.setCheckable(True) self._follow.toggled.connect(self._view.enable_follow_mode) self._copy_all.clicked.connect(self._model.copy_all) layout = QVBoxLayout(self) layout.addWidget(self._view) layout.addWidget(dialog_buttons) def _init_thread(self): self._thread = QThread() self._worker = FileUpdateWorker(self._file) self._worker.moveToThread(self._thread) self._worker.read.connect(self._model.append_text) self._thread.started.connect(self._worker.setup) self._thread.finished.connect(self._worker.stop) self._thread.finished.connect(self._worker.deleteLater) self.finished.connect(self._stop_thread) self._thread.start()
class SpectralOperationHandler(QDialog): """ Widget to handle user interactions with operations that are communicated from the SpecViz viewer. This is built to work with :func:`~spectral_cube.SpectralCube.apply_function` method by passing in a callable :class:`specviz.analysis.operations.FunctionalOperation` object. Parameters ---------- data : :class:`~glue.core.data.Data` Glue data object on which the spectral operation will be performed. function : :class:`specviz.analysis.operations.FunctionalOperation` Python class instance whose `call` function will be performed on the :class:`~spectral_cube.SpectralCube` object. """ def __init__(self, data, component_id, layout, function=None, func_proxy=None, stack=None, operation_name=None, ui_settings=None, *args, **kwargs): super(SpectralOperationHandler, self).__init__(*args, **kwargs) self._data = data self._stack = stack self._func_proxy = func_proxy self._function = function or (stack[0] if stack is not None else None) self._operation_name = operation_name or (self._function.function.__name__ if self._function is not None else "None") self._component_id = component_id self._operation_thread = None self._layout = layout self._ui_settings = ui_settings self._op_thread = None self.setup_ui() self.setup_connections() def setup_ui(self): """Setup the PyQt UI for this dialog.""" # Load the ui dialog loadUi(os.path.join(os.path.dirname(__file__), "operation_dialog.ui"), self) if self._ui_settings is not None: self.setWindowTitle(self._ui_settings.get("title")) self.operation_group_box.setTitle(self._ui_settings.get("group_box_title")) self.description_label.setText(self._ui_settings.get("description")) component_ids = [str(i) for i in self._data.component_ids()] cur_ind = self._data.component_ids().index(self._component_id) if self._stack is not None: operation_stack = [] for oper in self._stack: func_params = [] for arg in oper.args: func_params.append("{}".format(arg)) for k, v in oper.kwargs.items(): func_params.append("{}={}".format(k, str(v)[:10] + "... " + str( v)[-5:] if len( str(v)) > 15 else str( v))) operation_stack.append("{}({})".format(oper.function.__name__, ", ".join(func_params))) self.operation_combo_box.addItems(operation_stack) else: self.operation_combo_box.addItem(self._operation_name) # Populate combo box self.data_component_combo_box.addItems(component_ids) self.data_component_combo_box.setCurrentIndex(cur_ind) # Disable the button box if there are no available operations if self._function is None: self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) def setup_connections(self): """Setup signal/slot connections for this dialog.""" # When an operation is selected, update the function reference self.operation_combo_box.currentIndexChanged.connect( self.on_operation_index_changed) # When a data component is selected, update the data object reference self.data_component_combo_box.currentIndexChanged.connect( self.on_data_component_index_changed) # If the abort button is clicked, attempted to stop execution self.abort_button.clicked.connect(self.on_aborted) def _compose_cube(self): """ Create a :class:`~spectral_cube.SpectralCube` from a Glue data component. """ if issubclass(self._data.__class__, Subset): wcs = self._data.data.coords.wcs data = self._data.data mask = self._data.to_mask() else: wcs = self._data.coords.wcs data = self._data mask = np.ones(self._data.shape).astype(bool) mask = BooleanArrayMask(mask=mask, wcs=wcs) return SpectralCube(data[self._component_id], wcs=wcs, mask=mask, meta={'unit':self._data.get_component(self._component_id).units}) def on_operation_index_changed(self, index): """Called when the index of the operation combo box has changed.""" self._function = self._stack[index] def on_data_component_index_changed(self, index): """Called when the index of the component combo box has changed.""" self._component_id = self._data.component_ids()[index] def accept(self): """Called when the user clicks the "Okay" button of the dialog.""" # Show the progress bar and abort button self.progress_bar.setEnabled(True) self.abort_button.setEnabled(True) self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) self.button_box.button(QDialogButtonBox.Cancel).setEnabled(False) if self._func_proxy is not None: op_func = lambda *args, **kwargs: self._func_proxy(self._function, *args, **kwargs) else: op_func = self._function self._op_thread = QThread() self._op_worker = OperationWorker(self._compose_cube(), op_func) self._op_worker.moveToThread(self._op_thread) self._op_worker.result.connect(self.on_finished) self._op_worker.status.connect(self.on_status_updated) self._op_thread.started.connect(self._op_worker.run) self._op_thread.start() # data, unit = op_func(self._compose_cube(), None) # self.on_finished(data, unit) def on_aborted(self): """Called when the user aborts the operation.""" self._op_thread.terminate() self.progress_bar.reset() # Hide the progress bar and abort button self.abort_button.setEnabled(False) self.button_box.button(QDialogButtonBox.Ok).setEnabled(True) self.button_box.button(QDialogButtonBox.Cancel).setEnabled(True) def on_status_updated(self, value): """ Called when the status of the operation has been updated. This can be optionally be passed a value to use as the new progress bar value. Parameters ---------- value : float The value passed to the :class:`~qtpy.QtWidgets.QProgressBar` instance. """ self.progress_bar.setValue(value * 100) def on_finished(self, data, unit=None): """ Called when the `QThread` has finished performing the operation on the `SpectralCube` object. Parameters ---------- data : ndarray The result of the operation performed on the `SpectralCube` object. """ component_name = "{} {}".format(self._component_id, self._operation_name) comp_count = len([x for x in self._data.component_ids() if component_name in str(x)]) if comp_count > 0: component_name = "{} {}".format(component_name, comp_count) if data.ndim == 2: coords = WCSCoordinates(wcs=self._data.coords.wcs.celestial) self._data.container_2d = Data(label=self._data.label + " [2d]", coords=coords) self._data.container_2d.add_component(data, component_name) # self._layout.session.data_collection.append(self._data.container_2d) self._layout.add_overlay(data, component_name, display_now=True) else: component = Component(data, units=unit) self._data.add_component(component, component_name) self._op_thread.exit() super(SpectralOperationHandler, self).accept()
class PMGIpythonConsole(RichJupyterWidget): def __init__(self, *args, **kwargs): super(PMGIpythonConsole, self).__init__(*args, **kwargs) self.is_first_execution = True self.confirm_restart = False self.commands_pool = [] self.command_callback_pool: Dict[str, Callable] = {} def change_ui_theme(self, style: str): """ 改变界面主题颜色 :param style: :return: """ style = style.lower() if style == 'fusion': self.style_sheet = default_light_style_sheet self.syntax_style = default_light_syntax_style elif style == 'qdarkstyle': self.style_sheet = default_dark_style_sheet self.syntax_style = default_dark_syntax_style elif style.lower() == 'windowsvista': self.style_sheet = default_light_style_sheet self.syntax_style = default_light_syntax_style elif style.lower() == 'windows': self.style_sheet = default_light_style_sheet self.syntax_style = default_light_syntax_style def _handle_kernel_died(self, since_last_heartbit): self.is_first_execution = True self.restart_kernel(None, True) self.initialize_ipython_builtins() self.execute_command('') return True def _handle_execute_input(self, msg): super()._handle_execute_result(msg) def setup_ui(self): self.kernel_manager = None self.kernel_client = None # initialize by thread self.init_thread = QThread(self) self.console_object = ConsoleInitThread() self.console_object.moveToThread(self.init_thread) self.console_object.initialized.connect(self.slot_initialized) self.init_thread.finished.connect(self.console_object.deleteLater) self.init_thread.finished.connect(self.init_thread.deleteLater) self.init_thread.started.connect(self.console_object.run) self.init_thread.start() cursor: QTextCursor = self._prompt_cursor cursor.movePosition(QTextCursor.End) def _context_menu_make(self, pos: 'QPoint') -> QMenu: menu = super(PMGIpythonConsole, self)._context_menu_make(pos) _translate = QCoreApplication.translate trans_dic = { 'Cut': _translate("PMGIpythonConsole", 'Cut'), 'Copy': _translate("PMGIpythonConsole", 'Copy'), 'Copy (Raw Text)': _translate("PMGIpythonConsole", 'Copy(Raw Text)'), 'Paste': _translate("PMGIpythonConsole", 'Paste'), 'Select All': _translate("PMGIpythonConsole", 'Select All'), 'Save as HTML/XML': _translate("PMGIpythonConsole", 'Save as HTML/XML'), 'Print': _translate("PMGIpythonConsole", 'Print') } for action in menu.actions(): trans = trans_dic.get(action.text()) trans = trans if trans is not None else action.text() action.setText(trans) restart_action = menu.addAction( _translate("PMGIpythonConsole", 'Restart')) restart_action.triggered.connect(self.slot_restart_kernel) stop_action = menu.addAction( _translate("PMGIpythonConsole", 'Interrupt')) # stop_action.triggered.connect(self.request_interrupt_kernel) stop_action.triggered.connect(self.on_interrupt_kernel) # stop_action.setEnabled(self._executing) return menu def on_interrupt_kernel(self): """ 当点击中断执行时。 IPython会出现一个奇怪的问题——当中断执行时,可能_executing恒为False。 因此干脆不屏蔽了。 Returns: """ self.interrupt_kernel() def _custom_context_menu_requested(self, pos): super(PMGIpythonConsole, self)._custom_context_menu_requested(pos) def slot_restart_kernel(self, arg): ret = QMessageBox.warning(self, '提示', '是否要重启控制台?\n一切变量都将被重置。', QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Cancel) if ret == QMessageBox.Ok: self._restart_kernel(arg) def _restart_kernel(self, arg1): self.is_first_execution = True self.restart_kernel(None, True) self.initialize_ipython_builtins() self.execute_command('') return True def slot_initialized(self, kernel_manager, kernel_client): """ Args: kernel_manager: `qtconsole.manager.QtKernelManager` kernel_client: `qtconsole.manager.QtKernelManager.client` Returns: """ self.kernel_manager = kernel_manager self.kernel_client = kernel_client self.initialize_ipython_builtins() def initialize_ipython_builtins(self): return def _update_list(self): try: super(PMGIpythonConsole, self)._update_list() except BaseException: import traceback traceback.print_exc() def _banner_default(self): """ 自定义控制台开始的文字 Returns: """ return 'Welcome To PMGWidgets Ipython Console!\n' def closeEvent(self, event): if self.init_thread.isRunning(): self.console_object.stop() self.init_thread.quit() self.init_thread.wait(500) super(PMGIpythonConsole, self).closeEvent(event) def execute_file(self, file: str, hidden: bool = False): if not os.path.exists(file) or not file.endswith('.py'): raise FileNotFoundError(f'{file} not found or invalid') base = os.path.basename(file) cmd = os.path.splitext(base)[0] with open(file, 'r', encoding='utf-8') as f: source = f.read() self.execute_command(source, hidden=hidden, hint_text=cmd) def execute_command(self, source, hidden: bool = False, hint_text: str = '') -> str: """ :param source: :param hidden: :param hint_text: 运行代码前显示的提示 :return: str 执行命令的 msgid """ cursor: QTextCursor = self._prompt_cursor cursor.movePosition(QTextCursor.End) # 运行文件时,显示文件名,无换行符,执行选中内容时,包含换行符 # 检测换行符,在ipy console中显示执行脚本内容 hint_row_list = hint_text.split("\n") for hint in hint_row_list: if hint != "": cursor.insertText('%s\n' % hint) self._insert_continuation_prompt(cursor) else: # 删除多余的continuation_prompt self.undo() self._finalize_input_request( ) # display input string buffer in console. cursor.movePosition(QTextCursor.End) if self.kernel_client is None: self.commands_pool.append((source, hidden, hint_text)) return '' else: return self.pmexecute(source, hidden) def _handle_stream(self, msg): parent_header = msg.get('parent_header') if parent_header is not None: msg_id = parent_header.get( 'msg_id') # 'fee0bee5-074c00d093b1455be6d166b1_10''] if msg_id in self.command_callback_pool.keys(): callback = self.command_callback_pool.pop(msg_id) assert callable(callback) callback() cursor: QTextCursor = self._prompt_cursor cursor.movePosition(QTextCursor.End) super()._handle_stream(msg) def append_stream(self, text): """重写的方法。原本before_prompt属性是False。""" self._append_plain_text(text, before_prompt=False) def pmexecute(self, source: str, hidden: bool = False) -> str: """ 执行代码并且返回Msgid :param source: :param hidden: :return: """ is_legal, msg = self.is_source_code_legal(source) if not is_legal: QMessageBox.warning(self, '警告', msg) source = '' msg_id = self.kernel_client.execute(source, hidden) self._request_info['execute'][msg_id] = self._ExecutionRequest( msg_id, 'user') self._hidden = hidden if not hidden: self.executing.emit(source) return msg_id # super()._execute(source, hidden) def is_source_code_legal(self, source_code: str) -> Tuple[bool, str]: """判断注入到shell中的命令是否合法。 如果命令不合法,应当避免执行该命令。 Args: source_code: 注入到shell中的命令。 Returns: * 是否合法; * 如不合法,返回原因;如合法,返回空字符串。 """ return True, '' @staticmethod def install_translator(): global _trans app = QApplication.instance() assert app is not None _trans = QTranslator() _trans.load(QLocale.system(), 'qt_zh_CN.qm', directory=os.path.join(os.path.dirname(__file__), 'translations')) app.installTranslator(_trans)
class GistWindow(QDialog): def __init__(self): super(GistWindow, self).__init__() self.thread = None self.worker = None self.gistToken = config.gistToken self.connected = False self.setWindowTitle("Gist") self.setMinimumWidth(380) self.layout = QVBoxLayout() self.testStatus = QLabel("") self.layout.addWidget(self.testStatus) self.layout.addWidget(QLabel("Gist Token")) self.gistTokenInput = QLineEdit() self.gistTokenInput.setText(self.gistToken) self.gistTokenInput.setMaxLength(40) self.gistTokenInput.textChanged.connect(self.enableButtons) self.layout.addWidget(self.gistTokenInput) self.testButton = QPushButton("Test Connection") self.testButton.setEnabled(False) self.testButton.clicked.connect(self.checkStatus) self.layout.addWidget(self.testButton) # self.syncHighlightsButton = QPushButton("Synch Highlights") # self.syncHighlightsButton.setEnabled(False) # self.syncHighlightsButton.clicked.connect(self.syncHighlights) # self.layout.addWidget(self.syncHighlightsButton) self.syncBibleNotesButton = QPushButton("Synch Bibles Notes") self.syncBibleNotesButton.setEnabled(False) self.syncBibleNotesButton.clicked.connect(self.syncBibleNotes) self.layout.addWidget(self.syncBibleNotesButton) buttons = QDialogButtonBox.Ok self.buttonBox = QDialogButtonBox(buttons) self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.stopSync) self.layout.addWidget(self.buttonBox) self.setLayout(self.layout) self.enableButtons() self.checkStatus() def enableButtons(self): if len(self.gistTokenInput.text()) >= 40: self.testButton.setEnabled(True) else: self.testButton.setEnabled(False) self.connected = False self.setStatus("Not connected", False) if self.connected: self.testButton.setEnabled(False) self.syncBibleNotesButton.setEnabled(True) else: self.syncBibleNotesButton.setEnabled(False) def checkStatus(self): if len(self.gistTokenInput.text()) < 40: self.setStatus("Not connected", False) self.connected = False else: self.gh = GitHubGist(self.gistTokenInput.text()) if self.gh.connected: self.setStatus("Connected to " + self.gh.user.name, True) self.connected = True config.gistToken = self.gistTokenInput.text() else: self.setStatus("Not connected", False) self.connected = False self.enableButtons() def setStatus(self, message, connected=True): self.testStatus.setText("Status: " + message) if connected: self.testStatus.setStyleSheet("color: rgb(128, 255, 7);") else: self.testStatus.setStyleSheet("color: rgb(253, 128, 8);") QApplication.processEvents() def syncBibleNotes(self): self.setStatus("Syncing ...", True) self.syncBibleNotesButton.setEnabled(False) self.thread = QThread() self.worker = SyncNotesWithGist() self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.worker.deleteLater) self.worker.finished.connect(self.syncCompleted) self.worker.progress.connect(self.setStatus) self.thread.start() def syncCompleted(self, count): self.setStatus("Done! Processed {0} notes".format(count), True) def stopSync(self): if self.thread and self.thread.isRunning(): self.thread.quit()
def new_worker_qthread( Worker: Type[QObject], *args, start=False, connections=None, **kwargs ): """This is a convenience method to start a worker in a Qthread It follows the pattern described here: https://www.qt.io/blog/2010/06/17/youre-doing-it-wrong and https://doc.qt.io/qt-5/qthread.html#details all *args, **kwargs will be passed to the Worker class on instantiation. Parameters ---------- Worker : QObject QObject type that implements a work() method. The Worker should also emit a finished signal when the work is done. start : bool If True, worker will be started immediately, otherwise, you must manually start the worker. connections: dict, optional Optional dictionary of {signal: function} to connect to the new worker. for instance: connections = {'incremented': myfunc} will result in: worker.incremented.connect(myfunc) Examples -------- Create some QObject that has a long-running work method: >>> class Worker(QObject): ... ... finished = Signal() ... increment = Signal(int) ... ... def __init__(self, argument): ... super().__init__() ... self.argument = argument ... ... @Slot() ... def work(self): ... # some long running task... ... import time ... for i in range(10): ... time.sleep(1) ... self.increment.emit(i) ... self.finished.emit() ... >>> worker, thread = new_worker_qthread( ... Worker, ... 'argument', ... start=True, ... connections={'increment': print}, ... ) >>> print([i for i in example_generator(4)]) [0, 1, 2, 3] """ if not isinstance(connections, (dict, type(None))): raise TypeError('connections parameter must be a dict') thread = QThread() worker = Worker(*args, **kwargs) worker.moveToThread(thread) thread.started.connect(worker.work) worker.finished.connect(thread.quit) worker.finished.connect(worker.deleteLater) thread.finished.connect(thread.deleteLater) if connections: [getattr(worker, key).connect(val) for key, val in connections.items()] if start: thread.start() # sometimes need to connect stuff before starting return worker, thread
class DownloadBibleMp3Dialog(QDialog): def __init__(self, parent): super().__init__() self.bibles = { "BBE (British accent)": ("BBE", "otseng/UniqueBible_MP3_BBE_british", "british"), "KJV (American accent)": ("KJV", "otseng/UniqueBible_MP3_KJV", "default"), "KJV (American soft music)": ("KJV", "otseng/UniqueBible_MP3_KJV_soft_music", "soft-music"), "NHEB (Indian accent)": ("NHEB", "otseng/UniqueBible_MP3_NHEB_indian", "indian"), "WEB (American accent)": ("WEB", "otseng/UniqueBible_MP3_WEB", "default"), "CUV (Chinese)": ("CUV", "otseng/UniqueBible_MP3_CUV", "default"), "HHBD (Hindi)": ("HHBD", "otseng/UniqueBible_MP3_HHBD", "default"), "RVA (Spanish)": ("RVA", "otseng/UniqueBible_MP3_RVA", "default"), "TR (Modern Greek)": ("TR", "otseng/UniqueBible_MP3_TR", "modern"), } self.parent = parent self.setWindowTitle(config.thisTranslation["gitHubBibleMp3Files"]) self.setMinimumSize(150, 450) self.selectedRendition = None self.selectedText = None self.selectedRepo = None self.selectedDirectory = None self.settingBibles = False self.thread = None self.setupUI() def setupUI(self): mainLayout = QVBoxLayout() title = QLabel(config.thisTranslation["gitHubBibleMp3Files"]) mainLayout.addWidget(title) self.versionsLayout = QVBoxLayout() self.renditionsList = QListWidget() self.renditionsList.itemClicked.connect(self.selectItem) for rendition in self.bibles.keys(): self.renditionsList.addItem(rendition) self.renditionsList.setMaximumHeight(100) self.versionsLayout.addWidget(self.renditionsList) mainLayout.addLayout(self.versionsLayout) self.downloadTable = QTableView() self.downloadTable.setEnabled(False) self.downloadTable.setFocusPolicy(Qt.StrongFocus) self.downloadTable.setEditTriggers(QAbstractItemView.NoEditTriggers) self.downloadTable.setSortingEnabled(True) self.dataViewModel = QStandardItemModel(self.downloadTable) self.downloadTable.setModel(self.dataViewModel) mainLayout.addWidget(self.downloadTable) buttonsLayout = QHBoxLayout() selectAllButton = QPushButton(config.thisTranslation["selectAll"]) selectAllButton.setFocusPolicy(Qt.StrongFocus) selectAllButton.clicked.connect(self.selectAll) buttonsLayout.addWidget(selectAllButton) selectNoneButton = QPushButton(config.thisTranslation["selectNone"]) selectNoneButton.setFocusPolicy(Qt.StrongFocus) selectNoneButton.clicked.connect(self.selectNone) buttonsLayout.addWidget(selectNoneButton) otButton = QPushButton("1-39") otButton.setFocusPolicy(Qt.StrongFocus) otButton.clicked.connect(self.selectOT) buttonsLayout.addWidget(otButton) ntButton = QPushButton("40-66") ntButton.setFocusPolicy(Qt.StrongFocus) ntButton.clicked.connect(self.selectNT) buttonsLayout.addWidget(ntButton) # buttonsLayout.addStretch() mainLayout.addLayout(buttonsLayout) self.downloadButton = QPushButton(config.thisTranslation["download"]) self.downloadButton.setFocusPolicy(Qt.StrongFocus) self.downloadButton.setAutoDefault(True) self.downloadButton.setFocus() self.downloadButton.clicked.connect(self.download) mainLayout.addWidget(self.downloadButton) self.status = QLabel("") mainLayout.addWidget(self.status) buttonLayout = QHBoxLayout() self.closeButton = QPushButton(config.thisTranslation["close"]) self.closeButton.setFocusPolicy(Qt.StrongFocus) self.closeButton.clicked.connect(self.closeDialog) buttonLayout.addWidget(self.closeButton) mainLayout.addLayout(buttonLayout) self.setLayout(mainLayout) self.renditionsList.item(0).setSelected(True) bible = self.renditionsList.item(0).text() self.selectRendition(bible) self.downloadButton.setDefault(True) QTimer.singleShot(0, self.downloadButton.setFocus) def selectItem(self, item): self.selectRendition(item.text()) def selectRendition(self, rendition): from util.GithubUtil import GithubUtil self.selectedRendition = rendition self.downloadTable.setEnabled(True) self.selectedText, self.selectedRepo, self.selectedDirectory = self.bibles[ self.selectedRendition] self.github = GithubUtil(self.selectedRepo) self.repoData = self.github.getRepoData() self.settingBibles = True self.dataViewModel.clear() rowCount = 0 for file in self.repoData.keys(): if len(str(file)) > 3: engFullBookName = file[3:] else: engFullBookName = BibleBooks().eng[str(int(file))][1] item = QStandardItem(file[:3].strip()) folder = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory, file) folderWithName = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory, file + " " + engFullBookName) if os.path.exists(folder) or os.path.exists(folderWithName): item.setCheckable(False) item.setCheckState(Qt.Unchecked) item.setEnabled(False) else: item.setCheckable(True) item.setCheckState(Qt.Checked) item.setEnabled(True) self.dataViewModel.setItem(rowCount, 0, item) item = QStandardItem(engFullBookName) self.dataViewModel.setItem(rowCount, 1, item) if os.path.exists(folder) or os.path.exists(folderWithName): item = QStandardItem("Installed") self.dataViewModel.setItem(rowCount, 2, item) else: item = QStandardItem("") self.dataViewModel.setItem(rowCount, 2, item) rowCount += 1 self.dataViewModel.setHorizontalHeaderLabels([ config.thisTranslation["menu_book"], config.thisTranslation["name"], "" ]) self.downloadTable.setColumnWidth(0, 90) self.downloadTable.setColumnWidth(1, 125) self.downloadTable.setColumnWidth(2, 125) # self.downloadTable.resizeColumnsToContents() self.settingBibles = False def selectAll(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) if item.isEnabled(): item.setCheckState(Qt.Checked) def selectNone(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) item.setCheckState(Qt.Unchecked) def selectOT(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) bookNum = int(item.text()) if bookNum <= 39: if item.isEnabled(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Unchecked) def selectNT(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) bookNum = int(item.text()) if bookNum >= 40: if item.isEnabled(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Unchecked) def download(self): self.downloadButton.setEnabled(False) self.setStatus(config.thisTranslation["message_installing"]) self.closeButton.setEnabled(False) folder = os.path.join("audio", "bibles") if not os.path.exists(folder): os.mkdir(folder) folder = os.path.join("audio", "bibles", self.selectedText) if not os.path.exists(folder): os.mkdir(folder) folder = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory) if not os.path.exists(folder): os.mkdir(folder) self.thread = QThread() self.worker = DownloadFromGitHub(self.github, self.repoData, self.dataViewModel, self.selectedText, self.selectedDirectory) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.worker.deleteLater) self.worker.finished.connect(self.finishedDownloading) self.worker.progress.connect(self.setStatus) self.thread.start() def finishedDownloading(self, count): self.selectRendition(self.selectedRendition) self.setStatus("") self.downloadButton.setEnabled(True) self.closeButton.setEnabled(True) if count > 0: self.parent.displayMessage( config.thisTranslation["message_installed"]) def setStatus(self, message): self.status.setText(message) QApplication.processEvents() def closeDialog(self): if self.thread: if self.thread.isRunning(): self.thread.quit() self.close()
class CondaPackagesWidget(QWidget): """Conda Packages Widget.""" # Location of updated repo.json files from continuum/binstar CONDA_CONF_PATH = get_conf_path('repo') # Location of continuum/anaconda default repos shipped with conda-manager DATA_PATH = get_module_data_path() # file inside DATA_PATH with metadata for conda packages DATABASE_FILE = 'packages.ini' sig_worker_ready = Signal() sig_packages_ready = Signal() sig_environment_created = Signal() def __init__(self, parent, name=None, prefix=None, channels=[]): super(CondaPackagesWidget, self).__init__(parent) self._parent = parent self._status = '' # Statusbar message self._conda_process = conda_api_q.CondaProcess(self) self._root_prefix = self._conda_process.ROOT_PREFIX self._prefix = None self._temporal_action_dic = {} self._download_manager = DownloadManager(self, self._on_download_finished, self._on_download_progress, self.CONDA_CONF_PATH) self._thread = QThread(self) self._worker = None self._db_metadata = cp.ConfigParser() self._db_file = CondaPackagesWidget.DATABASE_FILE self._db_metadata.readfp(open(osp.join(self.DATA_PATH, self._db_file))) self._packages_names = None self._row_data = None self._hide_widgets = False # TODO: Hardcoded channels for the moment self._default_channels = [ ['_free_', 'http://repo.continuum.io/pkgs/free'], #['_pro_', 'http://repo.continuum.io/pkgs/pro'] ] self._extra_channels = channels # pyqt not working with ssl some bug here on the anaconda compilation # [['binstar_goanpeca_', 'https://conda.binstar.org/goanpeca']] self._repo_name = None # linux-64, win-32, etc... self._channels = None # [['filename', 'channel url'], ...] self._repo_files = None # [filepath, filepath, ...] self._packages = {} self._download_error = None self._error = None # Widgets self.combobox_filter = QComboBox(self) self.button_update = QPushButton(_('Update package index')) self.textbox_search = SearchLineEdit(self) self.table = CondaPackagesTable(self) self.status_bar = QLabel(self) self.progress_bar = QProgressBar(self) self.button_ok = QPushButton(_('Ok')) self.bbox = QDialogButtonBox(Qt.Horizontal) self.bbox.addButton(self.button_ok, QDialogButtonBox.ActionRole) self.widgets = [self.button_update, self.combobox_filter, self.textbox_search, self.table, self.button_ok] # Widgets setup self.combobox_filter.addItems([k for k in const.COMBOBOX_VALUES_ORDERED]) self.combobox_filter.setMinimumWidth(120) self.button_ok.setDefault(True) self.button_ok.setAutoDefault(True) self.button_ok.setVisible(False) self.progress_bar.setVisible(False) self.progress_bar.setTextVisible(False) self.progress_bar.setMaximumHeight(16) self.progress_bar.setMaximumWidth(130) self.setWindowTitle(_("Conda Package Manager")) self.setMinimumSize(QSize(480, 300)) # Signals and slots self.combobox_filter.currentIndexChanged.connect(self.filter_package) self.button_update.clicked.connect(self.update_package_index) self.textbox_search.textChanged.connect(self.search_package) self._conda_process.sig_partial.connect(self._on_conda_process_partial) self._conda_process.sig_finished.connect(self._on_conda_process_ready) # NOTE: do not try to save the QSpacerItems in a variable for reuse # it will crash python on exit if you do! # Layout self._spacer_w = 250 self._spacer_h = 5 self._top_layout = QHBoxLayout() self._top_layout.addWidget(self.combobox_filter) self._top_layout.addWidget(self.button_update) self._top_layout.addWidget(self.textbox_search) self._middle_layout = QVBoxLayout() self._middle_layout.addWidget(self.table) self._bottom_layout = QHBoxLayout() self._bottom_layout.addWidget(self.status_bar, Qt.AlignLeft) self._bottom_layout.addWidget(self.progress_bar, Qt.AlignRight) self._layout = QVBoxLayout(self) self._layout.addItem(QSpacerItem(self._spacer_w, self._spacer_h)) self._layout.addLayout(self._top_layout) self._layout.addLayout(self._middle_layout) self._layout.addItem(QSpacerItem(self._spacer_w, self._spacer_h)) self._layout.addLayout(self._bottom_layout) self._layout.addItem(QSpacerItem(self._spacer_w, self._spacer_h)) self._layout.addWidget(self.bbox) self._layout.addItem(QSpacerItem(self._spacer_w, self._spacer_h)) self.setLayout(self._layout) # Setup self.set_environment(name=name, prefix=prefix, update=False) if self._supports_architecture(): self.update_package_index() else: status = _('no packages supported for this architecture!') self._update_status(progress=[0, 0], hide=True, status=status) def _supports_architecture(self): """ """ self._set_repo_name() if self._repo_name is None: return False else: return True def _set_repo_name(self): """Get python system and bitness, and return default repo name""" system = sys.platform.lower() bitness = 64 if sys.maxsize > 2**32 else 32 machine = platform.machine() fname = [None, None] if 'win' in system: fname[0] = 'win' elif 'lin' in system: fname[0] = 'linux' elif 'osx' in system or 'darwin' in system: # TODO: is this correct? fname[0] = 'osx' else: return None if bitness == 32: fname[1] = '32' elif bitness == 64: fname[1] = '64' else: return None # armv6l if machine.startswith('armv6'): fname[1] = 'armv6l' if None in fname: self._repo_name = None else: self._repo_name = '-'.join(fname) def _set_channels(self): """ """ default = self._default_channels extra = self._extra_channels body = self._repo_name tail = '/repodata.json' channels = [] files = [] for channel in default + extra: prefix = channel[0] url = '{0}/{1}{2}'.format(channel[1], body, tail) name = '{0}{1}.json'.format(prefix, body) channels.append([name, url]) files.append(osp.join(self.CONDA_CONF_PATH, name)) self._repo_files = files self._channels = channels def _download_repodata(self): """download the latest version available of the repo(s)""" status = _('Updating package index...') self._update_status(hide=True, progress=[0, 0], status=status) self._download_manager.set_queue(self._channels) self._download_manager.start_download() # --- Callback download manager # ------------------------------------------------------------------------ def _on_download_progress(self, progress): """function called by download manager when receiving data progress : [int, int] A two item list of integers with relating [downloaded, total] """ self._update_status(hide=True, progress=progress, status=None) def _on_download_finished(self): """function called by download manager when finished all downloads this will be called even if errors were encountered, so error handling is done here as well """ error = self._download_manager.get_errors() if error is not None: self._update_status(hide=False) if not osp.isdir(self.CONDA_CONF_PATH): os.mkdir(self.CONDA_CONF_PATH) for repo_file in self._repo_files: # if a file does not exists, look for one in DATA_PATH if not osp.isfile(repo_file): filename = osp.basename(repo_file) bck_repo_file = osp.join(self.DATA_PATH, filename) # if available copy to CONDA_CONF_PATH if osp.isfile(bck_repo_file): shutil.copy(bck_repo_file, repo_file) # otherwise remove from the repo_files list else: self._repo_files.remove(repo_file) self._error = None self.setup_packages() # ------------------------------------------------------------------------ def setup_packages(self): """ """ pip_packages = self._conda_process.pip_list(prefix=self._prefix) self._thread.terminate() self._thread = QThread(self) self._worker = PackagesWorker(self, self._repo_files, self._prefix, self._root_prefix, pip_packages) self._worker.sig_status_updated.connect(self._update_status) self._worker.sig_ready.connect(self._worker_ready) self._worker.sig_ready.connect(self._thread.quit) self._worker.moveToThread(self._thread) self._thread.started.connect(self._worker._prepare_model) self._thread.start() def _worker_ready(self): """ """ self._packages_names = self._worker.packages_names self._packages_versions = self._worker.packages_versions self._row_data = self._worker.row_data # depending on the size of table this might lock the gui for a moment self.table.setup_model(self._packages_names, self._packages_versions, self._row_data) self.table.filter_changed() self._update_status(hide=False) self.filter_package(const.INSTALLED) self.sig_worker_ready.emit() self.sig_packages_ready.emit() def _update_status(self, status=None, hide=True, progress=None, env=False): """Update status bar, progress bar display and widget visibility status : str TODO: hide : bool TODO: progress : [int, int] TODO: """ self.busy = hide for widget in self.widgets: widget.setDisabled(hide) self.progress_bar.setVisible(hide) if status is not None: self._status = status if self._prefix == self._root_prefix: short_env = 'root' elif self._conda_process.environment_exists(prefix=self._prefix): short_env = osp.basename(self._prefix) else: short_env = self._prefix if env: self._status = '{0} (<b>{1}</b>)'.format(self._status, short_env) self.status_bar.setText(self._status) if progress is not None: self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(progress[1]) self.progress_bar.setValue(progress[0]) def _run_action(self, package_name, action, version, versions): """ """ prefix = self._prefix dlg = CondaPackageActionDialog(self, prefix, package_name, action, version, versions) if dlg.exec_(): dic = {} self.status = 'Processing' self._update_status(hide=True) self.repaint() ver1 = dlg.label_version.text() ver2 = dlg.combobox_version.currentText() pkg = u'{0}={1}{2}'.format(package_name, ver1, ver2) dep = dlg.checkbox.checkState() state = dlg.checkbox.isEnabled() dlg.close() dic['pkg'] = pkg dic['dep'] = not (dep == 0 and state) dic['action'] = None self._run_conda_process(action, dic) def _run_conda_process(self, action, dic): """ """ cp = self._conda_process prefix = self._prefix if prefix == self._root_prefix: name = 'root' elif self._conda_process.environment_exists(prefix=prefix): name = osp.basename(prefix) else: name = prefix if 'pkg' in dic and 'dep' in dic: pkgs = dic['pkg'] if not isinstance(pkgs, list): pkgs = [pkgs] dep = dic['dep'] if action == const.INSTALL or action == const.UPGRADE or \ action == const.DOWNGRADE: status = _('Installing <b>') + dic['pkg'] + '</b>' status = status + _(' into <i>') + name + '</i>' cp.install(prefix=prefix, pkgs=pkgs, dep=dep) elif action == const.REMOVE: status = (_('moving <b>') + dic['pkg'] + '</b>' + _(' from <i>') + name + '</i>') cp.remove(pkgs[0], prefix=prefix) # --- Actions to be implemented in case of environment needs elif action == const.CREATE: status = _('Creating environment <b>') + name + '</b>' cp.create(prefix=prefix, pkgs=pkgs) elif action == const.CLONE: status = (_('Cloning ') + '<i>' + dic['cloned from'] + _('</i> into <b>') + name + '</b>') elif action == const.REMOVE_ENV: status = _('Removing environment <b>') + name + '</b>' self._update_status(hide=True, status=status, progress=[0, 0]) self._temporal_action_dic = dic def _on_conda_process_ready(self): """ """ error = self._conda_process.error if error is None: status = _('there was an error') self._update_status(hide=False, status=status) else: self._update_status(hide=True) dic = self._temporal_action_dic if dic['action'] == const.CREATE: self.sig_environment_created.emit() self.setup_packages() def _on_conda_process_partial(self): """ """ try: partial = self._conda_process.partial.split('\n')[0] partial = json.loads(partial) except: partial = {'progress': 0, 'maxval': 0} progress = partial['progress'] maxval = partial['maxval'] if 'fetch' in partial: status = _('Downloading <b>') + partial['fetch'] + '</b>' elif 'name' in partial: status = _('Installing and linking <b>') + partial['name'] + '</b>' else: progress = 0 maxval = 0 status = None self._update_status(status=status, progress=[progress, maxval]) # Public api # ---------- def get_package_metadata(self, name): """ """ db = self._db_metadata metadata = dict(description='', url='', pypi='', home='', docs='', dev='') for key in metadata: name_lower = name.lower() for name_key in (name_lower, name_lower.split('-')[0]): try: metadata[key] = db.get(name_key, key) break except (cp.NoSectionError, cp.NoOptionError): pass return metadata def update_package_index(self): """ """ self._set_channels() self._download_repodata() def search_package(self, text): """ """ self.table.search_string_changed(text) def filter_package(self, value): """ """ self.table.filter_status_changed(value) def set_environment(self, name=None, prefix=None, update=True): """ """ if name and prefix: raise Exception('#TODO:') if name and self._conda_process.environment_exists(name=name): self._prefix = self.get_prefix_envname(name) elif prefix and self._conda_process.environment_exists(prefix=prefix): self._prefix = prefix else: self._prefix = self._root_prefix # Reset environent to reflect this environment in the package model if update: self.setup_packages() def get_environment_prefix(self): """Returns the active environment prefix.""" return self._prefix def get_environment_name(self): """ Returns the active environment name if it is located in the default conda environments directory, otherwise it returns the prefix. """ name = osp.basename(self._prefix) if not (name and self._conda_process.environment_exists(name=name)): name = self._prefix return name def get_environments(self): """ Get a list of conda environments located in the default conda environments directory. """ return self._conda_process.get_envs() def get_prefix_envname(self, name): """Returns the prefix for a given environment by name.""" return self._conda_process.get_prefix_envname(name) def get_package_versions(self, name): """ """ return self.table.source_model.get_package_versions(name) def create_environment(self, name=None, prefix=None, packages=['python']): """ """ # If environment exists already? GUI should take care of this # BUT the api call should simply set that env as the env dic = {} dic['name'] = name dic['pkg'] = packages dic['dep'] = True # Not really needed but for the moment! dic['action'] = const.CREATE self._run_conda_process(const.CREATE, dic) def enable_widgets(self): """ """ self.table.hide_columns() def disable_widgets(self): """ """ self.table.hide_action_columns()
def _new_worker_qthread( Worker: Type[QObject], *args, _start_thread: bool = False, _connect: Dict[str, Callable] = None, **kwargs, ): """This is a convenience function to start a worker in a Qthread. In most cases, the @thread_worker decorator is sufficient and preferable. But this allows the user to completely customize the Worker object. However, they must then maintain control over the thread and clean up appropriately. It follows the pattern described here: https://www.qt.io/blog/2010/06/17/youre-doing-it-wrong and https://doc.qt.io/qt-5/qthread.html#details see also: https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/ A QThread object is not a thread! It should be thought of as a class to *manage* a thread, not as the actual code or object that runs in that thread. The QThread object is created on the main thread and lives there. Worker objects which derive from QObject are the things that actually do the work. They can be moved to a QThread as is done here. .. note:: Mostly ignorable detail While the signals/slots syntax of the worker looks very similar to standard "single-threaded" signals & slots, note that inter-thread signals and slots (automatically) use an event-based QueuedConnection, while intra-thread signals use a DirectConnection. See `Signals and Slots Across Threads <https://doc.qt.io/qt-5/threads-qobject.html#signals-and-slots-across-threads>`_ Parameters ---------- Worker : QObject QObject type that implements a work() method. The Worker should also emit a finished signal when the work is done. start_thread : bool If True, thread will be started immediately, otherwise, thread must be manually started with thread.start(). connections: dict, optional Optional dictionary of {signal: function} to connect to the new worker. for instance: connections = {'incremented': myfunc} will result in: worker.incremented.connect(myfunc) *args will be passed to the Worker class on instantiation. **kwargs will be passed to the Worker class on instantiation. Returns ------- worker : WorkerBase The created worker. thread : QThread The thread on which the worker is running. Examples -------- Create some QObject that has a long-running work method: .. code-block:: python class Worker(QObject): finished = Signal() increment = Signal(int) def __init__(self, argument): super().__init__() self.argument = argument @Slot() def work(self): # some long running task... import time for i in range(10): time.sleep(1) self.increment.emit(i) self.finished.emit() worker, thread = _new_worker_qthread( Worker, 'argument', start_thread=True, connections={'increment': print}, ) """ if _connect and not isinstance(_connect, dict): raise TypeError('_connect parameter must be a dict') thread = QThread() worker = Worker(*args, **kwargs) worker.moveToThread(thread) thread.started.connect(worker.work) worker.finished.connect(thread.quit) worker.finished.connect(worker.deleteLater) thread.finished.connect(thread.deleteLater) if _connect: [getattr(worker, key).connect(val) for key, val in _connect.items()] if _start_thread: thread.start() # sometimes need to connect stuff before starting return worker, thread
class SnippetsActor(QObject): #: Signal emitted when the Thread is ready sig_snippets_ready = Signal() sig_snippets_response = Signal(int, dict) sig_update_snippets = Signal(dict) sig_mailbox = Signal(dict) def __init__(self, parent): QObject.__init__(self) self.stopped = False self.daemon = True self.mutex = QMutex() self.language_snippets = {} self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_mailbox.connect(self.handle_msg) self.sig_update_snippets.connect(self.update_snippets) def stop(self): """Stop actor.""" with QMutexLocker(self.mutex): logger.debug("Snippets plugin stopping...") self.thread.quit() def start(self): """Start thread.""" self.thread.start() def started(self): """Thread started.""" logger.debug('Snippets plugin starting...') self.sig_snippets_ready.emit() @Slot(dict) def update_snippets(self, snippets): """Update available snippets.""" logger.debug('Updating snippets...') for language in snippets: lang_snippets = snippets[language] lang_trie = Trie() for trigger in lang_snippets: trigger_descriptions = lang_snippets[trigger] lang_trie[trigger] = (trigger, trigger_descriptions) self.language_snippets[language] = lang_trie @Slot(dict) def handle_msg(self, message): """Handle one message""" msg_type, _id, file, msg = [ message[k] for k in ('type', 'id', 'file', 'msg')] logger.debug(u'Perform request {0} with id {1}'.format(msg_type, _id)) if msg_type == CompletionRequestTypes.DOCUMENT_COMPLETION: language = msg['language'] current_word = msg['current_word'] snippets = [] if current_word is None: snippets = {'params': snippets} self.sig_snippets_response.emit(_id, snippets) return if language in self.language_snippets: language_snippets = self.language_snippets[language] if language_snippets[current_word]: for node in language_snippets[current_word]: trigger, info = node.value for description in info: description_snippet = info[description] text = description_snippet['text'] remove_trigger = description_snippet[ 'remove_trigger'] snippets.append({ 'kind': CompletionItemKind.SNIPPET, 'insertText': text, 'label': f'{trigger} ({description})', 'sortText': f'zzz{trigger}', 'filterText': trigger, 'documentation': '', 'provider': SNIPPETS_COMPLETION, 'remove_trigger': remove_trigger }) snippets = {'params': snippets} self.sig_snippets_response.emit(_id, snippets)
class FallbackActor(QObject): #: Signal emitted when the Thread is ready sig_fallback_ready = Signal() sig_set_tokens = Signal(int, dict) sig_mailbox = Signal(dict) def __init__(self, parent): QObject.__init__(self) self.stopped = False self.daemon = True self.mutex = QMutex() self.file_tokens = {} self.diff_patch = diff_match_patch() self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_mailbox.connect(self.handle_msg) def tokenize(self, text, offset, language, current_word): """ Return all tokens in `text` and all keywords associated by Pygments to `language`. """ valid = is_prefix_valid(text, offset, language) if not valid: return [] # Get language keywords provided by Pygments try: lexer = get_lexer_by_name(language) keywords = get_keywords(lexer) except Exception: keywords = [] keyword_set = set(keywords) keywords = [{ 'kind': CompletionItemKind.KEYWORD, 'insertText': keyword, 'label': keyword, 'sortText': keyword, 'filterText': keyword, 'documentation': '', 'provider': FALLBACK_COMPLETION } for keyword in keywords] # Get file tokens tokens = get_words(text, offset, language) tokens = [{ 'kind': CompletionItemKind.TEXT, 'insertText': token, 'label': token, 'sortText': token, 'filterText': token, 'documentation': '', 'provider': FALLBACK_COMPLETION } for token in tokens] for token in tokens: if token['insertText'] not in keyword_set: keywords.append(token) # Filter matching results if current_word is not None: current_word = current_word.lower() keywords = [ k for k in keywords if current_word in k['insertText'].lower() ] return keywords def stop(self): """Stop actor.""" with QMutexLocker(self.mutex): logger.debug("Fallback plugin stopping...") self.thread.quit() def start(self): """Start thread.""" self.thread.start() def started(self): """Thread started.""" logger.debug('Fallback plugin starting...') self.sig_fallback_ready.emit() @Slot(dict) def handle_msg(self, message): """Handle one message""" msg_type, _id, file, msg = [ message[k] for k in ('type', 'id', 'file', 'msg') ] logger.debug(u'Perform request {0} with id {1}'.format(msg_type, _id)) if msg_type == LSPRequestTypes.DOCUMENT_DID_OPEN: self.file_tokens[file] = { 'text': msg['text'], 'offset': msg['offset'], 'language': msg['language'], } elif msg_type == LSPRequestTypes.DOCUMENT_DID_CHANGE: if file not in self.file_tokens: self.file_tokens[file] = { 'text': '', 'offset': msg['offset'], 'language': msg['language'], } diff = msg['diff'] text = self.file_tokens[file] text['offset'] = msg['offset'] text, _ = self.diff_patch.patch_apply(diff, text['text']) self.file_tokens[file]['text'] = text elif msg_type == LSPRequestTypes.DOCUMENT_DID_CLOSE: self.file_tokens.pop(file, {}) elif msg_type == LSPRequestTypes.DOCUMENT_COMPLETION: tokens = [] if file in self.file_tokens: text_info = self.file_tokens[file] tokens = self.tokenize(text_info['text'], text_info['offset'], text_info['language'], msg['current_word']) tokens = {'params': tokens} self.sig_set_tokens.emit(_id, tokens)
class MainWindow(QWidget): def __init__(self, config: Config) -> None: """ Main window with the GUI and whatever player is being used. """ super().__init__() self.setWindowTitle('vidify') # Setting the window to stay on top if config.stay_on_top: self.setWindowFlags(Qt.WindowStaysOnTopHint) # Setting the fullscreen and window size if config.fullscreen: self.showFullScreen() else: self.resize(config.width or 800, config.height or 600) # Loading the used fonts (Inter) font_db = QFontDatabase() for font in Res.fonts: font_db.addApplicationFont(font) # Initializing the player and saving the config object in the window. self.layout = QHBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.player = initialize_player(config.player, config) logging.info("Using %s as the player", config.player) self.config = config # The API initialization is more complex. For more details, please # check the flow diagram in vidify.api. First we have to check if # the API is saved in the config: try: api_data = get_api_data(config.api) except KeyError: # Otherwise, the user is prompted for an API. After choosing one, # it will be initialized from outside this function. logging.info("API not found: prompting the user") self.API_selection = APISelection() self.layout.addWidget(self.API_selection) self.API_selection.api_chosen.connect(self.on_api_selection) else: logging.info("Using %s as the API", config.api) self.initialize_api(api_data) @Slot(str) def on_api_selection(self, api_str: str) -> None: """ Method called when the API is selected with APISelection. The provided api string must be an existent entry inside the APIData enumeration. """ # Removing the widget used to obtain the API string self.layout.removeWidget(self.API_selection) self.API_selection.setParent(None) self.API_selection.hide() del self.API_selection # Saving the API in the config self.config.api = api_str # Starting the API initialization self.initialize_api(APIData[api_str]) def initialize_api(self, api_data: APIData) -> None: """ Initializes an API with the information from APIData. """ # The API may need interaction with the user to obtain credentials # or similar data. This function will already take care of the # rest of the initialization. if api_data.gui_init_fn is not None: fn = getattr(self, api_data.gui_init_fn) fn() return # Initializing the API with dependency injection. mod = importlib.import_module(api_data.module) cls = getattr(mod, api_data.class_name) self.api = cls() self.wait_for_connection( self.api.connect_api, message=api_data.connect_msg, event_loop_interval=api_data.event_loop_interval) def wait_for_connection(self, conn_fn: Callable[[], None], message: Optional[str] = None, event_loop_interval: int = 1000) -> None: """ Creates an APIConnecter instance and waits for the API to be available, or times out otherwise. """ self.event_loop_interval = event_loop_interval self.api_connecter = APIConnecter( conn_fn, message or "Waiting for connection") self.api_connecter.success.connect(self.on_conn_success) self.api_connecter.fail.connect(self.on_conn_fail) self.layout.addWidget(self.api_connecter) self.api_connecter.start() @Slot() def on_conn_fail(self) -> None: """ If the API failed to connect, the app will be closed. """ print("Timed out waiting for the connection") QCoreApplication.exit(1) @Slot(float) def on_conn_success(self, start_time: float) -> None: """ Once the connection has been established correctly, the API can be started properly. """ logging.info("Succesfully connected to the API") self.layout.removeWidget(self.api_connecter) del self.api_connecter # Initializing the optional audio synchronization extension, now # that there's access to the API's data. Note that this feature # is only available on Linux. if self.config.audiosync: from vidify.audiosync import AudiosyncWorker self.audiosync = AudiosyncWorker(self.api.player_name) self.audiosync.success.connect(self.on_audiosync_success) self.audiosync.failed.connect(self.on_audiosync_fail) # Loading the player self.setStyleSheet(f"background-color:{Colors.black};") self.layout.addWidget(self.player) self.play_video(self.api.artist, self.api.title, start_time) # Connecting to the signals generated by the API self.api.new_song_signal.connect(self.play_video) self.api.position_signal.connect(self.change_video_position) self.api.status_signal.connect(self.change_video_status) # Starting the event loop if it was initially passed as # a parameter. if self.event_loop_interval is not None: self.start_event_loop(self.api.event_loop, self.event_loop_interval) def start_event_loop(self, event_loop: Callable[[], None], ms: int) -> None: """ Starts a "manual" event loop with a timer every `ms` milliseconds. This is used with the SwSpotify API and the Web API to check every `ms` seconds if a change has happened, like if the song was paused. """ logging.info("Starting event loop") timer = QTimer(self) # Qt doesn't accept a method as the parameter so it's converted # to a function. if isinstance(event_loop, types.MethodType): timer.timeout.connect(lambda: event_loop()) else: timer.timeout.connect(event_loop) timer.start(ms) @Slot(bool) def change_video_status(self, is_playing: bool) -> None: """ Slot used for API updates of the video status. """ self.player.pause = not is_playing # If there is an audiosync thread running, this will pause the sound # recording and youtube downloading. if self.config.audiosync and self.audiosync.status != 'idle': self.audiosync.is_running = is_playing @Slot(int) def change_video_position(self, ms: int) -> None: """ Slot used for API updates of the video position. """ if not self.config.audiosync: self.player.position = ms # Audiosync is aborted if the position of the video changed, since # the audio being recorded won't make sense. if self.config.audiosync and self.audiosync.status != 'idle': self.audiosync.abort() @Slot(str, str, float) def play_video(self, artist: str, title: str, start_time: float) -> None: """ Slot used to play a video. This is called when the API is first initialized from this GUI, and afterwards from the event loop handler whenever a new song is detected. If an error was detected when downloading the video, the default one is shown instead. Both audiosync and youtubedl work in separate threads to avoid blocking the GUI. This method will start both of them. """ # Checking that the artist and title are valid first of all if self.api.artist in (None, '') and self.api.title in (None, ''): logging.info("The provided artist and title are empty.") self.on_youtubedl_fail() if self.config.audiosync: self.on_audiosync_fail() return # This delay is used to know the elapsed time until the video # actually starts playing, used in the audiosync feature. self.timestamp = start_time query = f"ytsearch:{format_name(artist, title)} Official Video" if self.config.audiosync: self.launch_audiosync(query) self.launch_youtubedl(query) def launch_audiosync(self, query: str) -> None: """ Starts the audiosync thread, that will call either self.on_audiosync_success, or self.on_audiosync_fail once it's finished. First trying to stop the previous audiosync thread, as only one audiosync thread can be running at once. Note: QThread.start() is guaranteed to work once QThread.run() has returned. Thus, this will wait until it's done and launch the new one. """ self.audiosync.abort() self.audiosync.wait() self.audiosync.youtube_title = query self.audiosync.start() logging.info("Started a new audiosync job") def launch_youtubedl(self, query: str) -> None: """ Starts a YoutubeDL thread that will call either self.on_youtubedl_success or self.on_youtubedl_fail once it's done. """ logging.info("Starting the youtube-dl thread") self.youtubedl = YouTubeDLWorker( query, self.config.debug, self.config.width, self.config.height) self.yt_thread = QThread() self.youtubedl.moveToThread(self.yt_thread) self.yt_thread.started.connect(self.youtubedl.get_url) self.youtubedl.success.connect(self.on_yt_success) self.youtubedl.fail.connect(self.on_youtubedl_fail) self.youtubedl.finish.connect(self.yt_thread.exit) self.yt_thread.start() @Slot() def on_youtubedl_fail(self) -> None: """ If Youtube-dl for whatever reason failed to load the video, a fallback error video is shown, along with a message to let the user know what happened. """ self.player.start_video(Res.default_video, self.api.is_playing) print("The video wasn't found, either because of an issue with your" " internet connection or because the provided data was invalid." " For more information, enable the debug mode.") @Slot(str) def on_yt_success(self, url: str) -> None: """ Obtains the video URL from the Youtube-dl thread and starts playing the video. Also shows the lyrics if enabled. The position of the video isn't set if it's using audiosync, because this is done by the AudiosyncWorker thread. """ self.player.start_video(url, self.api.is_playing) if not self.config.audiosync: try: self.player.position = self.api.position except NotImplementedError: self.player.position = 0 # Finally, the lyrics are displayed. If the video wasn't found, an # error message is shown. if self.config.lyrics: print(get_lyrics(self.api.artist, self.api.title)) @Slot() def on_audiosync_fail(self) -> None: """ Currently, when audiosync fails, nothing happens. """ logging.info("Audiosync module failed to return the lag") @Slot(int) def on_audiosync_success(self, lag: int) -> None: """ Slot used after the audiosync function has finished. It sets the returned lag in milliseconds on the player. This assumes that the song wasn't paused until this issue is fixed: https://github.com/vidify/audiosync/issues/12 """ logging.info("Audiosync module returned %d ms", lag) # The current API position according to what's being recorded. playback_delay = round((time.time() - self.timestamp) * 1000) \ - self.player.position lag += playback_delay # The user's custom audiosync delay. This is basically the time taken # until the module started recording (which may depend on the user # hardware and other things). Thus, it will almost always be a # negative value. lag += self.config.audiosync_calibration logging.info("Total delay is %d ms", lag) if lag > 0: self.player.position += lag elif lag < 0: # If a negative delay is larger than the current player position, # the player position is set to zero after the lag has passed # with a timer. if self.player.position < -lag: self.sync_timer = QTimer(self) self.sync_timer.singleShot( -lag, lambda: self.change_video_position(0)) else: self.player.position += lag def init_spotify_web_api(self) -> None: """ SPOTIFY WEB API CUSTOM FUNCTION Note: the Tekore imports are done inside the functions so that Tekore isn't needed for whoever doesn't plan to use the Spotify Web API. """ from vidify.api.spotify.web import get_token from vidify.gui.api.spotify_web import SpotifyWebPrompt token = get_token(self.config.refresh_token, self.config.client_id, self.config.client_secret) if token is not None: # If the previous token was valid, the API can already start. logging.info("Reusing a previously generated token") self.start_spotify_web_api(token, save_config=False) else: # Otherwise, the credentials are obtained with the GUI. When # a valid auth token is ready, the GUI will initialize the API # automatically exactly like above. The GUI won't ask for a # redirect URI for now. logging.info("Asking the user for credentials") # The SpotifyWebPrompt handles the interaction with the user and # emits a `done` signal when it's done. self._spotify_web_prompt = SpotifyWebPrompt( self.config.client_id, self.config.client_secret, self.config.redirect_uri) self._spotify_web_prompt.done.connect(self.start_spotify_web_api) self.layout.addWidget(self._spotify_web_prompt) def start_spotify_web_api(self, token: 'RefreshingToken', save_config: bool = True) -> None: """ SPOTIFY WEB API CUSTOM FUNCTION Initializes the Web API, also saving them in the config for future usage (if `save_config` is true). """ from vidify.api.spotify.web import SpotifyWebAPI logging.info("Initializing the Spotify Web API") # Initializing the web API self.api = SpotifyWebAPI(token) api_data = APIData['SPOTIFY_WEB'] self.wait_for_connection( self.api.connect_api, message=api_data.connect_msg, event_loop_interval=api_data.event_loop_interval) # The obtained credentials are saved for the future if save_config: logging.info("Saving the Spotify Web API credentials") self.config.client_secret = self._spotify_web_prompt.client_secret self.config.client_id = self._spotify_web_prompt.client_id self.config.refresh_token = token.refresh_token # The credentials prompt widget is removed after saving the data. It # may not exist because start_spotify_web_api was called directly, # so errors are taken into account. try: self.layout.removeWidget(self._spotify_web_prompt) self._spotify_web_prompt.hide() del self._spotify_web_prompt except AttributeError: pass
class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget, FigureBrowserWidget): """ Shell widget for the IPython Console This is the widget in charge of executing code """ # NOTE: Signals can't be assigned separately to each widget # That's why we define all needed signals here. # For NamepaceBrowserWidget sig_show_syspath = Signal(object) sig_show_env = Signal(object) # For FigureBrowserWidget sig_new_inline_figure = Signal(object, str) # For DebuggingWidget sig_pdb_step = Signal(str, int) sig_pdb_state = Signal(bool, dict) # For ShellWidget focus_changed = Signal() new_client = Signal() sig_is_spykernel = Signal(object) sig_kernel_restarted_message = Signal(str) sig_kernel_restarted = Signal() sig_prompt_ready = Signal() sig_remote_execute = Signal() # For global working directory sig_change_cwd = Signal(str) # For printing internal errors sig_exception_occurred = Signal(dict) def __init__(self, ipyclient, additional_options, interpreter_versions, external_kernel, *args, **kw): # To override the Qt widget used by RichJupyterWidget self.custom_control = ControlWidget self.custom_page_control = PageControlWidget self.custom_edit = True self.spyder_kernel_comm = KernelComm() self.spyder_kernel_comm.sig_exception_occurred.connect( self.sig_exception_occurred) super(ShellWidget, self).__init__(*args, **kw) self.ipyclient = ipyclient self.additional_options = additional_options self.interpreter_versions = interpreter_versions self.external_kernel = external_kernel self._cwd = '' # Keyboard shortcuts self.shortcuts = self.create_shortcuts() # Set the color of the matched parentheses here since the qtconsole # uses a hard-coded value that is not modified when the color scheme is # set in the qtconsole constructor. See spyder-ide/spyder#4806. self.set_bracket_matcher_color_scheme(self.syntax_style) self.shutdown_called = False self.kernel_manager = None self.kernel_client = None self.shutdown_thread = None handlers = { 'pdb_state': self.set_pdb_state, 'pdb_continue': self.pdb_continue, 'get_pdb_settings': self.handle_get_pdb_settings, 'run_cell': self.handle_run_cell, 'cell_count': self.handle_cell_count, 'current_filename': self.handle_current_filename, 'get_file_code': self.handle_get_file_code, 'set_debug_state': self.handle_debug_state, 'update_syspath': self.update_syspath, 'do_where': self.do_where, } for request_id in handlers: self.spyder_kernel_comm.register_call_handler( request_id, handlers[request_id]) def __del__(self): """Avoid destroying shutdown_thread.""" if (self.shutdown_thread is not None and self.shutdown_thread.isRunning()): self.shutdown_thread.wait() # ---- Public API --------------------------------------------------------- def shutdown(self): """Shutdown kernel""" self.shutdown_called = True self.spyder_kernel_comm.close() self.spyder_kernel_comm.shutdown_comm_channel() self.kernel_manager.stop_restarter() self.shutdown_thread = QThread() self.shutdown_thread.run = self.kernel_manager.shutdown_kernel if self.kernel_client is not None: self.shutdown_thread.finished.connect( self.kernel_client.stop_channels) self.shutdown_thread.start() def will_close(self, externally_managed): """ Close communication channels with the kernel if shutdown was not called. If the kernel is not externally managed, shutdown the kernel as well. """ if not self.shutdown_called and not externally_managed: # Make sure the channels are stopped self.spyder_kernel_comm.close() self.spyder_kernel_comm.shutdown_comm_channel() self.kernel_manager.stop_restarter() self.kernel_manager.shutdown_kernel(now=True) if self.kernel_client is not None: self.kernel_client.stop_channels() if externally_managed: self.spyder_kernel_comm.close() if self.kernel_client is not None: self.kernel_client.stop_channels() super(ShellWidget, self).will_close(externally_managed) def call_kernel(self, interrupt=False, blocking=False, callback=None, timeout=None): """ Send message to Spyder kernel connected to this console. Parameters ---------- interrupt: bool Interrupt the kernel while running or in Pdb to perform the call. blocking: bool Make a blocking call, i.e. wait on this side until the kernel sends its response. callback: callable Callable to process the response sent from the kernel on the Spyder side. timeout: int or None Maximum time (in seconds) before giving up when making a blocking call to the kernel. If None, a default timeout (defined in commbase.py, present in spyder-kernels) is used. """ return self.spyder_kernel_comm.remote_call( interrupt=interrupt, blocking=blocking, callback=callback, timeout=timeout ) def set_kernel_client_and_manager(self, kernel_client, kernel_manager): """Set the kernel client and manager""" self.kernel_manager = kernel_manager self.kernel_client = kernel_client self.spyder_kernel_comm.open_comm(kernel_client) # Redefine the complete method to work while debugging. self.redefine_complete_for_dbg(self.kernel_client) def set_exit_callback(self): """Set exit callback for this shell.""" self.exit_requested.connect(self.ipyclient.exit_callback) def is_running(self): if self.kernel_client is not None and \ self.kernel_client.channels_running: return True else: return False def is_spyder_kernel(self): """Determine if the kernel is from Spyder.""" code = u"getattr(get_ipython().kernel, 'set_value', False)" if self._reading: return else: self.silent_exec_method(code) def set_cwd(self, dirname): """Set shell current working directory.""" if os.name == 'nt': # Use normpath instead of replacing '\' with '\\' # See spyder-ide/spyder#10785 dirname = osp.normpath(dirname) if self.ipyclient.hostname is None: self.call_kernel(interrupt=True).set_cwd(dirname) self._cwd = dirname def update_cwd(self): """Update current working directory. Retrieve the cwd and emit a signal connected to the working directory widget. (see: handle_exec_method()) """ if self.kernel_client is None: return self.call_kernel(callback=self.remote_set_cwd).get_cwd() def remote_set_cwd(self, cwd): """Get current working directory from kernel.""" self._cwd = cwd self.sig_change_cwd.emit(self._cwd) def set_bracket_matcher_color_scheme(self, color_scheme): """Set color scheme for matched parentheses.""" bsh = sh.BaseSH(parent=self, color_scheme=color_scheme) mpcolor = bsh.get_matched_p_color() self._bracket_matcher.format.setBackground(mpcolor) def set_color_scheme(self, color_scheme, reset=True): """Set color scheme of the shell.""" self.set_bracket_matcher_color_scheme(color_scheme) self.style_sheet, dark_color = create_qss_style(color_scheme) self.syntax_style = color_scheme self._style_sheet_changed() self._syntax_style_changed() if reset: self.reset(clear=True) if not dark_color: # Needed to change the colors of tracebacks self.silent_execute("%colors linux") self.call_kernel().set_sympy_forecolor(background_color='dark') else: self.silent_execute("%colors lightbg") self.call_kernel().set_sympy_forecolor(background_color='light') def update_syspath(self, path_dict, new_path_dict): """Update sys.path contents on kernel.""" self.call_kernel( interrupt=True, blocking=False).update_syspath(path_dict, new_path_dict) def request_syspath(self): """Ask the kernel for sys.path contents.""" self.call_kernel( interrupt=True, callback=self.sig_show_syspath.emit).get_syspath() def request_env(self): """Ask the kernel for environment variables.""" self.call_kernel( interrupt=True, callback=self.sig_show_env.emit).get_env() # --- To handle the banner def long_banner(self): """Banner for clients with additional content.""" # Default banner py_ver = self.interpreter_versions['python_version'].split('\n')[0] ipy_ver = self.interpreter_versions['ipython_version'] banner_parts = [ 'Python %s\n' % py_ver, 'Type "copyright", "credits" or "license" for more information.\n\n', 'IPython %s -- An enhanced Interactive Python.\n' % ipy_ver ] banner = ''.join(banner_parts) # Pylab additions pylab_o = self.additional_options['pylab'] autoload_pylab_o = self.additional_options['autoload_pylab'] mpl_installed = programs.is_module_installed('matplotlib') if mpl_installed and (pylab_o and autoload_pylab_o): pylab_message = ("\nPopulating the interactive namespace from " "numpy and matplotlib\n") banner = banner + pylab_message # Sympy additions sympy_o = self.additional_options['sympy'] if sympy_o: lines = """ These commands were executed: >>> from __future__ import division >>> from sympy import * >>> x, y, z, t = symbols('x y z t') >>> k, m, n = symbols('k m n', integer=True) >>> f, g, h = symbols('f g h', cls=Function) """ banner = banner + lines if (pylab_o and sympy_o): lines = """ Warning: pylab (numpy and matplotlib) and symbolic math (sympy) are both enabled at the same time. Some pylab functions are going to be overrided by the sympy module (e.g. plot) """ banner = banner + lines return banner def short_banner(self): """Short banner with Python and IPython versions only.""" py_ver = self.interpreter_versions['python_version'].split(' ')[0] ipy_ver = self.interpreter_versions['ipython_version'] banner = 'Python %s -- IPython %s' % (py_ver, ipy_ver) return banner # --- To define additional shortcuts def clear_console(self): if self.is_waiting_pdb_input(): self.dbg_exec_magic('clear') else: self.execute("%clear") # Stop reading as any input has been removed. self._reading = False def _reset_namespace(self): warning = CONF.get('ipython_console', 'show_reset_namespace_warning') self.reset_namespace(warning=warning) def reset_namespace(self, warning=False, message=False): """Reset the namespace by removing all names defined by the user.""" reset_str = _("Remove all variables") warn_str = _("All user-defined variables will be removed. " "Are you sure you want to proceed?") # Don't show the warning when running our tests. if running_under_pytest(): warning = False # This is necessary to make resetting variables work in external # kernels. # See spyder-ide/spyder#9505. try: kernel_env = self.kernel_manager._kernel_spec.env except AttributeError: kernel_env = {} if warning: box = MessageCheckBox(icon=QMessageBox.Warning, parent=self) box.setWindowTitle(reset_str) box.set_checkbox_text(_("Don't show again.")) box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) box.setDefaultButton(QMessageBox.Yes) box.set_checked(False) box.set_check_visible(True) box.setText(warn_str) answer = box.exec_() # Update checkbox based on user interaction CONF.set('ipython_console', 'show_reset_namespace_warning', not box.is_checked()) self.ipyclient.reset_warning = not box.is_checked() if answer != QMessageBox.Yes: return try: if self.is_waiting_pdb_input(): self.dbg_exec_magic('reset', '-f') else: if message: self.reset() self._append_html(_("<br><br>Removing all variables..." "\n<hr>"), before_prompt=False) self.silent_execute("%reset -f") if kernel_env.get('SPY_AUTOLOAD_PYLAB_O') == 'True': self.silent_execute("from pylab import *") if kernel_env.get('SPY_SYMPY_O') == 'True': sympy_init = """ from __future__ import division from sympy import * x, y, z, t = symbols('x y z t') k, m, n = symbols('k m n', integer=True) f, g, h = symbols('f g h', cls=Function) init_printing()""" self.silent_execute(dedent(sympy_init)) if kernel_env.get('SPY_RUN_CYTHON') == 'True': self.silent_execute("%reload_ext Cython") # This doesn't need to interrupt the kernel because # "%reset -f" is being executed before it. # Fixes spyder-ide/spyder#12689 self.refresh_namespacebrowser(interrupt=False) if not self.external_kernel: self.call_kernel().close_all_mpl_figures() except AttributeError: pass def create_shortcuts(self): """Create shortcuts for ipyconsole.""" inspect = CONF.config_shortcut( self._control.inspect_current_object, context='Console', name='Inspect current object', parent=self) clear_console = CONF.config_shortcut( self.clear_console, context='Console', name='Clear shell', parent=self) restart_kernel = CONF.config_shortcut( self.ipyclient.restart_kernel, context='ipython_console', name='Restart kernel', parent=self) new_tab = CONF.config_shortcut( lambda: self.new_client.emit(), context='ipython_console', name='new tab', parent=self) reset_namespace = CONF.config_shortcut( lambda: self._reset_namespace(), context='ipython_console', name='reset namespace', parent=self) array_inline = CONF.config_shortcut( self._control.enter_array_inline, context='array_builder', name='enter array inline', parent=self) array_table = CONF.config_shortcut( self._control.enter_array_table, context='array_builder', name='enter array table', parent=self) clear_line = CONF.config_shortcut( self.ipyclient.clear_line, context='console', name='clear line', parent=self) return [inspect, clear_console, restart_kernel, new_tab, reset_namespace, array_inline, array_table, clear_line] # --- To communicate with the kernel def silent_execute(self, code): """Execute code in the kernel without increasing the prompt""" try: self.kernel_client.execute(to_text_string(code), silent=True) except AttributeError: pass def silent_exec_method(self, code): """Silently execute a kernel method and save its reply The methods passed here **don't** involve getting the value of a variable but instead replies that can be handled by ast.literal_eval. To get a value see `get_value` Parameters ---------- code : string Code that contains the kernel method as part of its string See Also -------- handle_exec_method : Method that deals with the reply Note ---- This is based on the _silent_exec_callback method of RichJupyterWidget. Therefore this is licensed BSD """ # Generate uuid, which would be used as an indication of whether or # not the unique request originated from here local_uuid = to_text_string(uuid.uuid1()) code = to_text_string(code) if self.kernel_client is None: return msg_id = self.kernel_client.execute('', silent=True, user_expressions={ local_uuid:code }) self._kernel_methods[local_uuid] = code self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_method') def handle_exec_method(self, msg): """ Handle data returned by silent executions of kernel methods This is based on the _handle_exec_callback of RichJupyterWidget. Therefore this is licensed BSD. """ user_exp = msg['content'].get('user_expressions') if not user_exp: return for expression in user_exp: if expression in self._kernel_methods: # Process kernel reply method = self._kernel_methods[expression] reply = user_exp[expression] data = reply.get('data') if 'getattr' in method: if data is not None and 'text/plain' in data: is_spyder_kernel = data['text/plain'] if 'SpyderKernel' in is_spyder_kernel: self.sig_is_spykernel.emit(self) # Remove method after being processed self._kernel_methods.pop(expression) def set_backend_for_mayavi(self, command): """ Mayavi plots require the Qt backend, so we try to detect if one is generated to change backends """ calling_mayavi = False lines = command.splitlines() for l in lines: if not l.startswith('#'): if 'import mayavi' in l or 'from mayavi' in l: calling_mayavi = True break if calling_mayavi: message = _("Changing backend to Qt for Mayavi") self._append_plain_text(message + '\n') self.silent_execute("%gui inline\n%gui qt") def change_mpl_backend(self, command): """ If the user is trying to change Matplotlib backends with %matplotlib, send the same command again to the kernel to correctly change it. Fixes spyder-ide/spyder#4002. """ if command.startswith('%matplotlib') and \ len(command.splitlines()) == 1: if not 'inline' in command: self.silent_execute(command) # ---- Spyder-kernels methods --------------------------------------------- def get_editor(self, filename): """Get editor for filename and set it as the current editor.""" editorstack = self.get_editorstack() if editorstack is None: return None if not filename: return None index = editorstack.has_filename(filename) if index is None: return None return editorstack.data[index].editor def get_editorstack(self): """Get the current editorstack.""" plugin = self.ipyclient.plugin if plugin.main.editor is not None: editor = plugin.main.editor return editor.get_current_editorstack() raise RuntimeError('No editorstack found.') def handle_get_file_code(self, filename, save_all=True): """ Return the bytes that compose the file. Bytes are returned instead of str to support non utf-8 files. """ editorstack = self.get_editorstack() if save_all and CONF.get('editor', 'save_all_before_run', True): editorstack.save_all(save_new_files=False) editor = self.get_editor(filename) if editor is None: # Load it from file instead text, _enc = encoding.read(filename) return text return editor.toPlainText() def handle_run_cell(self, cell_name, filename): """ Get cell code from cell name and file name. """ editorstack = self.get_editorstack() editor = self.get_editor(filename) if editor is None: raise RuntimeError( "File {} not open in the editor".format(filename)) editorstack.last_cell_call = (filename, cell_name) # The file is open, load code from editor return editor.get_cell_code(cell_name) def handle_cell_count(self, filename): """Get number of cells in file to loop.""" editorstack = self.get_editorstack() editor = self.get_editor(filename) if editor is None: raise RuntimeError( "File {} not open in the editor".format(filename)) # The file is open, get cell count from editor return editor.get_cell_count() def handle_current_filename(self): """Get the current filename.""" return self.get_editorstack().get_current_finfo().filename # ---- Public methods (overrode by us) ------------------------------------ def request_restart_kernel(self): """Reimplemented to call our own restart mechanism.""" self.ipyclient.restart_kernel() # ---- Private methods (overrode by us) ----------------------------------- def _handle_error(self, msg): """ Reimplemented to reset the prompt if the error comes after the reply """ self._process_execute_error(msg) def _context_menu_make(self, pos): """Reimplement the IPython context menu""" menu = super(ShellWidget, self)._context_menu_make(pos) return self.ipyclient.add_actions_to_context_menu(menu) def _banner_default(self): """ Reimplement banner creation to let the user decide if he wants a banner or not """ # Don't change banner for external kernels if self.external_kernel: return '' show_banner_o = self.additional_options['show_banner'] if show_banner_o: return self.long_banner() else: return self.short_banner() def _kernel_restarted_message(self, died=True): msg = _("Kernel died, restarting") if died else _("Kernel restarting") self.sig_kernel_restarted_message.emit(msg) def _handle_kernel_restarted(self): super(ShellWidget, self)._handle_kernel_restarted() self.sig_kernel_restarted.emit() def _syntax_style_changed(self): """Refresh the highlighting with the current syntax style by class.""" if self._highlighter is None: # ignore premature calls return if self.syntax_style: self._highlighter._style = create_style_class(self.syntax_style) self._highlighter._clear_caches() else: self._highlighter.set_style_sheet(self.style_sheet) def _prompt_started_hook(self): """Emit a signal when the prompt is ready.""" if not self._reading: self._highlighter.highlighting_on = True self.sig_prompt_ready.emit() def _handle_execute_input(self, msg): """Handle an execute_input message""" super(ShellWidget, self)._handle_execute_input(msg) self.sig_remote_execute.emit() #---- Qt methods ---------------------------------------------------------- def focusInEvent(self, event): """Reimplement Qt method to send focus change notification""" self.focus_changed.emit() return super(ShellWidget, self).focusInEvent(event) def focusOutEvent(self, event): """Reimplement Qt method to send focus change notification""" self.focus_changed.emit() return super(ShellWidget, self).focusOutEvent(event)
class ClientWidget(QWidget, SaveHistoryMixin, SpyderWidgetMixin): """ Client widget for the IPython Console This widget is necessary to handle the interaction between the plugin and each shell widget. """ sig_append_to_history_requested = Signal(str, str) sig_execution_state_changed = Signal() CONF_SECTION = 'ipython_console' SEPARATOR = '{0}## ---({1})---'.format(os.linesep*2, time.ctime()) INITHISTORY = ['# -*- coding: utf-8 -*-', '# *** Spyder Python Console History Log ***', ] def __init__(self, parent, id_, history_filename, config_options, additional_options, interpreter_versions, connection_file=None, hostname=None, context_menu_actions=(), menu_actions=None, is_external_kernel=False, is_spyder_kernel=True, given_name=None, give_focus=True, options_button=None, time_label=None, show_elapsed_time=False, reset_warning=True, ask_before_restart=True, ask_before_closing=False, css_path=None, handlers={}, stderr_obj=None, stdout_obj=None, fault_obj=None): super(ClientWidget, self).__init__(parent) SaveHistoryMixin.__init__(self, history_filename) # --- Init attrs self.container = parent self.id_ = id_ self.connection_file = connection_file self.hostname = hostname self.menu_actions = menu_actions self.is_external_kernel = is_external_kernel self.given_name = given_name self.show_elapsed_time = show_elapsed_time self.reset_warning = reset_warning self.ask_before_restart = ask_before_restart self.ask_before_closing = ask_before_closing # --- Other attrs self.context_menu_actions = context_menu_actions self.time_label = time_label self.options_button = options_button self.history = [] self.allow_rename = True self.is_error_shown = False self.error_text = None self.restart_thread = None self.give_focus = give_focus if css_path is None: self.css_path = CSS_PATH else: self.css_path = css_path # --- Widgets self.shellwidget = ShellWidget( config=config_options, ipyclient=self, additional_options=additional_options, interpreter_versions=interpreter_versions, is_external_kernel=is_external_kernel, is_spyder_kernel=is_spyder_kernel, handlers=handlers, local_kernel=True ) self.infowidget = self.container.infowidget self.blank_page = self._create_blank_page() self.loading_page = self._create_loading_page() # To keep a reference to the page to be displayed # in infowidget self.info_page = None self._before_prompt_is_ready() # Elapsed time self.t0 = time.monotonic() self.timer = QTimer(self) # --- Layout self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.shellwidget) if self.infowidget is not None: self.layout.addWidget(self.infowidget) self.setLayout(self.layout) # --- Exit function self.exit_callback = lambda: self.container.close_client(client=self) # --- Dialog manager self.dialog_manager = DialogManager() # --- Standard files handling self.stderr_obj = stderr_obj self.stdout_obj = stdout_obj self.fault_obj = fault_obj self.std_poll_timer = None if self.stderr_obj is not None or self.stdout_obj is not None: self.std_poll_timer = QTimer(self) self.std_poll_timer.timeout.connect(self.poll_std_file_change) self.std_poll_timer.setInterval(1000) self.std_poll_timer.start() self.shellwidget.executed.connect(self.poll_std_file_change) self.start_successful = False def __del__(self): """Close threads to avoid segfault.""" if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.quit() self.restart_thread.wait() # ----- Private methods --------------------------------------------------- def _before_prompt_is_ready(self, show_loading_page=True): """Configuration before kernel is connected.""" if show_loading_page: self._show_loading_page() self.shellwidget.sig_prompt_ready.connect( self._when_prompt_is_ready) # If remote execution, the loading page should be hidden as well self.shellwidget.sig_remote_execute.connect( self._when_prompt_is_ready) def _when_prompt_is_ready(self): """Configuration after the prompt is shown.""" self.start_successful = True # To hide the loading page self._hide_loading_page() # Show possible errors when setting Matplotlib backend self._show_mpl_backend_errors() # To show if special console is valid self._check_special_console_error() # Set the initial current working directory self._set_initial_cwd() self.shellwidget.sig_prompt_ready.disconnect( self._when_prompt_is_ready) self.shellwidget.sig_remote_execute.disconnect( self._when_prompt_is_ready) # It's necessary to do this at this point to avoid giving # focus to _control at startup. self._connect_control_signals() if self.give_focus: self.shellwidget._control.setFocus() def _create_loading_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(LOADING) loading_img = get_image_path('loading_sprites') if os.name == 'nt': loading_img = loading_img.replace('\\', '/') message = _("Connecting to kernel...") page = loading_template.substitute(css_path=self.css_path, loading_img=loading_img, message=message) return page def _create_blank_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(BLANK) page = loading_template.substitute(css_path=self.css_path) return page def _show_loading_page(self): """Show animation while the kernel is loading.""" if self.infowidget is not None: self.shellwidget.hide() self.infowidget.show() self.info_page = self.loading_page self.set_info_page() def _hide_loading_page(self): """Hide animation shown while the kernel is loading.""" if self.infowidget is not None: self.infowidget.hide() self.info_page = self.blank_page self.set_info_page() self.shellwidget.show() def _read_stderr(self): """Read the stderr file of the kernel.""" # We need to read stderr_file as bytes to be able to # detect its encoding with chardet f = open(self.stderr_file, 'rb') try: stderr_text = f.read() # This is needed to avoid showing an empty error message # when the kernel takes too much time to start. # See spyder-ide/spyder#8581. if not stderr_text: return '' # This is needed since the stderr file could be encoded # in something different to utf-8. # See spyder-ide/spyder#4191. encoding = get_coding(stderr_text) stderr_text = to_text_string(stderr_text, encoding) return stderr_text finally: f.close() def _show_mpl_backend_errors(self): """ Show possible errors when setting the selected Matplotlib backend. """ if self.shellwidget.is_spyder_kernel: self.shellwidget.call_kernel().show_mpl_backend_errors() def _check_special_console_error(self): """Check if the dependecies for special consoles are available.""" self.shellwidget.call_kernel( callback=self._show_special_console_error ).is_special_kernel_valid() def _show_special_console_error(self, missing_dependency): if missing_dependency is not None: error_message = _( "Your Python environment or installation doesn't have the " "<tt>{missing_dependency}</tt> module installed or it " "occurred a problem importing it. Due to that, it is not " "possible for Spyder to create this special console for " "you." ).format(missing_dependency=missing_dependency) self.show_kernel_error(error_message) def _abort_kernel_restart(self): """ Abort kernel restart if there are errors while starting it. We also ignore errors about comms, which are irrelevant. """ if self.start_successful: return False stderr = self.stderr_obj.get_contents() if not stderr: return False # There is an error. If it is benign, ignore. for line in stderr.splitlines(): if line and not self.is_benign_error(line): return True return False def _connect_control_signals(self): """Connect signals of control widgets.""" control = self.shellwidget._control page_control = self.shellwidget._page_control control.sig_focus_changed.connect( self.container.sig_focus_changed) page_control.sig_focus_changed.connect( self.container.sig_focus_changed) control.sig_visibility_changed.connect( self.container.refresh_container) page_control.sig_visibility_changed.connect( self.container.refresh_container) page_control.sig_show_find_widget_requested.connect( self.container.find_widget.show) def _set_initial_cwd(self): """Set initial cwd according to preferences.""" logger.debug("Setting initial working directory") cwd_path = get_home_dir() project_path = self.container.get_active_project_path() # This is for the first client if self.id_['int_id'] == '1': if self.get_conf( 'startup/use_project_or_home_directory', section='workingdir' ): cwd_path = get_home_dir() if project_path is not None: cwd_path = project_path elif self.get_conf( 'startup/use_fixed_directory', section='workingdir' ): cwd_path = self.get_conf( 'startup/fixed_directory', default=get_home_dir(), section='workingdir' ) else: # For new clients if self.get_conf( 'console/use_project_or_home_directory', section='workingdir' ): cwd_path = get_home_dir() if project_path is not None: cwd_path = project_path elif self.get_conf('console/use_cwd', section='workingdir'): cwd_path = self.container.get_working_directory() elif self.get_conf( 'console/use_fixed_directory', section='workingdir' ): cwd_path = self.get_conf( 'console/fixed_directory', default=get_home_dir(), section='workingdir' ) if osp.isdir(cwd_path): self.shellwidget.set_cwd(cwd_path) # ----- Public API -------------------------------------------------------- @property def kernel_id(self): """Get kernel id.""" if self.connection_file is not None: json_file = osp.basename(self.connection_file) return json_file.split('.json')[0] def remove_std_files(self, is_last_client=True): """Remove stderr_file associated with the client.""" try: self.shellwidget.executed.disconnect(self.poll_std_file_change) except TypeError: pass if self.std_poll_timer is not None: self.std_poll_timer.stop() if is_last_client: if self.stderr_obj is not None: self.stderr_obj.remove() if self.stdout_obj is not None: self.stdout_obj.remove() if self.fault_obj is not None: self.fault_obj.remove() @Slot() def poll_std_file_change(self): """Check if the stderr or stdout file just changed.""" self.shellwidget.call_kernel().flush_std() starting = self.shellwidget._starting if self.stderr_obj is not None: stderr = self.stderr_obj.poll_file_change() if stderr: if self.is_benign_error(stderr): return if self.shellwidget.isHidden(): # Avoid printing the same thing again if self.error_text != '<tt>%s</tt>' % stderr: full_stderr = self.stderr_obj.get_contents() self.show_kernel_error('<tt>%s</tt>' % full_stderr) if starting: self.shellwidget.banner = ( stderr + '\n' + self.shellwidget.banner) else: self.shellwidget._append_plain_text( '\n' + stderr, before_prompt=True) if self.stdout_obj is not None: stdout = self.stdout_obj.poll_file_change() if stdout: if starting: self.shellwidget.banner = ( stdout + '\n' + self.shellwidget.banner) else: self.shellwidget._append_plain_text( '\n' + stdout, before_prompt=True) def configure_shellwidget(self, give_focus=True): """Configure shellwidget after kernel is connected.""" self.give_focus = give_focus # Make sure the kernel sends the comm config over self.shellwidget.call_kernel()._send_comm_config() # Set exit callback self.shellwidget.set_exit_callback() # To save history self.shellwidget.executing.connect(self.add_to_history) # For Mayavi to run correctly self.shellwidget.executing.connect( self.shellwidget.set_backend_for_mayavi) # To update history after execution self.shellwidget.executed.connect(self.update_history) # To update the Variable Explorer after execution self.shellwidget.executed.connect( self.shellwidget.refresh_namespacebrowser) # To enable the stop button when executing a process self.shellwidget.executing.connect( self.sig_execution_state_changed) # To disable the stop button after execution stopped self.shellwidget.executed.connect( self.sig_execution_state_changed) # To show kernel restarted/died messages self.shellwidget.sig_kernel_restarted_message.connect( self.kernel_restarted_message) self.shellwidget.sig_kernel_restarted.connect( self._finalise_restart) # To correctly change Matplotlib backend interactively self.shellwidget.executing.connect( self.shellwidget.change_mpl_backend) # To show env and sys.path contents self.shellwidget.sig_show_syspath.connect(self.show_syspath) self.shellwidget.sig_show_env.connect(self.show_env) # To sync with working directory toolbar self.shellwidget.executed.connect(self.shellwidget.update_cwd) # To apply style self.set_color_scheme(self.shellwidget.syntax_style, reset=False) if self.fault_obj is not None: # To display faulthandler self.shellwidget.call_kernel().enable_faulthandler( self.fault_obj.filename) def add_to_history(self, command): """Add command to history""" if self.shellwidget.is_debugging(): return return super(ClientWidget, self).add_to_history(command) def is_client_executing(self): return (self.shellwidget._executing or self.shellwidget.is_waiting_pdb_input()) @Slot() def stop_button_click_handler(self): """Method to handle what to do when the stop button is pressed""" # Interrupt computations or stop debugging if not self.shellwidget.is_waiting_pdb_input(): self.interrupt_kernel() else: self.shellwidget.pdb_execute_command('exit') def show_kernel_error(self, error): """Show kernel initialization errors in infowidget.""" self.error_text = error if self.is_benign_error(error): return InstallerIPythonKernelError(error) # Replace end of line chars with <br> eol = sourcecode.get_eol_chars(error) if eol: error = error.replace(eol, '<br>') # Don't break lines in hyphens # From https://stackoverflow.com/q/7691569/438386 error = error.replace('-', '‑') # Create error page message = _("An error ocurred while starting the kernel") kernel_error_template = Template(KERNEL_ERROR) self.info_page = kernel_error_template.substitute( css_path=self.css_path, message=message, error=error) # Show error if self.infowidget is not None: self.set_info_page() self.shellwidget.hide() self.infowidget.show() # Tell the client we're in error mode self.is_error_shown = True # Stop shellwidget self.shellwidget.shutdown() self.remove_std_files(is_last_client=False) def is_benign_error(self, error): """Decide if an error is benign in order to filter it.""" benign_errors = [ # Error when switching from the Qt5 backend to the Tk one. # See spyder-ide/spyder#17488 "KeyboardInterrupt caught in kernel", "QSocketNotifier: Multiple socket notifiers for same socket", # Error when switching from the Tk backend to the Qt5 one. # See spyder-ide/spyder#17488 "Tcl_AsyncDelete async handler deleted by the wrong thread", "error in background error handler:", " while executing", '"::tcl::Bgerror', # Avoid showing this warning because it was up to the user to # disable secure writes. "WARNING: Insecure writes have been enabled via environment", # Old error "No such comm" ] return any([err in error for err in benign_errors]) def get_name(self): """Return client name""" if self.given_name is None: # Name according to host if self.hostname is None: name = _("Console") else: name = self.hostname # Adding id to name client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = name + u' ' + client_id elif self.given_name in ["Pylab", "SymPy", "Cython"]: client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = self.given_name + u' ' + client_id else: name = self.given_name + u'/' + self.id_['str_id'] return name def get_control(self): """Return the text widget (or similar) to give focus to""" # page_control is the widget used for paging page_control = self.shellwidget._page_control if page_control and page_control.isVisible(): return page_control else: return self.shellwidget._control def get_kernel(self): """Get kernel associated with this client""" return self.shellwidget.kernel_manager def add_actions_to_context_menu(self, menu): """Add actions to IPython widget context menu""" add_actions(menu, self.context_menu_actions) return menu def set_font(self, font): """Set IPython widget's font""" self.shellwidget._control.setFont(font) self.shellwidget.font = font def set_color_scheme(self, color_scheme, reset=True): """Set IPython color scheme.""" # Needed to handle not initialized kernel_client # See spyder-ide/spyder#6996. try: self.shellwidget.set_color_scheme(color_scheme, reset) except AttributeError: pass def shutdown(self, is_last_client): """Shutdown connection and kernel if needed.""" self.dialog_manager.close_all() if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.finished.disconnect() self.restart_thread.quit() self.restart_thread.wait() shutdown_kernel = ( is_last_client and not self.is_external_kernel and not self.is_error_shown) self.shellwidget.shutdown(shutdown_kernel) self.remove_std_files(shutdown_kernel) def interrupt_kernel(self): """Interrupt the associanted Spyder kernel if it's running""" # Needed to prevent a crash when a kernel is not running. # See spyder-ide/spyder#6299. try: self.shellwidget.request_interrupt_kernel() except RuntimeError: pass @Slot() def restart_kernel(self): """ Restart the associated kernel. Took this code from the qtconsole project Licensed under the BSD license """ sw = self.shellwidget if not running_under_pytest() and self.ask_before_restart: message = _('Are you sure you want to restart the kernel?') buttons = QMessageBox.Yes | QMessageBox.No result = QMessageBox.question(self, _('Restart kernel?'), message, buttons) else: result = None if (result == QMessageBox.Yes or running_under_pytest() or not self.ask_before_restart): if sw.kernel_manager: if self.infowidget is not None: if self.infowidget.isVisible(): self.infowidget.hide() if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return self._show_loading_page() # Close comm sw.spyder_kernel_comm.close() # Stop autorestart mechanism sw.kernel_manager.stop_restarter() sw.kernel_manager.autorestart = False # Reconfigure client before the new kernel is connected again. self._before_prompt_is_ready(show_loading_page=False) # Create and run restarting thread if (self.restart_thread is not None and self.restart_thread.isRunning()): self.restart_thread.finished.disconnect() self.restart_thread.quit() self.restart_thread.wait() self.restart_thread = QThread(None) self.restart_thread.run = self._restart_thread_main self.restart_thread.error = None self.restart_thread.finished.connect( lambda: self._finalise_restart(True)) self.restart_thread.start() else: sw._append_plain_text( _('Cannot restart a kernel not started by Spyder\n'), before_prompt=True ) self._hide_loading_page() def _restart_thread_main(self): """Restart the kernel in a thread.""" try: self.shellwidget.kernel_manager.restart_kernel( stderr=self.stderr_obj.handle, stdout=self.stdout_obj.handle) except RuntimeError as e: self.restart_thread.error = e def _finalise_restart(self, reset=False): """Finishes the restarting of the kernel.""" sw = self.shellwidget if self._abort_kernel_restart(): sw.spyder_kernel_comm.close() return if self.restart_thread and self.restart_thread.error is not None: sw._append_plain_text( _('Error restarting kernel: %s\n') % self.restart_thread.error, before_prompt=True ) else: if self.fault_obj is not None: fault = self.fault_obj.get_contents() if fault: fault = self.filter_fault(fault) self.shellwidget._append_plain_text( '\n' + fault, before_prompt=True) # Reset Pdb state and reopen comm sw._pdb_in_loop = False sw.spyder_kernel_comm.remove() try: sw.spyder_kernel_comm.open_comm(sw.kernel_client) except AttributeError: # An error occurred while opening our comm channel. # Aborting! return # Start autorestart mechanism sw.kernel_manager.autorestart = True sw.kernel_manager.start_restarter() # For spyder-ide/spyder#6235, IPython was changing the # setting of %colors on windows by assuming it was using a # dark background. This corrects it based on the scheme. self.set_color_scheme(sw.syntax_style, reset=reset) sw._append_html(_("<br>Restarting kernel...<br>"), before_prompt=True) sw.insert_horizontal_ruler() if self.fault_obj is not None: self.shellwidget.call_kernel().enable_faulthandler( self.fault_obj.filename) self._hide_loading_page() self.restart_thread = None self.sig_execution_state_changed.emit() def filter_fault(self, fault): """Get a fault from a previous session.""" thread_regex = ( r"(Current thread|Thread) " r"(0x[\da-f]+) \(most recent call first\):" r"(?:.|\r\n|\r|\n)+?(?=Current thread|Thread|\Z)") # Keep line for future improvments # files_regex = r"File \"([^\"]+)\", line (\d+) in (\S+)" main_re = "Main thread id:(?:\r\n|\r|\n)(0x[0-9a-f]+)" main_id = 0 for match in re.finditer(main_re, fault): main_id = int(match.group(1), base=16) system_re = ("System threads ids:" "(?:\r\n|\r|\n)(0x[0-9a-f]+(?: 0x[0-9a-f]+)+)") ignore_ids = [] start_idx = 0 for match in re.finditer(system_re, fault): ignore_ids = [int(i, base=16) for i in match.group(1).split()] start_idx = match.span()[1] text = "" for idx, match in enumerate(re.finditer(thread_regex, fault)): if idx == 0: text += fault[start_idx:match.span()[0]] thread_id = int(match.group(2), base=16) if thread_id != main_id: if thread_id in ignore_ids: continue if "wurlitzer.py" in match.group(0): # Wurlitzer threads are launched later continue text += "\n" + match.group(0) + "\n" else: try: pattern = (r".*(?:/IPython/core/interactiveshell\.py|" r"\\IPython\\core\\interactiveshell\.py).*") match_internal = next(re.finditer(pattern, match.group(0))) end_idx = match_internal.span()[0] except StopIteration: end_idx = None text += "\nMain thread:\n" + match.group(0)[:end_idx] + "\n" return text @Slot(str) def kernel_restarted_message(self, msg): """Show kernel restarted/died messages.""" if self.stderr_obj is not None: # If there are kernel creation errors, jupyter_client will # try to restart the kernel and qtconsole prints a # message about it. # So we read the kernel's stderr_file and display its # contents in the client instead of the usual message shown # by qtconsole. self.poll_std_file_change() else: self.shellwidget._append_html("<br>%s<hr><br>" % msg, before_prompt=False) @Slot() def enter_array_inline(self): """Enter and show the array builder on inline mode.""" self.shellwidget._control.enter_array_inline() @Slot() def enter_array_table(self): """Enter and show the array builder on table.""" self.shellwidget._control.enter_array_table() @Slot() def inspect_object(self): """Show how to inspect an object with our Help plugin""" self.shellwidget._control.inspect_current_object() @Slot() def clear_line(self): """Clear a console line""" self.shellwidget._keyboard_quit() @Slot() def clear_console(self): """Clear the whole console""" self.shellwidget.clear_console() @Slot() def reset_namespace(self): """Resets the namespace by removing all names defined by the user""" self.shellwidget.reset_namespace(warning=self.reset_warning, message=True) def update_history(self): self.history = self.shellwidget._history @Slot(object) def show_syspath(self, syspath): """Show sys.path contents.""" if syspath is not None: editor = CollectionsEditor(self) editor.setup(syspath, title="sys.path contents", readonly=True, icon=ima.icon('syspath')) self.dialog_manager.show(editor) else: return @Slot(object) def show_env(self, env): """Show environment variables.""" self.dialog_manager.show(RemoteEnvDialog(env, parent=self)) def show_time(self, end=False): """Text to show in time_label.""" if self.time_label is None: return elapsed_time = time.monotonic() - self.t0 # System time changed to past date, so reset start. if elapsed_time < 0: self.t0 = time.monotonic() elapsed_time = 0 if elapsed_time > 24 * 3600: # More than a day...! fmt = "%d %H:%M:%S" else: fmt = "%H:%M:%S" if end: color = QStylePalette.COLOR_TEXT_3 else: color = QStylePalette.COLOR_ACCENT_4 text = "<span style=\'color: %s\'><b>%s" \ "</b></span>" % (color, time.strftime(fmt, time.gmtime(elapsed_time))) if self.show_elapsed_time: self.time_label.setText(text) else: self.time_label.setText("") @Slot(bool) def set_show_elapsed_time(self, state): """Slot to show/hide elapsed time label.""" self.show_elapsed_time = state def set_info_page(self): """Set current info_page.""" if self.infowidget is not None and self.info_page is not None: self.infowidget.setHtml( self.info_page, QUrl.fromLocalFile(self.css_path) )
class FallbackActor(QObject): #: Signal emitted when the Thread is ready sig_fallback_ready = Signal() sig_set_tokens = Signal(int, dict) sig_mailbox = Signal(dict) def __init__(self, parent): QObject.__init__(self) self.stopped = False self.daemon = True self.mutex = QMutex() self.file_tokens = {} self.diff_patch = diff_match_patch() self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_mailbox.connect(self.handle_msg) def tokenize(self, text, language): """ Return all tokens in `text` and all keywords associated by Pygments to `language`. """ try: lexer = get_lexer_by_name(language) keywords = get_keywords(lexer) except Exception: keywords = [] keyword_set = set(keywords) keywords = [{ 'kind': CompletionItemKind.KEYWORD, 'insertText': keyword, 'sortText': u'zz{0}'.format(keyword[0].lower()), 'filterText': keyword, 'documentation': '' } for keyword in keywords] # logger.debug(keywords) # tokens = list(lexer.get_tokens(text)) # logger.debug(tokens) tokens = get_words(text, language) tokens = [{ 'kind': CompletionItemKind.TEXT, 'insertText': token, 'sortText': u'zz{0}'.format(token[0].lower()), 'filterText': token, 'documentation': '' } for token in tokens] for token in tokens: if token['insertText'] not in keyword_set: keywords.append(token) return keywords def stop(self): """Stop actor.""" with QMutexLocker(self.mutex): logger.debug("Fallback plugin stopping...") self.thread.quit() def start(self): """Start thread.""" self.thread.start() def started(self): """Thread started.""" logger.debug('Fallback plugin starting...') self.sig_fallback_ready.emit() @Slot(dict) def handle_msg(self, message): """Handle one message""" msg_type, _id, file, msg = [ message[k] for k in ('type', 'id', 'file', 'msg') ] logger.debug('Got request id {0}: {1} for file {2}'.format( _id, msg_type, file)) if msg_type == LSPRequestTypes.DOCUMENT_DID_OPEN: self.file_tokens[file] = { 'text': msg['text'], 'language': msg['language'] } elif msg_type == LSPRequestTypes.DOCUMENT_DID_CHANGE: if file not in self.file_tokens: self.file_tokens[file] = { 'text': '', 'language': msg['language'] } diff = msg['diff'] text = self.file_tokens[file] text, _ = self.diff_patch.patch_apply(diff, text['text']) self.file_tokens[file]['text'] = text elif msg_type == LSPRequestTypes.DOCUMENT_DID_CLOSE: self.file_tokens.pop(file, {}) elif msg_type == LSPRequestTypes.DOCUMENT_COMPLETION: tokens = [] if file in self.file_tokens: text_info = self.file_tokens[file] tokens = self.tokenize(text_info['text'], text_info['language']) tokens = {'params': tokens} self.sig_set_tokens.emit(_id, tokens)
class ClientWidget(QWidget, SaveHistoryMixin): """ Client widget for the IPython Console This widget is necessary to handle the interaction between the plugin and each shell widget. """ SEPARATOR = '{0}## ---({1})---'.format(os.linesep*2, time.ctime()) INITHISTORY = ['# -*- coding: utf-8 -*-', '# *** Spyder Python Console History Log ***',] append_to_history = Signal(str, str) def __init__(self, plugin, id_, history_filename, config_options, additional_options, interpreter_versions, connection_file=None, hostname=None, menu_actions=None, slave=False, external_kernel=False, given_name=None, options_button=None, show_elapsed_time=False, reset_warning=True, ask_before_restart=True, css_path=None): super(ClientWidget, self).__init__(plugin) SaveHistoryMixin.__init__(self, history_filename) # --- Init attrs self.plugin = plugin self.id_ = id_ self.connection_file = connection_file self.hostname = hostname self.menu_actions = menu_actions self.slave = slave self.external_kernel = external_kernel self.given_name = given_name self.show_elapsed_time = show_elapsed_time self.reset_warning = reset_warning self.ask_before_restart = ask_before_restart # --- Other attrs self.options_button = options_button self.stop_button = None self.reset_button = None self.stop_icon = ima.icon('stop') self.history = [] self.allow_rename = True self.stderr_dir = None self.is_error_shown = False if css_path is None: self.css_path = CSS_PATH else: self.css_path = css_path # --- Widgets self.shellwidget = ShellWidget(config=config_options, ipyclient=self, additional_options=additional_options, interpreter_versions=interpreter_versions, external_kernel=external_kernel, local_kernel=True) self.infowidget = plugin.infowidget self.blank_page = self._create_blank_page() self.loading_page = self._create_loading_page() # To keep a reference to the page to be displayed # in infowidget self.info_page = None self._show_loading_page() # Elapsed time self.time_label = None self.t0 = time.monotonic() self.timer = QTimer(self) self.show_time_action = create_action(self, _("Show elapsed time"), toggled=self.set_elapsed_time_visible) # --- Layout self.layout = QVBoxLayout() toolbar_buttons = self.get_toolbar_buttons() hlayout = QHBoxLayout() hlayout.addWidget(self.create_time_label()) hlayout.addStretch(0) for button in toolbar_buttons: hlayout.addWidget(button) self.layout.addLayout(hlayout) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.shellwidget) self.layout.addWidget(self.infowidget) self.setLayout(self.layout) # --- Exit function self.exit_callback = lambda: plugin.close_client(client=self) # --- Dialog manager self.dialog_manager = DialogManager() # Show timer self.update_time_label_visibility() #------ Public API -------------------------------------------------------- @property def kernel_id(self): """Get kernel id""" if self.connection_file is not None: json_file = osp.basename(self.connection_file) return json_file.split('.json')[0] @property def stderr_file(self): """Filename to save kernel stderr output.""" stderr_file = None if self.connection_file is not None: stderr_file = self.kernel_id + '.stderr' if self.stderr_dir is not None: stderr_file = osp.join(self.stderr_dir, stderr_file) else: try: stderr_file = osp.join(get_temp_dir(), stderr_file) except (IOError, OSError): stderr_file = None return stderr_file @property def stderr_handle(self): """Get handle to stderr_file.""" if self.stderr_file is not None: # Needed to prevent any error that could appear. # See spyder-ide/spyder#6267. try: handle = codecs.open(self.stderr_file, 'w', encoding='utf-8') except Exception: handle = None else: handle = None return handle def remove_stderr_file(self): """Remove stderr_file associated with the client.""" try: # Defer closing the stderr_handle until the client # is closed because jupyter_client needs it open # while it tries to restart the kernel self.stderr_handle.close() os.remove(self.stderr_file) except Exception: pass def configure_shellwidget(self, give_focus=True): """Configure shellwidget after kernel is started""" if give_focus: self.get_control().setFocus() # Set exit callback self.shellwidget.set_exit_callback() # To save history self.shellwidget.executing.connect(self.add_to_history) # For Mayavi to run correctly self.shellwidget.executing.connect( self.shellwidget.set_backend_for_mayavi) # To update history after execution self.shellwidget.executed.connect(self.update_history) # To update the Variable Explorer after execution self.shellwidget.executed.connect( self.shellwidget.refresh_namespacebrowser) # To enable the stop button when executing a process self.shellwidget.executing.connect(self.enable_stop_button) # To disable the stop button after execution stopped self.shellwidget.executed.connect(self.disable_stop_button) # To show kernel restarted/died messages self.shellwidget.sig_kernel_restarted.connect( self.kernel_restarted_message) # To correctly change Matplotlib backend interactively self.shellwidget.executing.connect( self.shellwidget.change_mpl_backend) # To show env and sys.path contents self.shellwidget.sig_show_syspath.connect(self.show_syspath) self.shellwidget.sig_show_env.connect(self.show_env) # To sync with working directory toolbar self.shellwidget.executed.connect(self.shellwidget.update_cwd) # To apply style self.set_color_scheme(self.shellwidget.syntax_style, reset=False) # To hide the loading page self.shellwidget.sig_prompt_ready.connect(self._hide_loading_page) # Show possible errors when setting Matplotlib backend self.shellwidget.sig_prompt_ready.connect( self._show_mpl_backend_errors) def enable_stop_button(self): self.stop_button.setEnabled(True) def disable_stop_button(self): # This avoids disabling automatically the button when # re-running files on dedicated consoles. # See spyder-ide/spyder#5958. if not self.shellwidget._executing: self.stop_button.setDisabled(True) @Slot() def stop_button_click_handler(self): """Method to handle what to do when the stop button is pressed""" self.stop_button.setDisabled(True) # Interrupt computations or stop debugging if not self.shellwidget.is_waiting_pdb_input(): self.interrupt_kernel() else: self.shellwidget.pdb_execute('exit', hidden=True) def show_kernel_error(self, error): """Show kernel initialization errors in infowidget.""" # Replace end of line chars with <br> eol = sourcecode.get_eol_chars(error) if eol: error = error.replace(eol, '<br>') # Don't break lines in hyphens # From https://stackoverflow.com/q/7691569/438386 error = error.replace('-', '‑') # Create error page message = _("An error ocurred while starting the kernel") kernel_error_template = Template(KERNEL_ERROR) self.info_page = kernel_error_template.substitute( css_path=self.css_path, message=message, error=error) # Show error self.set_info_page() self.shellwidget.hide() self.infowidget.show() # Tell the client we're in error mode self.is_error_shown = True def get_name(self): """Return client name""" if self.given_name is None: # Name according to host if self.hostname is None: name = _("Console") else: name = self.hostname # Adding id to name client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = name + u' ' + client_id elif self.given_name in ["Pylab", "SymPy", "Cython"]: client_id = self.id_['int_id'] + u'/' + self.id_['str_id'] name = self.given_name + u' ' + client_id else: name = self.given_name + u'/' + self.id_['str_id'] return name def get_control(self): """Return the text widget (or similar) to give focus to""" # page_control is the widget used for paging page_control = self.shellwidget._page_control if page_control and page_control.isVisible(): return page_control else: return self.shellwidget._control def get_kernel(self): """Get kernel associated with this client""" return self.shellwidget.kernel_manager def get_options_menu(self): """Return options menu""" env_action = create_action( self, _("Show environment variables"), icon=ima.icon('environ'), triggered=self.shellwidget.request_env ) syspath_action = create_action( self, _("Show sys.path contents"), icon=ima.icon('syspath'), triggered=self.shellwidget.request_syspath ) self.show_time_action.setChecked(self.show_elapsed_time) additional_actions = [MENU_SEPARATOR, env_action, syspath_action, self.show_time_action] if self.menu_actions is not None: console_menu = self.menu_actions + additional_actions return console_menu else: return additional_actions def get_toolbar_buttons(self): """Return toolbar buttons list.""" buttons = [] # Code to add the stop button if self.stop_button is None: self.stop_button = create_toolbutton( self, text=_("Stop"), icon=self.stop_icon, tip=_("Stop the current command")) self.disable_stop_button() # set click event handler self.stop_button.clicked.connect(self.stop_button_click_handler) if is_dark_interface(): self.stop_button.setStyleSheet("QToolButton{padding: 3px;}") if self.stop_button is not None: buttons.append(self.stop_button) # Reset namespace button if self.reset_button is None: self.reset_button = create_toolbutton( self, text=_("Remove"), icon=ima.icon('editdelete'), tip=_("Remove all variables"), triggered=self.reset_namespace) if is_dark_interface(): self.reset_button.setStyleSheet("QToolButton{padding: 3px;}") if self.reset_button is not None: buttons.append(self.reset_button) if self.options_button is None: options = self.get_options_menu() if options: self.options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) add_actions(menu, options) self.options_button.setMenu(menu) if self.options_button is not None: buttons.append(self.options_button) return buttons def add_actions_to_context_menu(self, menu): """Add actions to IPython widget context menu""" inspect_action = create_action( self, _("Inspect current object"), QKeySequence(CONF.get_shortcut('console', 'inspect current object')), icon=ima.icon('MessageBoxInformation'), triggered=self.inspect_object) clear_line_action = create_action( self, _("Clear line or block"), QKeySequence(CONF.get_shortcut('console', 'clear line')), triggered=self.clear_line) reset_namespace_action = create_action( self, _("Remove all variables"), QKeySequence(CONF.get_shortcut('ipython_console', 'reset namespace')), icon=ima.icon('editdelete'), triggered=self.reset_namespace) clear_console_action = create_action( self, _("Clear console"), QKeySequence(CONF.get_shortcut('console', 'clear shell')), triggered=self.clear_console) quit_action = create_action( self, _("&Quit"), icon=ima.icon('exit'), triggered=self.exit_callback) add_actions(menu, (None, inspect_action, clear_line_action, clear_console_action, reset_namespace_action, None, quit_action)) return menu def set_font(self, font): """Set IPython widget's font""" self.shellwidget._control.setFont(font) self.shellwidget.font = font def set_color_scheme(self, color_scheme, reset=True): """Set IPython color scheme.""" # Needed to handle not initialized kernel_client # See spyder-ide/spyder#6996. try: self.shellwidget.set_color_scheme(color_scheme, reset) except AttributeError: pass def shutdown(self): """Shutdown kernel""" if self.get_kernel() is not None and not self.slave: self.shellwidget.spyder_kernel_comm.close() self.shellwidget.spyder_kernel_comm.shutdown_comm_channel() self.shellwidget._pdb_history_file.save_thread.stop() self.shellwidget.kernel_manager.stop_restarter() self.shutdown_thread = QThread() self.shutdown_thread.run = self.finalize_shutdown self.shutdown_thread.finished.connect(self.stop_kernel_channels) self.shutdown_thread.start() def finalize_shutdown(self): """Finalise the shutdown.""" if self.get_kernel() is not None and not self.slave: self.shellwidget.kernel_manager.shutdown_kernel() def stop_kernel_channels(self): """Stop kernel channels.""" if self.shellwidget.kernel_client is not None: self.shellwidget.kernel_client.stop_channels() def interrupt_kernel(self): """Interrupt the associanted Spyder kernel if it's running""" # Needed to prevent a crash when a kernel is not running. # See spyder-ide/spyder#6299. try: self.shellwidget.request_interrupt_kernel() except RuntimeError: pass @Slot() def restart_kernel(self): """ Restart the associated kernel. Took this code from the qtconsole project Licensed under the BSD license """ sw = self.shellwidget if not running_under_pytest() and self.ask_before_restart: message = _('Are you sure you want to restart the kernel?') buttons = QMessageBox.Yes | QMessageBox.No result = QMessageBox.question(self, _('Restart kernel?'), message, buttons) else: result = None if (result == QMessageBox.Yes or running_under_pytest() or not self.ask_before_restart): if sw.kernel_manager: if self.infowidget.isVisible(): self.infowidget.hide() sw.show() try: # Close comm sw.spyder_kernel_comm.close() sw.kernel_manager.restart_kernel( stderr=self.stderr_handle) # Reopen comm sw.spyder_kernel_comm.open_comm(sw.kernel_client) except RuntimeError as e: sw._append_plain_text( _('Error restarting kernel: %s\n') % e, before_prompt=True ) else: # For spyder-ide/spyder#6235, IPython was changing the # setting of %colors on windows by assuming it was using a # dark background. This corrects it based on the scheme. self.set_color_scheme(sw.syntax_style) sw._append_html(_("<br>Restarting kernel...\n<hr><br>"), before_prompt=False) else: sw._append_plain_text( _('Cannot restart a kernel not started by Spyder\n'), before_prompt=True ) @Slot(str) def kernel_restarted_message(self, msg): """Show kernel restarted/died messages.""" if not self.is_error_shown: # If there are kernel creation errors, jupyter_client will # try to restart the kernel and qtconsole prints a # message about it. # So we read the kernel's stderr_file and display its # contents in the client instead of the usual message shown # by qtconsole. try: stderr = self._read_stderr() except Exception: stderr = None if stderr: self.show_kernel_error('<tt>%s</tt>' % stderr) else: self.shellwidget._append_html("<br>%s<hr><br>" % msg, before_prompt=False) @Slot() def inspect_object(self): """Show how to inspect an object with our Help plugin""" self.shellwidget._control.inspect_current_object() @Slot() def clear_line(self): """Clear a console line""" self.shellwidget._keyboard_quit() @Slot() def clear_console(self): """Clear the whole console""" self.shellwidget.clear_console() @Slot() def reset_namespace(self): """Resets the namespace by removing all names defined by the user""" self.shellwidget.reset_namespace(warning=self.reset_warning, message=True) def update_history(self): self.history = self.shellwidget._history @Slot(object) def show_syspath(self, syspath): """Show sys.path contents.""" if syspath is not None: editor = CollectionsEditor(self) editor.setup(syspath, title="sys.path contents", readonly=True, icon=ima.icon('syspath')) self.dialog_manager.show(editor) else: return @Slot(object) def show_env(self, env): """Show environment variables.""" self.dialog_manager.show(RemoteEnvDialog(env, parent=self)) def create_time_label(self): """Create elapsed time label widget (if necessary) and return it""" if self.time_label is None: self.time_label = QLabel() return self.time_label def show_time(self, end=False): """Text to show in time_label.""" if self.time_label is None: return elapsed_time = time.monotonic() - self.t0 # System time changed to past date, so reset start. if elapsed_time < 0: self.t0 = time.monotonic() elapsed_time = 0 if elapsed_time > 24 * 3600: # More than a day...! fmt = "%d %H:%M:%S" else: fmt = "%H:%M:%S" if end: color = "#AAAAAA" else: color = "#AA6655" text = "<span style=\'color: %s\'><b>%s" \ "</b></span>" % (color, time.strftime(fmt, time.gmtime(elapsed_time))) self.time_label.setText(text) def update_time_label_visibility(self): """Update elapsed time visibility.""" self.time_label.setVisible(self.show_elapsed_time) @Slot(bool) def set_elapsed_time_visible(self, state): """Slot to show/hide elapsed time label.""" self.show_elapsed_time = state if self.time_label is not None: self.time_label.setVisible(state) def set_info_page(self): """Set current info_page.""" if self.info_page is not None: self.infowidget.setHtml( self.info_page, QUrl.fromLocalFile(self.css_path) ) #------ Private API ------------------------------------------------------- def _create_loading_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(LOADING) loading_img = get_image_path('loading_sprites.png') if os.name == 'nt': loading_img = loading_img.replace('\\', '/') message = _("Connecting to kernel...") page = loading_template.substitute(css_path=self.css_path, loading_img=loading_img, message=message) return page def _create_blank_page(self): """Create html page to show while the kernel is starting""" loading_template = Template(BLANK) page = loading_template.substitute(css_path=self.css_path) return page def _show_loading_page(self): """Show animation while the kernel is loading.""" self.shellwidget.hide() self.infowidget.show() self.info_page = self.loading_page self.set_info_page() def _hide_loading_page(self): """Hide animation shown while the kernel is loading.""" self.infowidget.hide() self.shellwidget.show() self.info_page = self.blank_page self.set_info_page() self.shellwidget.sig_prompt_ready.disconnect(self._hide_loading_page) def _read_stderr(self): """Read the stderr file of the kernel.""" # We need to read stderr_file as bytes to be able to # detect its encoding with chardet f = open(self.stderr_file, 'rb') try: stderr_text = f.read() # This is needed to avoid showing an empty error message # when the kernel takes too much time to start. # See spyder-ide/spyder#8581. if not stderr_text: return '' # This is needed since the stderr file could be encoded # in something different to utf-8. # See spyder-ide/spyder#4191. encoding = get_coding(stderr_text) stderr_text = to_text_string(stderr_text, encoding) return stderr_text finally: f.close() def _show_mpl_backend_errors(self): """ Show possible errors when setting the selected Matplotlib backend. """ if not self.external_kernel: self.shellwidget.call_kernel().show_mpl_backend_errors() self.shellwidget.sig_prompt_ready.disconnect( self._show_mpl_backend_errors)