def _main(args): # import pydm # app = QApplication([]) # app = pydm.PyDMApplication() app = QApplication([]) signal.signal(signal.SIGINT, signal.SIG_DFL) from xicam.gui.windows import splash from xicam.core import msg if args.verbose in sys.argv: QErrorMessage.qtHandler() # start splash in subprocess splash_proc = QProcess() # splash_proc.started.connect(lambda: print('started splash')) # splash_proc.finished.connect(lambda: print('finished splashing')) log_file = msg.file_handler.baseFilename initial_length = os.path.getsize(log_file) splash_proc.start(sys.executable, [splash.__file__, log_file, str(initial_length)]) from xicam.gui.windows.mainwindow import XicamMainWindow mainwindow = XicamMainWindow() while splash_proc.state() != QProcess.NotRunning: app.processEvents() # splash_proc.waitForFinished() mainwindow.show() # splash = splash.XicamSplashScreen(args=args) return sys.exit(app.exec_())
def update_environment(self, filename, name=None, prefix=None, emit=False): """ Set environment at 'prefix' or 'name' to match 'filename' spec as yaml. """ if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if self._is_not_running: if name: temporal_envname = name if prefix: temporal_envname = 'tempenv' + int(random.random()*10000000) envs_dir = self.info()['envs_dirs'][0] os.symlink(prefix, os.sep.join([envs_dir, temporal_envname])) cmd = self._abspath(True) cmd.extend(['env', 'update', '--name', temporal_envname, '--file', os.path.abspath(filename)]) qprocess = QProcess() qprocess.start(cmd[0], cmd[1:]) qprocess.waitForFinished() if prefix: os.unlink(os.sep.join([envs_dir, 'tempenv']))
def environment_exists(self, name=None, prefix=None, abspath=True, emit=False): """ Check if an environment exists by 'name' or by 'prefix'. If query is by 'name' only the default conda environments directory is searched. """ if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(['list', '--json']) if name: cmd_list.extend(['--name', name]) else: cmd_list.extend(['--prefix', prefix]) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) if emit: self.sig_finished.emit("info", unicode(info), "") return 'error' not in info
def pip_list(self, name=None, prefix=None, abspath=True, emit=False): """ Get list of pip installed packages. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' " "required.") if self._is_not_running: cmd_list = self._abspath(abspath) if name: cmd_list.extend(['list', '--name', name]) if prefix: cmd_list.extend(['list', '--prefix', prefix]) qprocess = QProcess() qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) result = [] lines = output.split('\n') for line in lines: if '<pip>' in line: temp = line.split()[:-1] + ['pip'] result.append('-'.join(temp)) if emit: self.sig_finished.emit("pip", str(result), "") return result
class Installer: def __init__(self, output_widget: QTextEdit = None): from ...plugins import plugin_manager # create install process self._output_widget = None self.process = QProcess() self.process.setProgram(sys.executable) self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.readyReadStandardOutput.connect(self._on_stdout_ready) # setup process path env = QProcessEnvironment() combined_paths = os.pathsep.join([ user_site_packages(), env.systemEnvironment().value("PYTHONPATH") ]) env.insert("PYTHONPATH", combined_paths) # use path of parent process env.insert("PATH", QProcessEnvironment.systemEnvironment().value("PATH")) self.process.setProcessEnvironment(env) self.process.finished.connect(lambda: plugin_manager.discover()) self.process.finished.connect(lambda: plugin_manager.prune()) self.set_output_widget(output_widget) def set_output_widget(self, output_widget: QTextEdit): if output_widget: self._output_widget = output_widget self.process.setParent(output_widget) def _on_stdout_ready(self): if self._output_widget: text = self.process.readAllStandardOutput().data().decode() self._output_widget.append(text) def install(self, pkg_list: Sequence[str]): cmd = ['-m', 'pip', 'install', '--upgrade'] if running_as_bundled_app() and sys.platform.startswith('linux'): cmd += [ '--no-warn-script-location', '--prefix', user_plugin_dir(), ] self.process.setArguments(cmd + list(pkg_list)) if self._output_widget: self._output_widget.clear() self.process.start() def uninstall(self, pkg_list: Sequence[str]): args = ['-m', 'pip', 'uninstall', '-y'] self.process.setArguments(args + list(pkg_list)) if self._output_widget: self._output_widget.clear() self.process.start() for pkg in pkg_list: plugin_manager.unregister(pkg)
def start_server(self, filename, interpreter): """ Start a notebook server asynchronously. Start a server which can render the given notebook and return immediately. Assume the server uses the given interpreter. The manager will check periodically whether the server is accepting requests and emit `sig_server_started` or `sig_server_timed_out` when appropriate. Parameters ---------- filename : str File name of notebook to be rendered by the server. interpreter : str File name of Python interpreter to be used. """ home_dir = get_home_dir() if filename.startswith(home_dir): nbdir = home_dir else: nbdir = osp.dirname(filename) logger.debug('Starting new notebook server for %s', nbdir) process = QProcess(None) serverscript = osp.join(osp.dirname(__file__), '../server/main.py') serverscript = osp.normpath(serverscript) arguments = [ serverscript, '--no-browser', '--notebook-dir={}'.format(nbdir), '--NotebookApp.password='******'--KernelSpecManager.kernel_spec_class={}'.format(KERNELSPEC) ] if self.dark_theme: arguments.append('--dark') logger.debug('Arguments: %s', repr(arguments)) if DEV: env = QProcessEnvironment.systemEnvironment() env.insert('PYTHONPATH', osp.dirname(get_module_path('spyder'))) process.setProcessEnvironment(env) server_process = ServerProcess(process, notebook_dir=nbdir, interpreter=interpreter) process.setProcessChannelMode(QProcess.MergedChannels) process.readyReadStandardOutput.connect( lambda: self.read_server_output(server_process)) process.errorOccurred.connect( lambda error: self.handle_error(server_process, error)) process.finished.connect(lambda code, status: self.handle_finished( server_process, code, status)) process.start(sys.executable, arguments) self.servers.append(server_process) self._check_server_started(server_process)
def info(self, abspath=True): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ # return self._call_and_parse(['info', '--json'], abspath=abspath) qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(['info', '--json']) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) return info
def info(self, abspath=True): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ if self._is_not_running(): qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(["info", "--json"]) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) self.sig_finished.emit("info", str(info), "") return info
def _main(args, exec=True): global mainwindow, splash_proc, show_check_timer # import pydm # app = QApplication([]) # app = pydm.PyDMApplication() app = QApplication.instance() or QApplication([]) signal.signal(signal.SIGINT, signal.SIG_DFL) from xicam.gui.windows import splash from xicam.core import msg if getattr(args, 'verbose', False): QErrorMessage.qtHandler() # start splash in subprocess splash_proc = QProcess() # splash_proc.started.connect(lambda: print('started splash')) # splash_proc.finished.connect(lambda: print('finished splashing')) log_file = msg.file_handler.baseFilename initial_length = os.path.getsize(log_file) splash_proc.start(sys.executable, [splash.__file__, log_file, str(initial_length)]) show_check_timer = QTimer() show_check_timer.timeout.connect(check_show_mainwindow) show_check_timer.start(100) from xicam.gui.windows.mainwindow import XicamMainWindow mainwindow = XicamMainWindow() if exec: return app.exec_() else: return mainwindow
class ProcessWorker(QObject): """Process worker based on a QProcess for non blocking UI.""" sig_started = Signal(object) sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, environ=None): """ Process worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. environ : dict Process environment, """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._fired = False self._communicate_first = False self._partial_stdout = None self._started = False self._timer = QTimer() self._process = QProcess() self._set_environment(environ) self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _get_encoding(self): """Return the encoding/codepage to use.""" enco = 'utf-8' # Currently only cp1252 is allowed? if WIN: import ctypes codepage = to_text_string(ctypes.cdll.kernel32.GetACP()) # import locale # locale.getpreferredencoding() # Differences? enco = 'cp' + codepage return enco def _set_environment(self, environ): """Set the environment on the QProcess.""" if environ: q_environ = self._process.processEnvironment() for k, v in environ.items(): q_environ.insert(k, v) self._process.setProcessEnvironment(q_environ) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, self._get_encoding()) if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() enco = self._get_encoding() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, enco) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, enco) result = [stdout.encode(enco), stderr.encode(enco)] if PY2: stderr = stderr.decode() result[-1] = '' self._result = result if not self._fired: self.sig_finished.emit(self, result[0], result[-1]) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def _start(self): """Start process.""" if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() def terminate(self): """Terminate running processes.""" if self._process.state() == QProcess.Running: try: self._process.terminate() except Exception: pass self._fired = True def start(self): """Start worker.""" if not self._started: self.sig_started.emit(self) self._started = True
class QJackCaptureMainWindow(QDialog): sample_formats = { "32-bit float": "FLOAT", "8-bit integer": "8", "16-bit integer": "16", "24-bit integer": "24", "32-bit integer": "32", } def __init__(self, parent, jack_client, jack_name=PROGRAM): QDialog.__init__(self, parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) self.fFreewheel = False self.fLastTime = -1 self.fMaxTime = 180 self.fTimer = QTimer(self) self.fProcess = QProcess(self) self.fJackClient = jack_client self.fJackName = jack_name self.fBufferSize = self.fJackClient.get_buffer_size() self.fSampleRate = self.fJackClient.get_sample_rate() # Selected ports used as recording sources self.rec_sources = set() self.createUi() self.loadSettings() self.populatePortLists(init=True) # listen to changes to JACK ports self._refresh_timer = None self.fJackClient.ports_changed.connect(self.slot_refreshPortsLists) @Slot() def slot_refreshPortsLists(self, delay=200): if not self._refresh_timer or not self._refresh_timer.isActive(): log.debug("Scheduling port lists refresh in %i ms...", delay) self._refresh_timer = QTimer() self._refresh_timer.setSingleShot(True) self._refresh_timer.timeout.connect(self.populatePortLists) self._refresh_timer.start(delay) def populateFileFormats(self): # Get list of supported file formats self.fProcess.start(gJackCapturePath, ["-pf"]) self.fProcess.waitForFinished() formats = [] for fmt in str(self.fProcess.readAllStandardOutput(), encoding="utf-8").split(): fmt = fmt.strip() if fmt: formats.append(fmt) # Put all file formats in combo-box, select 'wav' option self.ui.cb_format.clear() for i, fmt in enumerate(sorted(formats)): self.ui.cb_format.addItem(fmt) if fmt == "wav": self.ui.cb_format.setCurrentIndex(i) def populateSampleFormats(self): # Put all sample formats in combo-box, select 'FLOAT' option self.ui.cb_depth.clear() for i, (label, fmt) in enumerate(self.sample_formats.items()): self.ui.cb_depth.addItem(label, fmt) if fmt == "FLOAT": self.ui.cb_depth.setCurrentIndex(i) def populatePortLists(self, init=False): log.debug("Populating port lists (init=%s)...", init) if init: self.outputs_model = QStandardItemModel(0, 1, self) self.inputs_model = QStandardItemModel(0, 1, self) else: self.outputs_model.clear() self.inputs_model.clear() output_ports = list(self.fJackClient.get_output_ports()) self.populatePortList(self.outputs_model, self.ui.tree_outputs, output_ports) input_ports = list(self.fJackClient.get_input_ports()) self.populatePortList(self.inputs_model, self.ui.tree_inputs, input_ports) # Remove ports, which are no longer present, from recording sources all_ports = set((p.client, p.name) for p in output_ports) all_ports |= set((p.client, p.name) for p in input_ports) self.rec_sources.intersection_update(all_ports) self.slot_toggleRecordingSource() def makePortTooltip(self, port): s = [] if port.pretty_name: s.append(f"<b>Pretty name:</b> <em>{port.pretty_name}</em><br>") s.append(f"<b>Port:</b> <tt>{port.client}:{port.name}</tt><br>") for i, alias in enumerate(port.aliases, 1): s.append(f"<b>Alias {i}:</b> <tt>{alias}</tt><br>") s.append(f"<b>UUID:</b> <tt>{port.uuid}</tt>") return "<small>{}</small>".format("\n".join(s)) def populatePortList(self, model, tv, ports): tv.setModel(model) root = model.invisibleRootItem() portsdict = {} for port in ports: if port.client not in portsdict: portsdict[port.client] = [] portsdict[port.client].append(port) for client in humansorted(portsdict): clientitem = QStandardItem(client) for port in humansorted(portsdict[client], key=attrgetter("group", "order", "name")): portspec = (port.client, port.name) if port.pretty_name: label = "%s (%s)" % (port.pretty_name, port.name) else: label = port.name portitem = QStandardItem(label) portitem.setData(portspec) portitem.setCheckable(True) portitem.setUserTristate(False) # Check box toggling is done in the treeview clicked handler "on_port_clicked" portitem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) portitem.setToolTip(self.makePortTooltip(port)) if portspec in self.rec_sources: portitem.setCheckState(2) clientitem.appendRow(portitem) root.appendRow(clientitem) tv.expandAll() def createUi(self): # ------------------------------------------------------------- # Set-up GUI stuff for i in range(self.ui.cb_buffer_size.count()): if int(self.ui.cb_buffer_size.itemText(i)) == self.fBufferSize: self.ui.cb_buffer_size.setCurrentIndex(i) break else: self.ui.cb_buffer_size.addItem(str(self.fBufferSize)) self.ui.cb_buffer_size.setCurrentIndex( self.ui.cb_buffer_size.count() - 1) self.populateFileFormats() self.populateSampleFormats() self.ui.rb_stereo.setChecked(True) self.ui.te_end.setTime(QTime(0, 3, 0)) self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(1) self.ui.progressBar.setValue(0) self.ui.b_render.setIcon(get_icon("media-record")) self.ui.b_stop.setIcon(get_icon("media-playback-stop")) self.ui.b_close.setIcon(get_icon("window-close")) self.ui.b_open.setIcon(get_icon("document-open")) self.ui.b_stop.setVisible(False) self.ui.le_folder.setText(expanduser("~")) # ------------------------------------------------------------- # Set-up connections self.ui.b_render.clicked.connect(self.slot_renderStart) self.ui.b_stop.clicked.connect(self.slot_renderStop) self.ui.b_open.clicked.connect(self.slot_getAndSetPath) self.ui.b_now_start.clicked.connect(self.slot_setStartNow) self.ui.b_now_end.clicked.connect(self.slot_setEndNow) self.ui.te_start.timeChanged.connect(self.slot_updateStartTime) self.ui.te_end.timeChanged.connect(self.slot_updateEndTime) self.ui.group_time.clicked.connect(self.slot_transportChecked) self.ui.rb_source_default.toggled.connect( self.slot_toggleRecordingSource) self.ui.rb_source_manual.toggled.connect( self.slot_toggleRecordingSource) self.ui.rb_source_selected.toggled.connect( self.slot_toggleRecordingSource) self.fTimer.timeout.connect(self.slot_updateProgressbar) for tv in (self.ui.tree_outputs, self.ui.tree_inputs): menu = QMenu() menu.addAction(get_icon("expand-all"), self.tr("E&xpand all"), tv.expandAll) menu.addAction(get_icon("collapse-all"), self.tr("&Collapse all"), tv.collapseAll) menu.addSeparator() menu.addAction( get_icon("list-select-all"), self.tr("&Select all in group"), partial(self.on_select_port_group, tv), ) menu.addAction( get_icon("list-select-none"), self.tr("&Unselect all in group"), partial(self.on_select_port_group, tv, enable=False), ) menu.addSeparator() if tv is self.ui.tree_outputs: menu.addAction( get_icon("select-none"), self.tr("Unselect all &outputs"), partial(self.on_clear_all_ports, tv), ) else: menu.addAction( get_icon("select-none"), self.tr("Unselect all &inputs"), partial(self.on_clear_all_ports, tv), ) tv.setContextMenuPolicy(Qt.CustomContextMenu) tv.customContextMenuRequested.connect( partial(self.on_port_menu, treeview=tv, menu=menu)) tv.clicked.connect(self.on_port_clicked) def enable_port(self, item, enable=True): item.setCheckState(2 if enable else 0) port = item.data() if enable: self.rec_sources.add(port) else: self.rec_sources.discard(port) def on_port_menu(self, pos, treeview=None, menu=None): if treeview and menu: menu.popup(treeview.viewport().mapToGlobal(pos)) def foreach_item(self, model, parent, func, leaves_only=True): for row in range(model.rowCount(parent)): index = model.index(row, 0, parent) is_leaf = not model.hasChildren(index) if is_leaf or not leaves_only: func(model.itemFromIndex(index)) if not is_leaf: self.foreach_item(model, index, func) def on_clear_all_ports(self, treeview): self.foreach_item(treeview.model(), QModelIndex(), partial(self.enable_port, enable=False)) self.checkRecordEnable() def on_port_clicked(self, index): model = index.model() item = model.itemFromIndex(index) if not model.hasChildren(index): self.enable_port(item, not item.checkState()) self.checkRecordEnable() def on_select_port_group(self, treeview, enable=True): index = treeview.currentIndex() model = index.model() if not model.hasChildren(index): index = index.parent() self.foreach_item(model, index, partial(self.enable_port, enable=enable)) self.checkRecordEnable() @Slot() def slot_renderStart(self): if not exists(self.ui.le_folder.text()): QMessageBox.warning( self, self.tr("Warning"), self. tr("The selected directory does not exist. Please choose a valid one." ), ) return timeStart = self.ui.te_start.time() timeEnd = self.ui.te_end.time() minTime = (timeStart.hour() * 3600) + (timeStart.minute() * 60) + (timeStart.second()) maxTime = (timeEnd.hour() * 3600) + (timeEnd.minute() * 60) + (timeEnd.second()) newBufferSize = int(self.ui.cb_buffer_size.currentText()) useTransport = self.ui.group_time.isChecked() self.fFreewheel = self.ui.rb_freewheel.isChecked() self.fLastTime = -1 self.fMaxTime = maxTime if self.fFreewheel: self.fTimer.setInterval(100) else: self.fTimer.setInterval(500) self.ui.group_render.setEnabled(False) self.ui.group_time.setEnabled(False) self.ui.group_encoding.setEnabled(False) self.ui.b_render.setVisible(False) self.ui.b_stop.setVisible(True) self.ui.b_close.setEnabled(False) if useTransport: self.ui.progressBar.setFormat("%p%") self.ui.progressBar.setMinimum(minTime) self.ui.progressBar.setMaximum(maxTime) self.ui.progressBar.setValue(minTime) else: self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(0) self.ui.progressBar.setValue(0) self.ui.progressBar.update() arguments = [] # JACK client name arguments.append("-jn") arguments.append(self.fJackName) # Filename prefix arguments.append("-fp") arguments.append(self.ui.le_prefix.text()) # File format arguments.append("-f") arguments.append(self.ui.cb_format.currentText()) # Sanple format (bit depth, int/float) arguments.append("-b") arguments.append(self.ui.cb_depth.currentData()) # Channels arguments.append("-c") if self.ui.rb_mono.isChecked(): arguments.append("1") elif self.ui.rb_stereo.isChecked(): arguments.append("2") else: arguments.append(str(self.ui.sb_channels.value())) # Recording sources if self.ui.rb_source_manual.isChecked(): arguments.append("-mc") elif self.ui.rb_source_selected.isChecked(): for client, port in self.rec_sources: arguments.append("-p") arguments.append("{}:{}".format(client, port)) # Controlled only by freewheel if self.fFreewheel: arguments.append("-jf") # Controlled by transport elif useTransport: arguments.append("-jt") # Silent mode arguments.append("--daemon") # Extra arguments extra_args = self.ui.le_extra_args.text().strip() if extra_args: arg_list = shlex.split(extra_args) arguments.extend(arg_list) # Change current directory os.chdir(self.ui.le_folder.text()) if newBufferSize != self.fJackClient.get_buffer_size(): log.info("Buffer size changed before render.") self.fJackClient.set_buffer_size(newBufferSize) if useTransport: if self.fJackClient.transport_running(): # rolling or starting self.fJackClient.transport_stop() self.fJackClient.transport_locate(minTime * self.fSampleRate) log.debug("jack_capture command line args: %r", arguments) self.fProcess.start(gJackCapturePath, arguments) status = self.fProcess.waitForStarted() if not status: self.fProcess.close() log.error("Could not start jack_capture.") return if self.fFreewheel: log.info("Rendering in freewheel mode.") sleep(1) self.fJackClient.set_freewheel(True) if useTransport: log.info("Rendering using JACK transport.") self.fTimer.start() self.fJackClient.transport_start() @Slot() def slot_renderStop(self): useTransport = self.ui.group_time.isChecked() if useTransport: self.fJackClient.transport_stop() if self.fFreewheel: self.fJackClient.set_freewheel(False) sleep(1) self.fProcess.terminate() # self.fProcess.waitForFinished(5000) if useTransport: self.fTimer.stop() self.ui.group_render.setEnabled(True) self.ui.group_time.setEnabled(True) self.ui.group_encoding.setEnabled(True) self.ui.b_render.setVisible(True) self.ui.b_stop.setVisible(False) self.ui.b_close.setEnabled(True) self.ui.progressBar.setFormat("") self.ui.progressBar.setMinimum(0) self.ui.progressBar.setMaximum(1) self.ui.progressBar.setValue(0) self.ui.progressBar.update() # Restore buffer size newBufferSize = self.fJackClient.get_buffer_size() if newBufferSize != self.fBufferSize: self.fJackClient.set_buffer_size(newBufferSize) @Slot() def slot_getAndSetPath(self): new_path = QFileDialog.getExistingDirectory(self, self.tr("Set Path"), self.ui.le_folder.text(), QFileDialog.ShowDirsOnly) if new_path: self.ui.le_folder.setText(new_path) @Slot() def slot_setStartNow(self): time = self.fJackClient.transport_frame() // self.fSampleRate secs = time % 60 mins = int(time / 60) % 60 hrs = int(time / 3600) % 60 self.ui.te_start.setTime(QTime(hrs, mins, secs)) @Slot() def slot_setEndNow(self): time = self.fJackClient.transport_frame() // self.fSampleRate secs = time % 60 mins = int(time / 60) % 60 hrs = int(time / 3600) % 60 self.ui.te_end.setTime(QTime(hrs, mins, secs)) @Slot(QTime) def slot_updateStartTime(self, time): if time >= self.ui.te_end.time(): self.ui.te_end.setTime(time) renderEnabled = False else: renderEnabled = True if self.ui.group_time.isChecked(): self.ui.b_render.setEnabled(renderEnabled) @Slot(QTime) def slot_updateEndTime(self, time): if time <= self.ui.te_start.time(): self.ui.te_start.setTime(time) renderEnabled = False else: renderEnabled = True if self.ui.group_time.isChecked(): self.ui.b_render.setEnabled(renderEnabled) @Slot(bool) def slot_toggleRecordingSource(self, dummy=None): enabled = self.ui.rb_source_selected.isChecked() self.ui.tree_outputs.setEnabled(enabled) self.ui.tree_inputs.setEnabled(enabled) self.checkRecordEnable() @Slot(bool) def slot_transportChecked(self, dummy=None): self.checkRecordEnable() @Slot() def slot_updateProgressbar(self): time = self.fJackClient.transport_frame() / self.fSampleRate self.ui.progressBar.setValue(time) if time > self.fMaxTime or (self.fLastTime > time and not self.fFreewheel): self.slot_renderStop() self.fLastTime = time def checkRecordEnable(self): enable = True if self.ui.rb_source_selected.isChecked() and not self.rec_sources: enable = False if self.ui.group_time.isChecked( ) and self.ui.te_end.time() <= self.ui.te_start.time(): enable = False self.ui.b_render.setEnabled(enable) log.debug("Recording sources: %s", ", ".join( ("%s:%s" % (c, p) for c, p in self.rec_sources))) def saveSettings(self): settings = QSettings(ORGANIZATION, PROGRAM) if self.ui.rb_mono.isChecked(): channels = 1 elif self.ui.rb_stereo.isChecked(): channels = 2 else: channels = self.ui.sb_channels.value() settings.setValue("Geometry", self.saveGeometry()) settings.setValue("OutputFolder", self.ui.le_folder.text()) settings.setValue("FilenamePrefix", self.ui.le_prefix.text()) settings.setValue("EncodingFormat", self.ui.cb_format.currentText()) settings.setValue("EncodingDepth", self.ui.cb_depth.currentData()) settings.setValue("EncodingChannels", channels) settings.setValue("UseTransport", self.ui.group_time.isChecked()) settings.setValue("StartTime", self.ui.te_start.time()) settings.setValue("EndTime", self.ui.te_end.time()) settings.setValue("ExtraArgs", self.ui.le_extra_args.text().strip()) if self.ui.rb_source_default.isChecked(): settings.setValue("RecordingSource", 0) elif self.ui.rb_source_manual.isChecked(): settings.setValue("RecordingSource", 1) elif self.ui.rb_source_selected.isChecked(): settings.setValue("RecordingSource", 2) settings.beginWriteArray("Sources") for i, (client, port) in enumerate(self.rec_sources): settings.setArrayIndex(i) settings.setValue("Client", client) settings.setValue("Port", port) settings.endArray() def loadSettings(self): settings = QSettings(ORGANIZATION, PROGRAM) self.restoreGeometry(settings.value("Geometry", b"")) outputFolder = settings.value("OutputFolder", get_user_dir("MUSIC")) if isdir(outputFolder): self.ui.le_folder.setText(outputFolder) self.ui.le_prefix.setText( settings.value("FilenamePrefix", "jack_capture_")) encFormat = settings.value("EncodingFormat", "wav", type=str) for i in range(self.ui.cb_format.count()): if self.ui.cb_format.itemText(i) == encFormat: self.ui.cb_format.setCurrentIndex(i) break encDepth = settings.value("EncodingDepth", "FLOAT", type=str) for i in range(self.ui.cb_depth.count()): if self.ui.cb_depth.itemData(i) == encDepth: self.ui.cb_depth.setCurrentIndex(i) break encChannels = settings.value("EncodingChannels", 2, type=int) if encChannels == 1: self.ui.rb_mono.setChecked(True) elif encChannels == 2: self.ui.rb_stereo.setChecked(True) else: self.ui.rb_outro.setChecked(True) self.ui.sb_channels.setValue(encChannels) recSource = settings.value("RecordingSource", 0, type=int) if recSource == 1: self.ui.rb_source_manual.setChecked(True) elif recSource == 2: self.ui.rb_source_selected.setChecked(True) else: self.ui.rb_source_default.setChecked(True) self.ui.group_time.setChecked( settings.value("UseTransport", False, type=bool)) self.ui.te_start.setTime( settings.value("StartTime", self.ui.te_start.time(), type=QTime)) self.ui.te_end.setTime( settings.value("EndTime", self.ui.te_end.time(), type=QTime)) self.ui.le_extra_args.setText(settings.value("ExtraArgs", "", type=str)) size = settings.beginReadArray("Sources") for i in range(size): settings.setArrayIndex(i) client = settings.value("Client", type=str) port = settings.value("Port", type=str) if client and port: self.rec_sources.add((client, port)) settings.endArray() def closeEvent(self, event): self.saveSettings() self.fJackClient.close() QDialog.closeEvent(self, event) def done(self, r): QDialog.done(self, r) self.close()
messages.append(message) except zmq.ZMQError: pass finally: self.notifier.setEnabled(True) if messages: self.sig_received.emit(messages) def close(self): """Read any remaining messages and close stream.""" self.received_message() # Flush remaining messages self.notifier.setEnabled(False) self.socket.close() self.context.destroy() if __name__ == '__main__': # For testing, construct a ZMQ stream between two processes and send # the number 42 over the stream if len(sys.argv) == 1: app = QApplication(sys.argv) manager = ZmqStreamReader() manager.sig_received.connect(print) process = QProcess() process.start('python', [sys.argv[0], str(manager.port)]) process.finished.connect(app.quit) sys.exit(app.exec_()) else: worker = ZmqStreamWriter(sys.argv[1]) worker.write(42)
class JobRunner(QWidget): """objuect to run GUI subprocess calls""" def __init__(self, run_button=None, qwidgets_to_disable=None, text_edit=None, busy=None, timer_label=None, parent=None, setup_layout=True, **kwargs): super(JobRunner, self).__init__(parent, **kwargs) self.is_passed = False self._iteration_edit = QLineEdit(self, placeholderText="Iteration (n)") self._iteration_edit.setText('5') if run_button is None: run_button = QPushButton("Calculate Prime", self) run_button.clicked.connect(self.prime_requested) if text_edit is None: text_edit = QTextEdit("Result:<br>", self) text_edit.setReadOnly(True) if busy is None: busy = QProgressBar(self) self._busy = busy if timer_label is None: timer_label = QLabel('0:00') self._timer_label = timer_label self._busy.setVisible(False) self._timer_label.setVisible(False) self._run_button = run_button self._text_edit = text_edit self._time0 = None self.process = None if qwidgets_to_disable is None: qwidgets_to_disable = [self._run_button, self._iteration_edit] self.qwidgets_to_disable = qwidgets_to_disable if setup_layout: self.setup_layout() def setup_layout(self): hbox_timer = QHBoxLayout() hbox_timer.addWidget(self._busy) hbox_timer.addWidget(self._timer_label) vbox = QVBoxLayout(self) vbox.addWidget(self._iteration_edit) vbox.addWidget(self._run_button) vbox.addLayout(hbox_timer) vbox.addWidget(self._text_edit) self.setLayout(vbox) self.show() def prime_requested(self): try: n = int(self._iteration_edit.text()) except: return #command_list = [str(n)] command_list = ['python', 'ls.py'] self._text_edit.clear() self.run_command(command_list) def run_command(self, command_list): for qwidget in self.qwidgets_to_disable: qwidget.setEnabled(False) self._busy.setRange(0, 0) self._busy.setVisible(True) self._busy.update() self._timer_label.setVisible(True) self._time0 = time.time() self.process = QProcess(self) self.process.readyReadStandardOutput.connect(self.stdout_ready) self.process.finished.connect(self.on_finished) #self.process.started.connect(lambda: print_func('Started!')) #self.process.finished.connect(lambda: print_func('Finished!')) #self.process.startDetached(command_list[0], command_list[1:]) self.process.start(command_list[0], command_list[1:]) for qwidget in self.qwidgets_to_disable: qwidget.setEnabled(True) def stdout_ready(self): text = str(self.process.readAllStandardOutput()) self.append(text) dt = time.time() - self._time0 minutes = dt // 60 seconds = dt % 60 self._timer_label.setText('%i:%02i' % (minutes, seconds)) def append(self, text): cursor = self._text_edit.textCursor() cursor.movePosition(cursor.End) cursor.insertText(text) #self.output.ensureCursorVisible() def on_finished(self, exit_code): #print('exit_code =', exit_code) self.is_passed = True text = str(self.process.readAllStandardError()) self.append(text) dt = time.time() - self._time0 minutes = dt // 60 seconds = dt % 60 time_msg = '' if minutes: time_msg += '%i minutes and ' % minutes time_msg += '%i seconds' % seconds self.append('\nJob completed in %s' % time_msg) self._busy.setVisible(False) self._timer_label.setVisible(False) #self._timer_label.stop() def display_prime(self, msg): text_edit = self._text_edit text_cursor = text_edit.textCursor() end = text_cursor.End # end of text_edit text_cursor.movePosition(end) text_edit.setTextCursor(text_cursor) text_edit.insertHtml(msg) text_edit.ensureCursorVisible() # new message will be visible def unlock(self): #self._busy.setRange(0, 100) for qwidget in self.qwidgets_to_disable: qwidget.setEnabled(True)
class QtPipDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setup_ui() self.setAttribute(Qt.WA_DeleteOnClose) # create install process self.process = QProcess(self) self.process.setProgram(sys.executable) self.process.setProcessChannelMode(QProcess.MergedChannels) # setup process path env = QProcessEnvironment() combined_paths = os.pathsep.join( [user_site_packages(), env.systemEnvironment().value("PYTHONPATH")] ) env.insert("PYTHONPATH", combined_paths) self.process.setProcessEnvironment(env) # connections self.install_button.clicked.connect(self._install) self.uninstall_button.clicked.connect(self._uninstall) self.process.readyReadStandardOutput.connect(self._on_stdout_ready) from ..plugins import plugin_manager self.process.finished.connect(plugin_manager.discover) self.process.finished.connect(plugin_manager.prune) def setup_ui(self): layout = QVBoxLayout() self.setLayout(layout) title = QLabel("Install/Uninstall Packages:") title.setObjectName("h2") self.line_edit = QLineEdit() self.install_button = QPushButton("install", self) self.uninstall_button = QPushButton("uninstall", self) self.text_area = QTextEdit(self, readOnly=True) hlay = QHBoxLayout() hlay.addWidget(self.line_edit) hlay.addWidget(self.install_button) hlay.addWidget(self.uninstall_button) layout.addWidget(title) layout.addLayout(hlay) layout.addWidget(self.text_area) self.setFixedSize(700, 400) self.setMaximumHeight(800) self.setMaximumWidth(1280) def _install(self): cmd = ['-m', 'pip', 'install'] if running_as_bundled_app() and sys.platform.startswith('linux'): cmd += [ '--no-warn-script-location', '--prefix', user_plugin_dir(), ] self.process.setArguments(cmd + self.line_edit.text().split()) self.text_area.clear() self.process.start() def _uninstall(self): args = ['-m', 'pip', 'uninstall', '-y'] self.process.setArguments(args + self.line_edit.text().split()) self.text_area.clear() self.process.start() def _on_stdout_ready(self): text = self.process.readAllStandardOutput().data().decode() self.text_area.append(text)
class ExternalSystemShell(ExternalShellBase): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = TerminalWidget started = Signal() def __init__(self, parent=None, wdir=None, path=[], light_background=True, menu_actions=None, show_buttons_inside=True, show_elapsed_time=True): ExternalShellBase.__init__(self, parent=parent, fname=None, wdir=wdir, history_filename='.history', light_background=light_background, menu_actions=menu_actions, show_buttons_inside=show_buttons_inside, show_elapsed_time=show_elapsed_time) # Additional python path list self.path = path # For compatibility with the other shells that can live in the external # console self.is_ipykernel = False self.connection_file = None def get_icon(self): return ima.icon('cmdprompt') def finish_process(self): while not self.process.waitForFinished(100): self.process.kill(); def create_process(self): self.shell.clear() self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.MergedChannels) # PYTHONPATH (in case we use Python in this terminal, e.g. py2exe) env = [to_text_string(_path) for _path in self.process.systemEnvironment()] processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) add_pathlist_to_PYTHONPATH(env, self.path) self.process.setProcessEnvironment(processEnvironment) # Working directory if self.wdir is not None: self.process.setWorkingDirectory(self.wdir) # Shell arguments if os.name == 'nt': p_args = ['/Q'] else: p_args = ['-i'] if self.arguments: p_args.extend( shell_split(self.arguments) ) self.process.readyReadStandardOutput.connect(self.write_output) self.process.finished.connect(self.finished) self.kill_button.clicked.connect(self.process.kill) if os.name == 'nt': self.process.start('cmd.exe', p_args) else: # Using bash: self.process.start('bash', p_args) self.send_to_process('PS1="\\u@\\h:\\w> "\n') running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) else: self.shell.setFocus() self.started.emit() return self.process #=============================================================================== # Input/Output #=============================================================================== def transcode(self, qba): if os.name == 'nt': return to_text_string( CP850_CODEC.toUnicode(qba.data()) ) else: return ExternalShellBase.transcode(self, qba) def send_to_process(self, text): if not is_text_string(text): text = to_text_string(text) if text[:-1] in ["clear", "cls", "CLS"]: self.shell.clear() self.send_to_process(os.linesep) return if not text.endswith('\n'): text += '\n' if os.name == 'nt': self.process.write(text.encode('cp850')) else: self.process.write(LOCALE_CODEC.fromUnicode(text)) self.process.waitForBytesWritten(-1) def keyboard_interrupt(self): # This does not work on Windows: # (unfortunately there is no easy way to send a Ctrl+C to cmd.exe) self.send_ctrl_to_process('c')
class PylintWidget(QWidget): """ Pylint widget """ DATAPATH = get_conf_path('pylint.results') VERSION = '1.1.0' redirect_stdio = Signal(bool) start_analysis = Signal() def __init__(self, parent, max_entries=100, options_button=None, text_color=None, prevrate_color=None): super().__init__(parent) self.setWindowTitle("Pylint") self.output = None self.error_output = None self.filename = None self.text_color = text_color self.prevrate_color = prevrate_color self.max_entries = max_entries self.rdata = [] if osp.isfile(self.DATAPATH): try: data = pickle.loads(open(self.DATAPATH, 'rb').read()) if data[0] == self.VERSION: self.rdata = data[1:] except (EOFError, ImportError): pass self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton( self, icon=ima.icon('run'), text=_("Analyze"), tip=_("Run analysis"), triggered=self.analyze_button_handler, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current analysis"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) self.filecombo.valid.connect(self.check_new_file) browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python file'), triggered=self.select_file) self.ratelabel = QLabel() self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Complete output"), triggered=self.show_log) self.treewidget = ResultsTree(self) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) if options_button: hlayout1.addWidget(options_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.ratelabel) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.treewidget) self.setLayout(layout) self.process = None self.set_running_state(False) self.show_data() if self.rdata: self.remove_obsolete_items() self.filecombo.addItems(self.get_filenames()) self.start_button.setEnabled(self.filecombo.is_valid()) else: self.start_button.setEnabled(False) def check_new_file(self): fname = self.get_filename() if fname != self.filename: self.filename = fname self.show_data() def get_filename(self): """Get current filename in combobox.""" return self.filecombo.currentText() @Slot(str) def set_filename(self, filename): """Set filename without performing code analysis.""" filename = str(filename) # filename is a QString instance self.kill_if_running() index, _data = self.get_data(filename) if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() def analyze(self, filename=None): """ Perform code analysis for given `filename`. If `filename` is None default to current filename in combobox. """ if filename is not None: self.set_filename(filename) if self.filecombo.is_valid(): self.start() @Slot() def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python file"), getcwd_or_home(), _("Python files") + " (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def remove_obsolete_items(self): """Removing obsolete items""" self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): return [filename for filename, _data in self.rdata] def get_data(self, filename): filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) self.save() def save(self): while len(self.rdata) > self.max_entries: self.rdata.pop(-1) pickle.dump([self.VERSION] + self.rdata, open(self.DATAPATH, 'wb'), 2) @Slot() def show_log(self): if self.output: output_dialog = TextEditor(self.output, title=_("Pylint output"), parent=self, readonly=True) output_dialog.resize(700, 500) output_dialog.exec_() @Slot() def analyze_button_handler(self): """Try to start code analysis when Analyze button pressed.""" self.start_analysis.emit() def get_pylintrc_path(self, filename): """Get the path to the most proximate pylintrc config to the file.""" parent = self.parentWidget() if parent is not None: project_dir = parent.main.projects.get_active_project_path() else: project_dir = None search_paths = [ osp.dirname(filename), # File's directory getcwd_or_home(), # Working directory project_dir, # Project directory osp.expanduser("~"), # Home directory ] return get_pylintrc_path(search_paths=search_paths) @Slot() def start(self): """Start the code analysis.""" filename = str(self.filecombo.currentText()) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(getcwd_or_home()) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) self.output = '' self.error_output = '' if PYLINT_VER is not None: pylint_args = [ '-m', 'pylint', '--output-format=text', '--msg-template=' "'{msg_id}:{symbol}:{line:3d},{column}: {msg}'" ] pylintrc_path = self.get_pylintrc_path(filename=filename) if pylintrc_path is not None: pylint_args += ['--rcfile={}'.format(pylintrc_path)] pylint_args.append(filename) processEnvironment = QProcessEnvironment() processEnvironment.insert("PYTHONIOENCODING", "utf8") self.process.setProcessEnvironment(processEnvironment) self.process.start(sys.executable, pylint_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = str(qba.data(), 'utf-8') if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) if not self.output: if self.error_output: QMessageBox.critical(self, _("Error"), self.error_output) print("pylint error:\n\n" + self.error_output, file=sys.stderr) return # Convention, Refactor, Warning, Error results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} txt_module = '************* Module ' module = '' # Should not be needed - just in case something goes wrong for line in self.output.splitlines(): if line.startswith(txt_module): # New module module = line[len(txt_module):] continue # Supporting option include-ids: ('R3873:' instead of 'R:') if not re.match(r'^[CRWE]+([0-9]{4})?:', line): continue items = {} idx_0 = 0 idx_1 = 0 key_names = ["msg_id", "message_name", "line_nb", "message"] for key_idx, key_name in enumerate(key_names): if key_idx == len(key_names) - 1: idx_1 = len(line) else: idx_1 = line.find(":", idx_0) if idx_1 < 0: break item = line[(idx_0):idx_1] if not item: break if key_name == "line_nb": item = int(item.split(',')[0]) items[key_name] = item idx_0 = idx_1 + 1 else: pylint_item = (module, items["line_nb"], items["message"], items["msg_id"], items["message_name"]) results[line[0] + ':'].append(pylint_item) # Rate rate = None txt_rate = 'Your code has been rated at ' i_rate = self.output.find(txt_rate) if i_rate > 0: i_rate_end = self.output.find('/10', i_rate) if i_rate_end > 0: rate = self.output[i_rate + len(txt_rate):i_rate_end] # Previous run previous = '' if rate is not None: txt_prun = 'previous run: ' i_prun = self.output.find(txt_prun, i_rate_end) if i_prun > 0: i_prun_end = self.output.find('/10', i_prun) previous = self.output[i_prun + len(txt_prun):i_prun_end] filename = str(self.filecombo.currentText()) self.set_data(filename, (time.localtime(), rate, previous, results)) self.output = self.error_output + self.output self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None and len(self.output) > 0) self.kill_if_running() filename = str(self.filecombo.currentText()) if not filename: return _index, data = self.get_data(filename) if data is None: text = _('Source code has not been rated yet.') self.treewidget.clear_results() date_text = '' else: datetime, rate, previous_rate, results = data if rate is None: text = _('Analysis did not succeed ' '(see output for more details).') self.treewidget.clear_results() date_text = '' else: text_style = "<span style=\'color: %s\'><b>%s </b></span>" rate_style = "<span style=\'color: %s\'><b>%s</b></span>" prevrate_style = "<span style=\'color: %s\'>%s</span>" color = "#FF0000" if float(rate) > 5.: color = "#22AA22" elif float(rate) > 3.: color = "#EE5500" text = _('Global evaluation:') text = ((text_style % (self.text_color, text)) + (rate_style % (color, ('%s/10' % rate)))) if previous_rate: text_prun = _('previous run:') text_prun = ' (%s %s/10)' % (text_prun, previous_rate) text += prevrate_style % (self.prevrate_color, text_prun) self.treewidget.set_results(filename, results) date = time.strftime("%Y-%m-%d %H:%M:%S", datetime) date_text = text_style % (self.text_color, date) self.ratelabel.setText(text) self.datelabel.setText(date_text)
class LineProfilerWidget(QWidget): """ Line profiler widget. """ DATAPATH = get_conf_path('lineprofiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) def __init__(self, parent): QWidget.__init__(self, parent) self.setWindowTitle("Line profiler") self.output = None self.error_output = None self.use_colors = True self._last_wdir = None self._last_args = None self._last_pythonpath = None self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton( self, icon=get_icon('run.png'), text=_("Profile by line"), tip=_("Run line profiler"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton( self, icon=get_icon('terminate.png'), text=_("Stop"), tip=_("Stop current profiling"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) #self.filecombo.valid.connect(self.show_data) # FIXME: The combobox emits this signal on almost any event # triggering show_data() too early, too often. browse_button = create_toolbutton( self, icon=get_icon('fileopen.png'), tip=_('Select Python script'), triggered=self.select_file) self.datelabel = QLabel() self.log_button = create_toolbutton( self, icon=get_icon('log.png'), text=_("Output"), text_beside_icon=True, tip=_("Show program's output"), triggered=self.show_log) self.datatree = LineProfilerDataTree(self) self.collapse_button = create_toolbutton( self, icon=get_icon('collapse.png'), triggered=lambda dD=-1: self.datatree.collapseAll(), tip=_('Collapse all')) self.expand_button = create_toolbutton( self, icon=get_icon('expand.png'), triggered=lambda dD=1: self.datatree.expandAll(), tip=_('Expand all')) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.collapse_button) hlayout2.addWidget(self.expand_button) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.datatree) self.setLayout(layout) self.process = None self.set_running_state(False) self.start_button.setEnabled(False) if not is_lineprofiler_installed(): for widget in (self.datatree, self.filecombo, self.log_button, self.start_button, self.stop_button, browse_button, self.collapse_button, self.expand_button): widget.setDisabled(True) text = _( '<b>Please install the <a href="%s">line_profiler module</a></b>' ) % WEBSITE_URL self.datelabel.setText(text) self.datelabel.setOpenExternalLinks(True) else: pass # self.show_data() def analyze(self, filename, wdir=None, args=None, pythonpath=None, use_colors=True): self.use_colors = use_colors if not is_lineprofiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) index = None # FIXME: storing data is not implemented yet if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python script"), getcwd(), _("Python scripts")+" (*.py ; *.pyw)") self.redirect_stdio.emit(False) if filename: self.analyze(filename) def show_log(self): if self.output: TextEditor(self.output, title=_("Line profiler output"), readonly=True, size=(700, 500)).exec_() def show_errorlog(self): if self.error_output: TextEditor(self.error_output, title=_("Line profiler output"), readonly=True, size=(700, 500)).exec_() def start(self, wdir=None, args=None, pythonpath=None): filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(self.finished) self.stop_button.clicked.connect(self.process.kill) if pythonpath is not None: env = [to_text_string(_pth) for _pth in self.process.systemEnvironment()] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): filename = osp.normpath(filename).replace(os.sep, '/') p_args = ['-lvb', '-o', '"' + self.DATAPATH + '"', '"' + filename + '"'] if args: p_args.extend(programs.shell_split(args)) executable = '"' + programs.find_program('kernprof') + '"' executable += ' ' + ' '.join(p_args) executable = executable.replace(os.sep, '/') self.process.start(executable) else: p_args = ['-lvb', '-o', self.DATAPATH, filename] if args: p_args.extend(programs.shell_split(args)) executable = 'kernprof' self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(locale_codec.toUnicode(qba.data())) if error: self.error_output += text else: self.output += text def finished(self): self.set_running_state(False) self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output # FIXME: figure out if show_data should be called here or # as a signal from the combobox self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled( self.output is not None and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return self.datatree.load_data(self.DATAPATH) self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.show_tree() text_style = "<span style=\'color: #444444\'><b>%s </b></span>" date_text = text_style % time.strftime("%d %b %Y %H:%M", time.localtime()) self.datelabel.setText(date_text)
class CondaProcess(QObject): """conda-api modified to work with QProcess instead of popen""" ENCODING = 'ascii' def __init__(self, parent, on_finished=None, on_partial=None): QObject.__init__(self, parent) self._parent = parent self.output = None self.partial = None self.stdout = None self.error = None self._parse = False self._function_called = '' self._name = None self._process = QProcess() self._on_finished = on_finished self._process.finished.connect(self._call_conda_ready) self._process.readyReadStandardOutput.connect(self._call_conda_partial) if on_finished is not None: self._process.finished.connect(on_finished) if on_partial is not None: self._process.readyReadStandardOutput.connect(on_partial) self.set_root_prefix() def _call_conda_partial(self): """ """ stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(stdout, CondaProcess.ENCODING) stderr = self._process.readAllStandardError() stderr = handle_qbytearray(stderr, CondaProcess.ENCODING) if self._parse: self.output = json.loads(stdout) else: self.output = stdout self.partial = self.output self.stdout = self.output self.error = stderr # print(self.partial) # print(self.error) def _call_conda_ready(self): """function called when QProcess in _call_conda finishes task""" function = self._function_called if self.stdout is None: stdout = to_text_string(self._process.readAllStandardOutput(), encoding=CondaProcess.ENCODING) else: stdout = self.stdout if self.error is None: stderr = to_text_string(self._process.readAllStandardError(), encoding=CondaProcess.ENCODING) else: stderr = self.error if function == 'get_conda_version': pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)') m = pat.match(stderr.strip()) if m is None: m = pat.match(stdout.strip()) if m is None: raise Exception('output did not match: %r' % stderr) self.output = m.group(1) # elif function == 'get_envs': # info = self.output # self.output = info['envs'] # elif function == 'get_prefix_envname': # name = self._name # envs = self.output # self.output = self._get_prefix_envname_helper(name, envs) # self._name = None elif function == 'config_path': result = self.output self.output = result['rc_path'] elif function == 'config_get': result = self.output self.output = result['get'] elif (function == 'config_delete' or function == 'config_add' or function == 'config_set' or function == 'config_remove'): result = self.output self.output = result.get('warnings', []) elif function == 'pip': result = [] lines = self.output.split('\n') for line in lines: if '<pip>' in line: temp = line.split()[:-1] + ['pip'] result.append('-'.join(temp)) self.output = result if stderr.strip(): self.error = stderr # raise Exception('conda %r:\nSTDERR:\n%s\nEND' % (extra_args, # stderr.decode())) self._parse = False def _get_prefix_envname_helper(self, name, envs): """ """ global ROOTPREFIX if name == 'root': return ROOT_PREFIX for prefix in envs: if basename(prefix) == name: return prefix return None def _abspath(self, abspath): """ """ if abspath: if sys.platform == 'win32': python = join(ROOT_PREFIX, 'python.exe') conda = join(ROOT_PREFIX, 'Scripts', 'conda-script.py') else: python = join(ROOT_PREFIX, 'bin/python') conda = join(ROOT_PREFIX, 'bin/conda') cmd_list = [python, conda] else: # just use whatever conda is on the path cmd_list = ['conda'] return cmd_list def _call_conda(self, extra_args, abspath=True): """ """ # call conda with the list of extra arguments, and return the tuple # stdout, stderr global ROOT_PREFIX # if abspath: # if sys.platform == 'win32': # python = join(ROOT_PREFIX, 'python.exe') # conda = join(ROOT_PREFIX, # 'Scripts', 'conda-script.py') # else: # python = join(ROOT_PREFIX, 'bin/python') # conda = join(ROOT_PREFIX, 'bin/conda') # cmd_list = [python, conda] # else: # just use whatever conda is on the path # cmd_list = ['conda'] cmd_list = self._abspath(abspath) cmd_list.extend(extra_args) # try: # p = Popen(cmd_list, stdout=PIPE, stderr=PIPE) # except OSError: # raise Exception("could not invoke %r\n" % args) # return p.communicate() # adapted code # ------------ self.error, self.output = None, None self._process.start(cmd_list[0], cmd_list[1:]) def _call_and_parse(self, extra_args, abspath=True): """ """ # stdout, stderr = _call_conda(extra_args, abspath=abspath) # if stderr.decode().strip(): # raise Exception('conda %r:\nSTDERR:\n%s\nEND' % (extra_args, # stderr.decode())) # return json.loads(stdout.decode()) # adapted code # ------------ self._parse = True self._call_conda(extra_args, abspath=abspath) def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()): """ """ cmd_list = [] if kwargs.get('override_channels', False) and 'channel' not in kwargs: raise TypeError('conda search: override_channels requires channel') if 'env' in kwargs: cmd_list.extend(['--name', kwargs.pop('env')]) if 'prefix' in kwargs: cmd_list.extend(['--prefix', kwargs.pop('prefix')]) if 'channel' in kwargs: channel = kwargs.pop('channel') if isinstance(channel, str): cmd_list.extend(['--channel', channel]) else: cmd_list.append('--channel') cmd_list.extend(channel) for key in keys: if key in kwargs and kwargs[key]: cmd_list.append('--' + key.replace('_', '-')) return cmd_list def set_root_prefix(self, prefix=None): """ Set the prefix to the root environment (default is /opt/anaconda). This function should only be called once (right after importing conda_api). """ global ROOT_PREFIX if prefix: ROOT_PREFIX = prefix # find *some* conda instance, and then use info() to get 'root_prefix' else: pass # i = self.info(abspath=False) # self.ROOT_PREFIX = i['root_prefix'] ''' plat = 'posix' if sys.platform.lower().startswith('win'): listsep = ';' plat = 'win' else: listsep = ':' for p in os.environ['PATH'].split(listsep): if (os.path.exists(os.path.join(p, 'conda')) or os.path.exists(os.path.join(p, 'conda.exe')) or os.path.exists(os.path.join(p, 'conda.bat'))): # TEMPORARY: ROOT_PREFIX = os.path.dirname(p) # root prefix is 1 dir up i = info() # REAL: ROOT_PREFIX = i['root_prefix'] break else: # fall back to standard install location, which may be wrong if plat == 'win': ROOT_PREFIX = 'C:\Anaconda' else: ROOT_PREFIX = '/opt/anaconda' ''' # adapted code # ------------ if ROOT_PREFIX is None: # qprocess = QProcess() # cmd_list = ['conda', 'info', '--json'] # qprocess.start(cmd_list[0], cmd_list[1:]) # qprocess.waitForFinished() # output = qprocess.readAllStandardOutput() # output = handle_qbytearray(output, CondaProcess.ENCODING) # info = json.loads(output) # ROOT_PREFIX = info['root_prefix'] info = self.info(abspath=False) ROOT_PREFIX = info['root_prefix'] def get_conda_version(self): """ return the version of conda being used (invoked) as a string """ # pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)') # stdout, stderr = self._call_conda(['--version']) # # argparse outputs version to stderr in Python < 3.4. # # http://bugs.python.org/issue18920 # m = pat.match(stderr.decode().strip()) # if m is None: # m = pat.match(stdout.decode().strip()) # # if m is None: # raise Exception('output did not match: %r' % stderr) # return m.group(1) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'get_conda_version' self._call_conda(['--version']) def get_envs(self): """ Return all of the (named) environment (this does not include the root environment), as a list of absolute path to their prefixes. """ # info = self._call_and_parse(['info', '--json']) # return info['envs'] info = self.info() return info['envs'] # adapted code # ------------ # if self._process.state() == QProcess.NotRunning: # self._function_called = 'get_envs' # self._call_and_parse(['info', '--json']) def get_prefix_envname(self, name): """ Given the name of an environment return its full prefix path, or None if it cannot be found. """ if name == 'root': return ROOT_PREFIX for prefix in self.get_envs(): if basename(prefix) == name: return prefix return None # adapted code # ------------ # if self._process.state() == QProcess.NotRunning: # self._name = name # self._function_called = 'get_prefix_envname' # self._call_and_parse(['info', '--json']) def info(self, abspath=True): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ # return self._call_and_parse(['info', '--json'], abspath=abspath) qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(['info', '--json']) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) return info # adapted code # ------------ # if self._process.state() == QProcess.NotRunning: # self._function_called = 'info' # self._call_and_parse(['info', '--json'], abspath=abspath) def package_info(self, package, abspath=True): """ Return a dictionary with package information. Structure is { 'package_name': [{ 'depends': list, 'version': str, 'name': str, 'license': str, 'fn': ..., ... }] } """ # return self._call_and_parse(['info', package, '--json'], # abspath=abspath) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'package_info' self._call_and_parse(['info', package, '--json'], abspath=abspath) def search(self, regex=None, spec=None, **kwargs): """ Search for packages. """ cmd_list = ['search', '--json'] if regex and spec: raise TypeError('conda search: only one of regex or spec allowed') if regex: cmd_list.append(regex) if spec: cmd_list.extend(['--spec', spec]) if 'platform' in kwargs: platform = kwargs.pop('platform') platforms = ('win-32', 'win-64', 'osx-64', 'linux-32', 'linux-64') if platform not in platforms: raise TypeError('conda search: platform must be one of ' + ', '.join(platforms)) cmd_list.extend(['--platform', platform]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('canonical', 'unknown', 'use_index_cache', 'outdated', 'override_channels'))) # return self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'search' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def share(self, prefix): """ Create a "share package" of the environment located in `prefix`, and return a dictionary with (at least) the following keys: - 'path': the full path to the created package - 'warnings': a list of warnings This file is created in a temp directory, and it is the callers responsibility to remove this directory (after the file has been handled in some way). """ # return self._call_and_parse(['share', '--json', '--prefix', prefix]) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'share' self._call_and_parse(['share', '--json', '--prefix', prefix]) def create(self, name=None, path=None, pkgs=None): """ Create an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into new environment') cmd_list = ['create', '--yes', '--quiet'] if name: ref = name search = [os.path.join(d, name) for d in self.info()['envs_dirs']] cmd_list = ['create', '--yes', '--quiet', '--name', name] elif path: ref = path search = [path] cmd_list = ['create', '--yes', '--quiet', '--prefix', path] else: raise TypeError('must specify either an environment name or a path' ' for new environment') if any(os.path.exists(path) for path in search): raise CondaEnvExistsError('Conda environment [%s] already exists' % ref) cmd_list.extend(pkgs) # (out, err) = self._call_conda(cmd_list) # if err.decode().strip(): # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # err.decode())) # return out # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'create' self._call_conda(cmd_list) def install(self, name=None, path=None, pkgs=None, dep=True): """ Install packages into an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--yes', '--json', '--force-pscheck'] # cmd_list = ['install', '--yes', '--quiet'] if name: cmd_list.extend(['--name', name]) elif path: cmd_list.extend(['--prefix', path]) else: # just install into the current environment, whatever that is pass cmd_list.extend(pkgs) # (out, err) = self._call_conda(cmd_list) # if err.decode().strip(): # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # err.decode())) # return out # adapted code # ------------ if not dep: cmd_list.extend(['--no-deps']) if self._process.state() == QProcess.NotRunning: self._function_called = 'install' self._call_conda(cmd_list) def update(self, *pkgs, **kwargs): """ Update package(s) (in an environment) by name. """ cmd_list = ['update', '--json', '--quiet', '--yes'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to update, \ or all=True.") cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'no_deps', 'override_channels', 'no_pin', 'force', 'all', 'use_index_cache', 'use_local', 'alt_hint'))) cmd_list.extend(pkgs) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # # return result # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'update' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove(self, *pkgs, **kwargs): """ Remove a package (from an environment) by name. Returns { success: bool, (this is always true), (other information) } """ cmd_list = ['remove', '--json', '--quiet', '--yes', '--force-pscheck'] # cmd_list = ['remove', '--json', '--quiet', '--yes'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to remove, \ or all=True.") if kwargs.get('name') and kwargs.get('path'): raise TypeError('conda remove: At most one of name, path allowed') if kwargs.get('name'): cmd_list.extend(['--name', kwargs.pop('name')]) if kwargs.get('path'): cmd_list.extend(['--prefix', kwargs.pop('path')]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'features', 'override_channels', 'no_pin', 'force', 'all'))) cmd_list.extend(pkgs) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # # return result # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'remove' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove_environment(self, name=None, path=None, **kwargs): """ Remove an environment entirely. See ``remove``. """ # return self.remove(name=name, path=path, all=True, **kwargs) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'remove_environment' self.remove(name=name, path=path, all=True, **kwargs) def clone_environment(self, clone, name=None, path=None, **kwargs): """ Clone the environment ``clone`` into ``name`` or ``path``. """ cmd_list = ['create', '--json', '--quiet'] if (name and path) or not (name or path): raise TypeError("conda clone_environment: exactly one of name or \ path required") if name: cmd_list.extend(['--name', name]) if path: cmd_list.extend(['--prefix', path]) cmd_list.extend(['--clone', clone]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'unknown', 'use_index_cache', 'use_local', 'no_pin', 'force', 'all', 'channel', 'override_channels', 'no_default_packages'))) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # # return result # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'clone_environment' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) # def process(self, name=None, path=None, cmd=None, args=None, stdin=None, # stdout=None, stderr=None, timeout=None): # """ # Create a Popen process for cmd using the specified args but in the # conda environment specified by name or path. # # The returned object will need to be invoked with p.communicate() or # similar. # # :param name: name of conda environment # :param path: path to conda environment (if no name specified) # :param cmd: command to invoke # :param args: argument # :param stdin: stdin # :param stdout: stdout # :param stderr: stderr # :return: Popen object # """ # # if bool(name) == bool(path): # raise TypeError('exactly one of name or path must be specified') # # if not cmd: # raise TypeError('cmd to execute must be specified') # # if not args: # args = [] # # if name: # path = self.get_prefix_envname(name) # # plat = 'posix' # if sys.platform.lower().startswith('win'): # listsep = ';' # plat = 'win' # else: # listsep = ':' # # conda_env = dict(os.environ) # # if plat == 'posix': # conda_env['PATH'] = path + os.path.sep + 'bin' + listsep + \ # conda_env['PATH'] # else: # win # conda_env['PATH'] = path + os.path.sep + 'Scripts' + listsep + \ # conda_env['PATH'] # # conda_env['PATH'] = path + listsep + conda_env['PATH'] # # cmd_list = [cmd] # cmd_list.extend(args) # # try: # p = Popen(cmd_list, env=conda_env, stdin=stdin, stdout=stdout, # stderr=stderr) # except OSError: # raise Exception("could not invoke %r\n" % cmd_list) # return p def clone(self, path, prefix): """ Clone a "share package" (located at `path`) by creating a new environment at `prefix`, and return a dictionary with (at least) the following keys: - 'warnings': a list of warnings The directory `path` is located in, should be some temp directory or some other directory OUTSIDE /opt/anaconda. After calling this function, the original file (at `path`) may be removed (by the caller of this function). The return object is a list of warnings. """ # return self._call_and_parse(['clone', '--json', '--prefix', prefix, # path]) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'clone' self._call_and_parse(['clone', '--json', '--prefix', prefix, path]) def _setup_config_from_kwargs(kwargs): cmd_list = ['--json', '--force'] if 'file' in kwargs: cmd_list.extend(['--file', kwargs['file']]) if 'system' in kwargs: cmd_list.append('--system') return cmd_list def config_path(self, **kwargs): """ Get the path to the config file. """ cmd_list = ['config', '--get'] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result['rc_path'] # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_path' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_get(self, *keys, **kwargs): """ Get the values of configuration keys. Returns a dictionary of values. Note, the key may not be in the dictionary if the key wasn't set in the configuration file. """ cmd_list = ['config', '--get'] cmd_list.extend(keys) cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result['get'] # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_get' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_set(self, key, value, **kwargs): """ Set a key to a (bool) value. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--set', key, str(value)] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result.get('warnings', []) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_set' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_add(self, key, value, **kwargs): """ Add a value to a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--add', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result.get('warnings', []) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_add' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_remove(self, key, value, **kwargs): """ Remove a value from a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result.get('warnings', []) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_remove' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_delete(self, key, **kwargs): """ Remove a key entirely. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove-key', key] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result.get('warnings', []) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_delete' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def run(self, command, abspath=True): """ Launch the specified app by name or full package name. Returns a dictionary containing the key "fn", whose value is the full package (ending in ``.tar.bz2``) of the app. """ cmd_list = ['run', '--json', command] # result = self._call_and_parse(cmd_list, abspath=abspath) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'run' self._call_and_parse(cmd_list, abspath=abspath) # def test(): # """ # Self-test function, which prints useful debug information. # This function returns None on success, and will crash the interpreter # on failure. # """ # print('sys.version: %r' % sys.version) # print('sys.prefix : %r' % sys.prefix) # print('conda_api.__version__: %r' % __version__) # print('conda_api.ROOT_PREFIX: %r' % ROOT_PREFIX) # if isdir(ROOT_PREFIX): # conda_version = get_conda_version() # print('conda version: %r' % conda_version) # print('conda info:') # d = info() # for kv in d.items(): # print('\t%s=%r' % kv) # assert d['conda_version'] == conda_version # else: # print('Warning: no such directory: %r' % ROOT_PREFIX) # print('OK') # ---- Additional methods not in conda-api def pip(self, name): """Get list of pip installed packages.""" cmd_list = ['list', '-n', name] if self._process.state() == QProcess.NotRunning: self._function_called = 'pip' self._call_conda(cmd_list) def dependencies(self, name=None, path=None, pkgs=None, dep=True): """ Install packages into an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--dry-run', '--json'] cmd_list = ['install', '--dry-run', '--json', '--force-pscheck'] if not dep: cmd_list.extend(['--no-deps']) if name: cmd_list.extend(['--name', name]) elif path: cmd_list.extend(['--prefix', path]) else: # just install into the current environment, whatever that is pass cmd_list.extend(pkgs) if self._process.state() == QProcess.NotRunning: self._function_called = 'install_dry' self._call_and_parse(cmd_list)
class LSPClient(QObject, LSPMethodProviderMixIn): """Language Server Protocol v3.0 client implementation.""" #: Signal to inform the editor plugin that the client has # started properly and it's ready to be used. sig_initialize = Signal(dict, str) #: Signal to report internal server errors through Spyder's # facilities. sig_server_error = Signal(str) #: Signal to warn the user when either the transport layer or the # server went down sig_went_down = Signal(str) def __init__(self, parent, server_settings={}, folder=getcwd_or_home(), language='python'): QObject.__init__(self) self.manager = parent self.zmq_in_socket = None self.zmq_out_socket = None self.zmq_in_port = None self.zmq_out_port = None self.transport = None self.server = None self.stdio_pid = None self.notifier = None self.language = language self.initialized = False self.ready_to_close = False self.request_seq = 1 self.req_status = {} self.watched_files = {} self.watched_folders = {} self.req_reply = {} self.server_unresponsive = False self.transport_unresponsive = False # Select a free port to start the server. # NOTE: Don't use the new value to set server_setttings['port']!! # That's not required because this doesn't really correspond to a # change in the config settings of the server. Else a server # restart would be generated when doing a # workspace/didChangeConfiguration request. if not server_settings['external']: self.server_port = select_port( default_port=server_settings['port']) else: self.server_port = server_settings['port'] self.server_host = server_settings['host'] self.external_server = server_settings.get('external', False) self.stdio = server_settings.get('stdio', False) # Setting stdio on implies that external_server is off if self.stdio and self.external_server: error = ('If server is set to use stdio communication, ' 'then it cannot be an external server') logger.error(error) raise AssertionError(error) self.folder = folder self.configurations = server_settings.get('configurations', {}) self.client_capabilites = CLIENT_CAPABILITES self.server_capabilites = SERVER_CAPABILITES self.context = zmq.Context() # To set server args self._server_args = server_settings.get('args', '') self._server_cmd = server_settings['cmd'] # Save requests name and id. This is only necessary for testing. self._requests = [] def _get_log_filename(self, kind): """ Get filename to redirect server or transport logs to in debugging mode. Parameters ---------- kind: str It can be "server" or "transport". """ if get_debug_level() == 0: return None fname = '{0}_{1}_{2}.log'.format(kind, self.language, os.getpid()) location = get_conf_path(osp.join('lsp_logs', fname)) # Create directory that contains the file, in case it doesn't # exist if not osp.exists(osp.dirname(location)): os.makedirs(osp.dirname(location)) return location @property def server_log_file(self): """ Filename to redirect the server process stdout/stderr output. """ return self._get_log_filename('server') @property def transport_log_file(self): """ Filename to redirect the transport process stdout/stderr output. """ return self._get_log_filename('transport') @property def server_args(self): """Arguments for the server process.""" args = [] if self.language == 'python': args += [sys.executable, '-m'] args += [self._server_cmd] # Replace host and port placeholders host_and_port = self._server_args.format( host=self.server_host, port=self.server_port) if len(host_and_port) > 0: args += host_and_port.split(' ') if self.language == 'python' and get_debug_level() > 0: args += ['--log-file', self.server_log_file] if get_debug_level() == 2: args.append('-v') elif get_debug_level() == 3: args.append('-vv') return args @property def transport_args(self): """Arguments for the transport process.""" args = [ sys.executable, '-u', osp.join(LOCATION, 'transport', 'main.py'), '--folder', self.folder, '--transport-debug', str(get_debug_level()) ] # Replace host and port placeholders host_and_port = '--server-host {host} --server-port {port} '.format( host=self.server_host, port=self.server_port) args += host_and_port.split(' ') # Add socket ports args += ['--zmq-in-port', str(self.zmq_out_port), '--zmq-out-port', str(self.zmq_in_port)] # Adjustments for stdio/tcp if self.stdio: args += ['--stdio-server'] if get_debug_level() > 0: args += ['--server-log-file', self.server_log_file] args += self.server_args else: args += ['--external-server'] return args def create_transport_sockets(self): """Create PyZMQ sockets for transport.""" self.zmq_out_socket = self.context.socket(zmq.PAIR) self.zmq_out_port = self.zmq_out_socket.bind_to_random_port( 'tcp://{}'.format(LOCALHOST)) self.zmq_in_socket = self.context.socket(zmq.PAIR) self.zmq_in_socket.set_hwm(0) self.zmq_in_port = self.zmq_in_socket.bind_to_random_port( 'tcp://{}'.format(LOCALHOST)) @Slot(QProcess.ProcessError) def handle_process_errors(self, error): """Handle errors with the transport layer or server processes.""" self.sig_went_down.emit(self.language) def start_server(self): """Start server.""" # This is not necessary if we're trying to connect to an # external server if self.external_server or self.stdio: return logger.info('Starting server: {0}'.format(' '.join(self.server_args))) # Create server process self.server = QProcess(self) env = self.server.processEnvironment() # Use local PyLS instead of site-packages one. if DEV or running_under_pytest(): running_in_ci = bool(os.environ.get('CI')) if os.name != 'nt' or os.name == 'nt' and not running_in_ci: env.insert('PYTHONPATH', os.pathsep.join(sys.path)[:]) # Adjustments for the Python language server. if self.language == 'python': # Set the PyLS current working to an empty dir inside # our config one. This avoids the server to pick up user # files such as random.py or string.py instead of the # standard library modules named the same. cwd = osp.join(get_conf_path(), 'lsp_paths', 'cwd') if not osp.exists(cwd): os.makedirs(cwd) # On Windows, some modules (notably Matplotlib) # cause exceptions if they cannot get the user home. # So, we need to pass the USERPROFILE env variable to # the PyLS. if os.name == "nt" and "USERPROFILE" in os.environ: env.insert("USERPROFILE", os.environ["USERPROFILE"]) else: # There's no need to define a cwd for other servers. cwd = None # Most LSP servers spawn other processes, which may require # some environment variables. for var in os.environ: env.insert(var, os.environ[var]) logger.info('Server process env variables: {0}'.format(env.keys())) # Setup server self.server.setProcessEnvironment(env) self.server.errorOccurred.connect(self.handle_process_errors) self.server.setWorkingDirectory(cwd) self.server.setProcessChannelMode(QProcess.MergedChannels) if self.server_log_file is not None: self.server.setStandardOutputFile(self.server_log_file) # Start server self.server.start(self.server_args[0], self.server_args[1:]) def start_transport(self): """Start transport layer.""" logger.info('Starting transport for {1}: {0}' .format(' '.join(self.transport_args), self.language)) # Create transport process self.transport = QProcess(self) env = self.transport.processEnvironment() # Most LSP servers spawn other processes other than Python, which may # require some environment variables if self.language != 'python' and self.stdio: for var in os.environ: env.insert(var, os.environ[var]) logger.info('Transport process env variables: {0}'.format( env.keys())) self.transport.setProcessEnvironment(env) # Modifying PYTHONPATH to run transport in development mode or # tests if DEV or running_under_pytest(): if running_under_pytest(): env.insert('PYTHONPATH', os.pathsep.join(sys.path)[:]) else: env.insert('PYTHONPATH', os.pathsep.join(sys.path)[1:]) self.transport.setProcessEnvironment(env) # Set up transport self.transport.errorOccurred.connect(self.handle_process_errors) if self.stdio: self.transport.setProcessChannelMode(QProcess.SeparateChannels) if self.transport_log_file is not None: self.transport.setStandardErrorFile(self.transport_log_file) else: self.transport.setProcessChannelMode(QProcess.MergedChannels) if self.transport_log_file is not None: self.transport.setStandardOutputFile(self.transport_log_file) # Start transport self.transport.start(self.transport_args[0], self.transport_args[1:]) def start(self): """Start client.""" # NOTE: DO NOT change the order in which these methods are called. self.create_transport_sockets() self.start_server() self.start_transport() # Create notifier fid = self.zmq_in_socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self.on_msg_received) # This is necessary for tests to pass locally! logger.debug('LSP {} client started!'.format(self.language)) def stop(self): """Stop transport and server.""" logger.info('Stopping {} client...'.format(self.language)) if self.notifier is not None: self.notifier.activated.disconnect(self.on_msg_received) self.notifier.setEnabled(False) self.notifier = None if self.transport is not None: self.transport.kill() self.context.destroy() if self.server is not None: self.server.kill() def is_transport_alive(self): """Detect if transport layer is alive.""" state = self.transport.state() return state != QProcess.NotRunning def is_stdio_alive(self): """Check if an stdio server is alive.""" alive = True if not psutil.pid_exists(self.stdio_pid): alive = False else: try: pid_status = psutil.Process(self.stdio_pid).status() except psutil.NoSuchProcess: pid_status = '' if pid_status == psutil.STATUS_ZOMBIE: alive = False return alive def is_server_alive(self): """Detect if a tcp server is alive.""" state = self.server.state() return state != QProcess.NotRunning def is_down(self): """ Detect if the transport layer or server are down to inform our users about it. """ is_down = False if self.transport and not self.is_transport_alive(): logger.debug( "Transport layer for {} is down!!".format(self.language)) if not self.transport_unresponsive: self.transport_unresponsive = True self.sig_went_down.emit(self.language) is_down = True if self.server and not self.is_server_alive(): logger.debug("LSP server for {} is down!!".format(self.language)) if not self.server_unresponsive: self.server_unresponsive = True self.sig_went_down.emit(self.language) is_down = True if self.stdio_pid and not self.is_stdio_alive(): logger.debug("LSP server for {} is down!!".format(self.language)) if not self.server_unresponsive: self.server_unresponsive = True self.sig_went_down.emit(self.language) is_down = True return is_down def send(self, method, params, kind): """Send message to transport.""" if self.is_down(): return if ClientConstants.CANCEL in params: return _id = self.request_seq if kind == MessageKind.REQUEST: msg = { 'id': self.request_seq, 'method': method, 'params': params } self.req_status[self.request_seq] = method elif kind == MessageKind.RESPONSE: msg = { 'id': self.request_seq, 'result': params } elif kind == MessageKind.NOTIFICATION: msg = { 'method': method, 'params': params } logger.debug('Perform request {0} with id {1}'.format(method, _id)) # Save requests to check their ordering. if running_under_pytest(): self._requests.append((_id, method)) # Try sending a message. If the send queue is full, keep trying for a # a second before giving up. timeout = 1 start_time = time.time() timeout_time = start_time + timeout while True: try: self.zmq_out_socket.send_pyobj(msg, flags=zmq.NOBLOCK) self.request_seq += 1 return int(_id) except zmq.error.Again: if time.time() > timeout_time: self.sig_went_down.emit(self.language) return # The send queue is full! wait 0.1 seconds before retrying. if self.initialized: logger.warning("The send queue is full! Retrying...") time.sleep(.1) @Slot() def on_msg_received(self): """Process received messages.""" self.notifier.setEnabled(False) while True: try: # events = self.zmq_in_socket.poll(1500) resp = self.zmq_in_socket.recv_pyobj(flags=zmq.NOBLOCK) try: method = resp['method'] logger.debug( '{} response: {}'.format(self.language, method)) except KeyError: pass if 'error' in resp: logger.debug('{} Response error: {}' .format(self.language, repr(resp['error']))) if self.language == 'python': # Show PyLS errors in our error report dialog only in # debug or development modes if get_debug_level() > 0 or DEV: message = resp['error'].get('message', '') traceback = (resp['error'].get('data', {}). get('traceback')) if traceback is not None: traceback = ''.join(traceback) traceback = traceback + '\n' + message self.sig_server_error.emit(traceback) req_id = resp['id'] if req_id in self.req_reply: self.req_reply[req_id](None, {'params': []}) elif 'method' in resp: if resp['method'][0] != '$': if 'id' in resp: self.request_seq = int(resp['id']) if resp['method'] in self.handler_registry: handler_name = ( self.handler_registry[resp['method']]) handler = getattr(self, handler_name) handler(resp['params']) elif 'result' in resp: if resp['result'] is not None: req_id = resp['id'] if req_id in self.req_status: req_type = self.req_status[req_id] if req_type in self.handler_registry: handler_name = self.handler_registry[req_type] handler = getattr(self, handler_name) handler(resp['result'], req_id) self.req_status.pop(req_id) if req_id in self.req_reply: self.req_reply.pop(req_id) except RuntimeError: # This is triggered when a codeeditor instance has been # removed before the response can be processed. pass except zmq.ZMQError: self.notifier.setEnabled(True) return def perform_request(self, method, params): if method in self.sender_registry: handler_name = self.sender_registry[method] handler = getattr(self, handler_name) _id = handler(params) if 'response_callback' in params: if params['requires_response']: self.req_reply[_id] = params['response_callback'] return _id # ------ LSP initialization methods -------------------------------- @handles(SERVER_READY) @send_request(method=LSPRequestTypes.INITIALIZE) def initialize(self, params, *args, **kwargs): self.stdio_pid = params['pid'] pid = self.transport.processId() if not self.external_server else None params = { 'processId': pid, 'rootUri': pathlib.Path(osp.abspath(self.folder)).as_uri(), 'capabilities': self.client_capabilites, 'trace': TRACE } return params @send_request(method=LSPRequestTypes.SHUTDOWN) def shutdown(self): params = {} return params @handles(LSPRequestTypes.SHUTDOWN) def handle_shutdown(self, response, *args): self.ready_to_close = True @send_notification(method=LSPRequestTypes.EXIT) def exit(self): params = {} return params @handles(LSPRequestTypes.INITIALIZE) def process_server_capabilities(self, server_capabilites, *args): """ Register server capabilities and inform other plugins that it's available. """ # Update server capabilities with the info sent by the server. server_capabilites = server_capabilites['capabilities'] if isinstance(server_capabilites['textDocumentSync'], int): kind = server_capabilites['textDocumentSync'] server_capabilites['textDocumentSync'] = TEXT_DOCUMENT_SYNC_OPTIONS server_capabilites['textDocumentSync']['change'] = kind if server_capabilites['textDocumentSync'] is None: server_capabilites.pop('textDocumentSync') self.server_capabilites.update(server_capabilites) # The initialized notification needs to be the first request sent by # the client according to the protocol. self.initialized = True self.initialized_call() # This sends a DidChangeConfiguration request to pass to the server # the configurations set by the user in our config system. self.send_configurations(self.configurations) # Inform other plugins that the server is up. self.sig_initialize.emit(self.server_capabilites, self.language) @send_notification(method=LSPRequestTypes.INITIALIZED) def initialized_call(self): params = {} return params # ------ Settings queries -------------------------------- @property def support_multiple_workspaces(self): workspace_settings = self.server_capabilites['workspace'] return workspace_settings['workspaceFolders']['supported'] @property def support_workspace_update(self): workspace_settings = self.server_capabilites['workspace'] return workspace_settings['workspaceFolders']['changeNotifications']
class TestRunner(QObject): """ Class for running tests with py.test or nose. All communication back to the caller is done via signals. Attributes ---------- process : QProcess or None Process running the unit test suite. resultfilename : str Name of file in which test results are stored. Signals ------- sig_finished(list of TestResult, str) Emitted when test process finishes. First argument contains the test results, second argument contains the output of the test process. """ sig_finished = Signal(object, str) def __init__(self, widget, resultfilename=None): """ Construct test runner. Parameters ---------- widget : UnitTestWidget Unit test widget which constructs the test runner. resultfilename : str or None Name of file in which to store test results. If None, use default. """ QObject.__init__(self, widget) self.process = None if resultfilename is None: self.resultfilename = os.path.join(tempfile.gettempdir(), 'unittest.results') else: self.resultfilename = resultfilename def start(self, config, pythonpath): """ Start process which will run the unit test suite. The process is run in the working directory specified in 'config', with the directories in `pythonpath` added to the Python path for the test process. The test results are written to the file `self.resultfilename`. The standard output and error are also recorded. Once the process is finished, `self.finished()` will be called. Parameters ---------- config : TestConfig Unit test configuration. pythonpath : list of str List of directories to be added to the Python path Raises ------ RuntimeError If process failed to start. """ framework = config.framework wdir = config.wdir self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.MergedChannels) self.process.setWorkingDirectory(wdir) self.process.finished.connect(self.finished) if pythonpath is not None: env = [ to_text_string(_pth) for _pth in self.process.systemEnvironment() ] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) if framework == 'nose': executable = 'nosetests' p_args = [ '--with-xunit', '--xunit-file={}'.format(self.resultfilename) ] elif framework == 'py.test': executable = 'py.test' p_args = ['--junit-xml', self.resultfilename] else: raise ValueError('Unknown framework') if os.name == 'nt': executable += '.exe' try: os.remove(self.resultfilename) except OSError: pass self.process.start(executable, p_args) running = self.process.waitForStarted() if not running: raise RuntimeError def finished(self): """ Called when the unit test process has finished. This function reads the results and emits `sig_finished`. """ qbytearray = self.process.readAllStandardOutput() locale_codec = QTextCodec.codecForLocale() output = to_text_string(locale_codec.toUnicode(qbytearray.data())) testresults = self.load_data() self.sig_finished.emit(testresults, output) def kill_if_running(self): """Kill testing process if it is running.""" if self.process and self.process.state() == QProcess.Running: self.process.kill() def load_data(self): """ Read and parse unit test results. This function reads the unit test results from the file with name `self.resultfilename` and parses them. The file should contain the test results in JUnitXML format. Returns ------- list of TestResult Unit test results. """ try: data = etree.parse(self.resultfilename).getroot() except OSError: data = [] testresults = [] for testcase in data: category = Category.OK status = 'ok' name = '{0}.{1}'.format(testcase.get('classname'), testcase.get('name')) message = '' time = float(testcase.get('time')) extras = [] for child in testcase: if child.tag in ('error', 'failure', 'skipped'): if child.tag == 'skipped': category = Category.SKIP else: category = Category.FAIL status = child.tag type_ = child.get('type') message = child.get('message', default='') if type_ and message: message = '{0}: {1}'.format(type_, message) elif type_: message = type_ if child.text: extras.append(child.text) elif child.tag in ('system-out', 'system-err'): if child.tag == 'system-out': heading = _('Captured stdout') else: heading = _('Captured stderr') contents = child.text.rstrip('\n') extras.append('----- {} -----\n{}'.format( heading, contents)) extra_text = '\n\n'.join(extras) testresults.append( TestResult(category, status, name, message, time, extra_text)) return testresults
class TerminalMainWidget(PluginMainWidget): """ Terminal plugin main widget. """ MAX_SERVER_CONTACT_RETRIES = 40 URL_ISSUES = ' https://github.com/spyder-ide/spyder-terminal/issues' # --- Signals # ------------------------------------------------------------------------ sig_server_is_ready = Signal() """ This signal is emitted when the server is ready to connect. """ def __init__(self, name, plugin, parent): """Widget constructor.""" self.terms = [] super().__init__(name, plugin, parent) # Attributes self.tab_widget = None self.menu_actions = None self.server_retries = 0 self.server_ready = False self.font = None self.port = select_port(default_port=8071) self.stdout_file = None self.stderr_file = None if get_debug_level() > 0: self.stdout_file = osp.join(os.getcwd(), 'spyder_terminal_out.log') self.stderr_file = osp.join(os.getcwd(), 'spyder_terminal_err.log') self.project_path = None self.current_file_path = None self.current_cwd = os.getcwd() # Widgets self.main = parent self.find_widget = FindTerminal(self) self.find_widget.hide() layout = QVBoxLayout() # Tab Widget self.tabwidget = Tabs(self, rename_tabs=True) self.tabwidget.currentChanged.connect(self.refresh_plugin) self.tabwidget.move_data.connect(self.move_tab) self.tabwidget.set_close_function(self.close_term) if (hasattr(self.tabwidget, 'setDocumentMode') and not sys.platform == 'darwin'): # Don't set document mode to true on OSX because it generates # a crash when the console is detached from the main window # Fixes Issue 561 self.tabwidget.setDocumentMode(True) layout.addWidget(self.tabwidget) layout.addWidget(self.find_widget) self.setLayout(layout) css = qstylizer.style.StyleSheet() css.QTabWidget.pane.setValues(border=0) self.setStyleSheet(css.toString()) self.__wait_server_to_start() # ---- PluginMainWidget API # ------------------------------------------------------------------------ def get_focus_widget(self): """ Set focus on current selected terminal. Return the widget to give focus to when this plugin's dockwidget is raised on top-level. """ term = self.tabwidget.currentWidget() if term is not None: return term.view def get_title(self): """Define the title of the widget.""" return _('Terminal') def setup(self): """Perform the setup of plugin's main menu and signals.""" self.cmd = find_program(self.get_conf('shell')) server_args = [ sys.executable, '-m', 'spyder_terminal.server', '--port', str(self.port), '--shell', self.cmd] self.server = QProcess(self) env = self.server.processEnvironment() for var in os.environ: env.insert(var, os.environ[var]) self.server.setProcessEnvironment(env) self.server.errorOccurred.connect(self.handle_process_errors) self.server.setProcessChannelMode(QProcess.SeparateChannels) if self.stdout_file and self.stderr_file: self.server.setStandardOutputFile(self.stdout_file) self.server.setStandardErrorFile(self.stderr_file) self.server.start(server_args[0], server_args[1:]) self.color_scheme = self.get_conf('appearance', 'ui_theme') self.theme = self.get_conf('appearance', 'selected') # Menu menu = self.get_options_menu() # Actions new_terminal_toolbar_action = self.create_toolbutton( TerminalMainWidgetToolbarSections.New, text=_("Open a new terminal"), icon=self.create_icon('expand_selection'), triggered=lambda: self.create_new_term(), ) self.add_corner_widget( TerminalMainWidgetCornerToolbar.NewTerm, new_terminal_toolbar_action) new_terminal_cwd = self.create_action( TerminalMainWidgetActions.NewTerminalForCWD, text=_("New terminal in current working directory"), tip=_("Sets the pwd at the current working directory"), triggered=lambda: self.create_new_term(), shortcut_context='terminal', register_shortcut=True) self.new_terminal_project = self.create_action( TerminalMainWidgetActions.NewTerminalForProject, text=_("New terminal in current project"), tip=_("Sets the pwd at the current project directory"), triggered=lambda: self.create_new_term(path=self.project_path)) new_terminal_file = self.create_action( TerminalMainWidgetActions.NewTerminalForFile, text=_("New terminal in current Editor file"), tip=_("Sets the pwd at the directory that contains the current " "opened file"), triggered=lambda: self.create_new_term( path=self.current_file_path)) rename_tab_action = self.create_action( TerminalMainWidgetActions.RenameTab, text=_("Rename terminal"), triggered=lambda: self.tab_name_editor()) # Context menu actions self.create_action( TerminalMainWidgetActions.Copy, text=_('Copy text'), icon=self.create_icon('editcopy'), shortcut_context='terminal', triggered=lambda: self.copy(), register_shortcut=True) self.create_action( TerminalMainWidgetActions.Paste, text=_('Paste text'), icon=self.create_icon('editpaste'), shortcut_context='terminal', triggered=lambda: self.paste(), register_shortcut=True) self.create_action( TerminalMainWidgetActions.Clear, text=_('Clear terminal'), shortcut_context='terminal', triggered=lambda: self.clear(), register_shortcut=True) self.create_action( TerminalMainWidgetActions.ZoomIn, text=_('Zoom in'), shortcut_context='terminal', triggered=lambda: self.increase_font(), register_shortcut=True) self.create_action( TerminalMainWidgetActions.ZoomOut, text=_('Zoom out'), shortcut_context='terminal', triggered=lambda: self.decrease_font(), register_shortcut=True) # Create context menu self.create_menu(TermViewMenus.Context) # Add actions to options menu for item in [new_terminal_cwd, self.new_terminal_project, new_terminal_file]: self.add_item_to_menu( item, menu=menu, section=TerminalMainWidgetMenuSections.New) self.add_item_to_menu( rename_tab_action, menu=menu, section=TerminalMainWidgetMenuSections.TabActions) def update_actions(self): """Setup and update the actions in the options menu.""" if self.project_path is None: self.new_terminal_project.setEnabled(False) # ------ Private API ------------------------------------------ def copy(self): if self.get_focus_widget(): self.get_focus_widget().copy() def paste(self): if self.get_focus_widget(): self.get_focus_widget().paste() def clear(self): if self.get_focus_widget(): self.get_focus_widget().clear() def increase_font(self): if self.get_focus_widget(): self.get_focus_widget().increase_font() def decrease_font(self): if self.get_focus_widget(): self.get_focus_widget().decrease_font() def __wait_server_to_start(self): try: code = requests.get('http://127.0.0.1:{0}'.format( self.port)).status_code except: code = 500 if self.server_retries == self.MAX_SERVER_CONTACT_RETRIES: QMessageBox.critical(self, _('Spyder Terminal Error'), _("Terminal server could not be located at " '<a href="http://127.0.0.1:{0}">' 'http://127.0.0.1:{0}</a>,' ' please restart Spyder on debugging mode ' "and open an issue with the contents of " "<tt>{1}</tt> and <tt>{2}</tt> " "files at {3}.").format(self.port, self.stdout_file, self.stderr_file, self.URL_ISSUES), QMessageBox.Ok) elif code != 200: self.server_retries += 1 QTimer.singleShot(250, self.__wait_server_to_start) elif code == 200: self.sig_server_is_ready.emit() self.server_ready = True self.create_new_term(give_focus=False) # ------ Plugin API -------------------------------- def update_font(self, font): """Update font from Preferences.""" self.font = font for term in self.terms: term.set_font(font.family()) def on_close(self, cancelable=False): """Perform actions before parent main window is closed.""" for term in self.terms: term.close() self.server.kill() return True def refresh_plugin(self): """Refresh tabwidget.""" term = None if self.tabwidget.count(): term = self.tabwidget.currentWidget() term.view.setFocus() else: term = None @on_conf_change def apply_plugin_settings(self, options): """Apply the config settings.""" term_options = {} for option in options: if option == 'color_scheme_name': term_options[option] = option else: term_options[option] = self.get_conf(option) for term in self.get_terms(): term.apply_settings(term_options) # ------ Public API (for terminals) ------------------------- def get_terms(self): """Return terminal list.""" return [cl for cl in self.terms if isinstance(cl, TerminalWidget)] def get_current_term(self): """Return the currently selected terminal.""" try: terminal = self.tabwidget.currentWidget() except AttributeError: terminal = None if terminal is not None: return terminal def create_new_term(self, name=None, give_focus=True, path=None): """Add a new terminal tab.""" if path is None: path = self.current_cwd if self.project_path is not None: path = self.project_path path = path.replace('\\', '/') term = TerminalWidget( self, self.port, path=path, font=self.font.family(), theme=self.theme, color_scheme=self.color_scheme) self.add_tab(term) term.terminal_closed.connect(lambda: self.close_term(term=term)) def close_term(self, index=None, term=None): """Close a terminal tab.""" if not self.tabwidget.count(): return if term is not None: index = self.tabwidget.indexOf(term) if index is None and term is None: index = self.tabwidget.currentIndex() if index is not None: term = self.tabwidget.widget(index) if term: term.close() self.tabwidget.removeTab(self.tabwidget.indexOf(term)) if term in self.terms: self.terms.remove(term) if self.tabwidget.count() == 0: self.create_new_term() def set_project_path(self, path): """Refresh current project path.""" self.project_path = path self.new_terminal_project.setEnabled(True) def set_current_opened_file(self, path): """Get path of current opened file in editor.""" self.current_file_path = osp.dirname(path) def unset_project_path(self): """Refresh current project path.""" self.project_path = None self.new_terminal_project.setEnabled(False) @Slot(str) def set_current_cwd(self, cwd): """Update current working directory.""" self.current_cwd = cwd def server_is_ready(self): """Return server status.""" return self.server_ready def search_next(self, text, case=False, regex=False, word=False):
class ProcessWorker(QObject): """Conda worker based on a QProcess for non blocking UI.""" sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs=None): """Conda worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. parse : bool (optional) Parse json from output. pip : bool (optional) Define as a pip command. callback : func (optional) If the process has a callback to process output from comd_list. extra_kwargs : dict Arguments for the callback. """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs if extra_kwargs else {} self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) try: json_stdout = [json.loads(s) for s in stdout.split('\x00') if s] json_stdout = json_stdout[-1] except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda' not in stderr.lower(): if stderr.strip() and self._conda: d = {'command': ' '.join(self._cmd_list), 'stderr': stderr} # print(d) # Should we just ignore any message here??? logger.warning('Conda command output on stderr', extra=d) elif stderr.strip() and self._pip: d = {'command': ' '.join(self._cmd_list)} # print(d) # Should we just ignore any message here??? logger.warning('Pip command output on stderr', extra=d) result[-1] = '' if self._parse and stdout: json_stdout = [] json_lines_output = stdout.split('\x00') for i, l in enumerate(json_lines_output): if l: try: json_stdout.append(json.loads(l)) except Exception as error: # An exception here could be product of: # - conda env installing pip stuff that is thrown to # stdout in non json form # - a post link script might be printing stuff to # stdout in non json format logger.warning( ('Problem parsing conda json output. ' 'Line {0}. Data - {1}. Error - {2}'.format( i, l, str(error))), ) if json_stdout: json_stdout = json_stdout[-1] result = json_stdout, result[-1] if 'exception_name' in result[0] or 'exception_type' in result[0]: if not isinstance(result[0], dict): result = {'error': str(result[0])}, None error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['message']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: d = {'stderr': result[-1]} logger.error('error', extra=d) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def start(self): """Start process.""" logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class ProcessWorker(QObject): """Conda worker based on a QProcess for non blocking UI.""" sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs=None): """Conda worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. parse : bool (optional) Parse json from output. pip : bool (optional) Define as a pip command. callback : func (optional) If the process has a callback to process output from comd_list. extra_kwargs : dict Arguments for the callback. """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs if extra_kwargs else {} self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda' not in stderr.lower(): if stderr.strip() and self._conda: logger.error('{0}:\nSTDERR:\n{1}\nEND'.format( ' '.join(self._cmd_list), stderr)) elif stderr.strip() and self._pip: logger.error("pip error: {}".format(self._cmd_list)) result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except Exception as error: result = stdout, str(error) if 'error' in result[0]: if not isinstance(result[0], dict): result = {'error': str(result[0])}, None error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def start(self): """Start process.""" logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class ExternalPythonShell(ExternalShellBase): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = ExtPythonShellWidget sig_pdb = Signal(str, int) open_file = Signal(str, int) started = Signal() sig_finished = Signal() def __init__(self, parent=None, fname=None, wdir=None, interact=False, debug=False, post_mortem=False, path=[], python_args='', arguments='', stand_alone=None, umr_enabled=True, umr_namelist=[], umr_verbose=True, pythonstartup=None, pythonexecutable=None, external_interpreter=False, monitor_enabled=True, mpl_backend=None, ets_backend='qt4', qt_api=None, merge_output_channels=False, colorize_sys_stderr=False, autorefresh_timeout=3000, autorefresh_state=True, light_background=True, menu_actions=None, show_buttons_inside=True, show_elapsed_time=True): assert qt_api in (None, 'pyqt', 'pyside', 'pyqt5') self.namespacebrowser = None # namespace browser widget! self.dialog_manager = DialogManager() self.stand_alone = stand_alone # stand alone settings (None: plugin) self.interact = interact self.pythonstartup = pythonstartup self.pythonexecutable = pythonexecutable self.external_interpreter = external_interpreter self.monitor_enabled = monitor_enabled self.mpl_backend = mpl_backend self.ets_backend = ets_backend self.qt_api = qt_api self.merge_output_channels = merge_output_channels self.colorize_sys_stderr = colorize_sys_stderr self.umr_enabled = umr_enabled self.umr_namelist = umr_namelist self.umr_verbose = umr_verbose self.autorefresh_timeout = autorefresh_timeout self.autorefresh_state = autorefresh_state self.namespacebrowser_button = None self.cwd_button = None self.env_button = None self.syspath_button = None self.terminate_button = None self.notification_thread = None ExternalShellBase.__init__(self, parent=parent, fname=fname, wdir=wdir, history_filename='history.py', light_background=light_background, menu_actions=menu_actions, show_buttons_inside=show_buttons_inside, show_elapsed_time=show_elapsed_time) if self.pythonexecutable is None: self.pythonexecutable = get_python_executable() self.python_args = None if python_args: assert is_text_string(python_args) self.python_args = python_args assert is_text_string(arguments) self.arguments = arguments self.connection_file = None self.shell.set_externalshell(self) self.toggle_globals_explorer(False) self.interact_action.setChecked(self.interact) self.debug_action.setChecked(debug) self.introspection_socket = None self.is_interpreter = fname is None if self.is_interpreter: self.terminate_button.hide() self.post_mortem_action.setChecked(post_mortem and not self.is_interpreter) # Additional python path list self.path = path self.shell.path = path def set_introspection_socket(self, introspection_socket): self.introspection_socket = introspection_socket if self.namespacebrowser is not None: settings = self.namespacebrowser.get_view_settings() communicate(introspection_socket, 'set_remote_view_settings()', settings=[settings]) def set_autorefresh_timeout(self, interval): if self.introspection_socket is not None: try: communicate(self.introspection_socket, "set_monitor_timeout(%d)" % interval) except socket.error: pass def closeEvent(self, event): self.quit_monitor() ExternalShellBase.closeEvent(self, event) def get_toolbar_buttons(self): ExternalShellBase.get_toolbar_buttons(self) if self.namespacebrowser_button is None \ and self.stand_alone is not None: self.namespacebrowser_button = create_toolbutton( self, text=_("Variables"), icon=ima.icon('dictedit'), tip=_("Show/hide global variables explorer"), toggled=self.toggle_globals_explorer, text_beside_icon=True) if self.terminate_button is None: self.terminate_button = create_toolbutton( self, text=_("Terminate"), icon=ima.icon('stop'), tip=_("Attempts to stop the process. The process\n" "may not exit as a result of clicking this\n" "button (it is given the chance to prompt\n" "the user for any unsaved files, etc).")) buttons = [] if self.namespacebrowser_button is not None: buttons.append(self.namespacebrowser_button) buttons += [ self.run_button, self.terminate_button, self.kill_button, self.options_button ] return buttons def get_options_menu(self): ExternalShellBase.get_options_menu(self) self.interact_action = create_action(self, _("Interact")) self.interact_action.setCheckable(True) self.debug_action = create_action(self, _("Debug")) self.debug_action.setCheckable(True) self.args_action = create_action(self, _("Arguments..."), triggered=self.get_arguments) self.post_mortem_action = create_action(self, _("Post Mortem Debug")) self.post_mortem_action.setCheckable(True) run_settings_menu = QMenu(_("Run settings"), self) add_actions(run_settings_menu, (self.interact_action, self.debug_action, self.args_action, self.post_mortem_action)) self.cwd_button = create_action( self, _("Working directory"), icon=ima.icon('DirOpenIcon'), tip=_("Set current working directory"), triggered=self.set_current_working_directory) self.env_button = create_action(self, _("Environment variables"), icon=ima.icon('environ'), triggered=self.show_env) self.syspath_button = create_action(self, _("Show sys.path contents"), icon=ima.icon('syspath'), triggered=self.show_syspath) actions = [ run_settings_menu, self.show_time_action, None, self.cwd_button, self.env_button, self.syspath_button ] if self.menu_actions is not None: actions += [None] + self.menu_actions return actions def is_interpreter(self): """Return True if shellwidget is a Python interpreter""" return self.is_interpreter def get_shell_widget(self): if self.stand_alone is None: return self.shell else: self.namespacebrowser = NamespaceBrowser(self) settings = self.stand_alone self.namespacebrowser.set_shellwidget(self) self.namespacebrowser.setup(**settings) self.namespacebrowser.sig_collapse.connect( lambda: self.toggle_globals_explorer(False)) # Shell splitter self.splitter = splitter = QSplitter(Qt.Vertical, self) self.splitter.splitterMoved.connect(self.splitter_moved) splitter.addWidget(self.shell) splitter.setCollapsible(0, False) splitter.addWidget(self.namespacebrowser) splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 0) splitter.setHandleWidth(5) splitter.setSizes([2, 1]) return splitter def get_icon(self): return ima.icon('python') def set_buttons_runnning_state(self, state): ExternalShellBase.set_buttons_runnning_state(self, state) self.interact_action.setEnabled(not state and not self.is_interpreter) self.debug_action.setEnabled(not state and not self.is_interpreter) self.args_action.setEnabled(not state and not self.is_interpreter) self.post_mortem_action.setEnabled(not state and not self.is_interpreter) if state: if self.arguments: argstr = _("Arguments: %s") % self.arguments else: argstr = _("No argument") else: argstr = _("Arguments...") self.args_action.setText(argstr) self.terminate_button.setVisible(not self.is_interpreter and state) if not state: self.toggle_globals_explorer(False) for btn in (self.cwd_button, self.env_button, self.syspath_button): btn.setEnabled(state and self.monitor_enabled) if self.namespacebrowser_button is not None: self.namespacebrowser_button.setEnabled(state) def set_namespacebrowser(self, namespacebrowser): """ Set namespace browser *widget* Note: this method is not used in stand alone mode """ self.namespacebrowser = namespacebrowser self.configure_namespacebrowser() def configure_namespacebrowser(self): """Connect the namespace browser to the notification thread""" if self.notification_thread is not None: self.notification_thread.refresh_namespace_browser.connect( self.namespacebrowser.refresh_table) signal = self.notification_thread.sig_process_remote_view signal.connect( lambda data: self.namespacebrowser.process_remote_view(data)) def create_process(self): self.shell.clear() self.process = QProcess(self) if self.merge_output_channels: self.process.setProcessChannelMode(QProcess.MergedChannels) else: self.process.setProcessChannelMode(QProcess.SeparateChannels) self.shell.wait_for_ready_read.connect( lambda: self.process.waitForReadyRead(250)) # Working directory if self.wdir is not None: self.process.setWorkingDirectory(self.wdir) #-------------------------Python specific------------------------------ # Python arguments p_args = ['-u'] if DEBUG >= 3: p_args += ['-v'] p_args += get_python_args(self.fname, self.python_args, self.interact_action.isChecked(), self.debug_action.isChecked(), self.arguments) env = [ to_text_string(_path) for _path in self.process.systemEnvironment() ] if self.pythonstartup: env.append('PYTHONSTARTUP=%s' % self.pythonstartup) #-------------------------Python specific------------------------------- # Post mortem debugging if self.post_mortem_action.isChecked(): env.append('SPYDER_EXCEPTHOOK=True') # Set standard input/output encoding for Python consoles # See http://stackoverflow.com/q/26312400/438386, specifically # the comments of Martijn Pieters env.append('PYTHONIOENCODING=UTF-8') # Monitor if self.monitor_enabled: env.append('SPYDER_SHELL_ID=%s' % id(self)) env.append('SPYDER_AR_TIMEOUT=%d' % self.autorefresh_timeout) env.append('SPYDER_AR_STATE=%r' % self.autorefresh_state) from spyder.widgets.externalshell import introspection introspection_server = introspection.start_introspection_server() introspection_server.register(self) notification_server = introspection.start_notification_server() self.notification_thread = notification_server.register(self) self.notification_thread.sig_pdb.connect( lambda fname, lineno: self.sig_pdb.emit(fname, lineno)) self.notification_thread.open_file.connect( lambda fname, lineno: self.open_file.emit(fname, lineno)) if self.namespacebrowser is not None: self.configure_namespacebrowser() env.append('SPYDER_I_PORT=%d' % introspection_server.port) env.append('SPYDER_N_PORT=%d' % notification_server.port) # External modules options env.append('ETS_TOOLKIT=%s' % self.ets_backend) if self.mpl_backend is not None: backends = {0: 'Automatic', 1: 'None', 2: 'TkAgg'} env.append('SPY_MPL_BACKEND=%s' % backends[self.mpl_backend]) if self.qt_api: env.append('QT_API=%s' % self.qt_api) env.append('COLORIZE_SYS_STDERR=%s' % self.colorize_sys_stderr) # # Socket-based alternative (see input hook in sitecustomize.py): # if self.install_qt_inputhook: # from PyQt4.QtNetwork import QLocalServer # self.local_server = QLocalServer() # self.local_server.listen(str(id(self))) # User Module Deleter if self.is_interpreter: env.append('UMR_ENABLED=%r' % self.umr_enabled) env.append('UMR_NAMELIST=%s' % ','.join(self.umr_namelist)) env.append('UMR_VERBOSE=%r' % self.umr_verbose) env.append('MATPLOTLIB_ION=True') else: if self.interact: env.append('MATPLOTLIB_ION=True') else: env.append('MATPLOTLIB_ION=False') # External interpreter env.append('EXTERNAL_INTERPRETER=%r' % self.external_interpreter) # Add sitecustomize path to path list pathlist = [] spy_path = get_module_source_path('spyder') sc_path = osp.join(spy_path, 'utils', 'site') pathlist.append(sc_path) # Adding Spyder path pathlist += self.path # Adding path list to PYTHONPATH environment variable add_pathlist_to_PYTHONPATH(env, pathlist) #-------------------------Python specific------------------------------ self.process.readyReadStandardOutput.connect(self.write_output) self.process.readyReadStandardError.connect(self.write_error) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.sig_finished.connect(self.dialog_manager.close_all) self.terminate_button.clicked.connect(self.process.terminate) self.kill_button.clicked.connect(self.process.kill) #-------------------------Python specific------------------------------ # Fixes for our Mac app: # 1. PYTHONPATH and PYTHONHOME are set while bootstrapping the app, # but their values are messing sys.path for external interpreters # (e.g. EPD) so we need to remove them from the environment. # 2. Set PYTHONPATH again but without grabbing entries defined in the # environment (Fixes Issue 1321) # 3. Remove PYTHONOPTIMIZE from env so that we can have assert # statements working with our interpreters (See Issue 1281) if running_in_mac_app(): if MAC_APP_NAME not in self.pythonexecutable: env = [p for p in env if not (p.startswith('PYTHONPATH') or \ p.startswith('PYTHONHOME'))] # 1. add_pathlist_to_PYTHONPATH(env, pathlist, drop_env=True) # 2. env = [p for p in env if not p.startswith('PYTHONOPTIMIZE')] # 3. processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.process.start(self.pythonexecutable, p_args) #-------------------------Python specific------------------------------ running = self.process.waitForStarted(3000) self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("A Python console failed to start!")) else: self.shell.setFocus() self.started.emit() return self.process def finished(self, exit_code, exit_status): """Reimplement ExternalShellBase method""" ExternalShellBase.finished(self, exit_code, exit_status) self.introspection_socket = None #============================================================================== # Input/Output #============================================================================== def write_error(self): if os.name == 'nt': #---This is apparently necessary only on Windows (not sure though): # emptying standard output buffer before writing error output self.process.setReadChannel(QProcess.StandardOutput) if self.process.waitForReadyRead(1): self.write_output() self.shell.write_error(self.get_stderr()) QApplication.processEvents() def send_to_process(self, text): if not self.is_running(): return if not is_text_string(text): text = to_text_string(text) if self.mpl_backend == 0 and os.name == 'nt' and \ self.introspection_socket is not None: communicate(self.introspection_socket, "toggle_inputhook_flag(True)") # # Socket-based alternative (see input hook in sitecustomize.py): # while self.local_server.hasPendingConnections(): # self.local_server.nextPendingConnection().write('go!') if any([text == cmd for cmd in ['%ls', '%pwd', '%scientific']]) or \ any([text.startswith(cmd) for cmd in ['%cd ', '%clear ']]): text = 'evalsc(r"%s")\n' % text if not text.endswith('\n'): text += '\n' self.process.write(to_binary_string(text, 'utf8')) self.process.waitForBytesWritten(-1) # Eventually write prompt faster (when hitting Enter continuously) # -- necessary/working on Windows only: if os.name == 'nt': self.write_error() def keyboard_interrupt(self): if self.introspection_socket is not None: communicate(self.introspection_socket, "thread.interrupt_main()") def quit_monitor(self): if self.introspection_socket is not None: try: write_packet(self.introspection_socket, "thread.exit()") except socket.error: pass #============================================================================== # Globals explorer #============================================================================== @Slot(bool) def toggle_globals_explorer(self, state): if self.stand_alone is not None: self.splitter.setSizes([1, 1 if state else 0]) self.namespacebrowser_button.setChecked(state) if state and self.namespacebrowser is not None: self.namespacebrowser.refresh_table() def splitter_moved(self, pos, index): self.namespacebrowser_button.setChecked(self.splitter.sizes()[1]) #============================================================================== # Misc. #============================================================================== @Slot() def set_current_working_directory(self): """Set current working directory""" cwd = self.shell.get_cwd() self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), cwd) if directory: self.shell.set_cwd(directory) self.redirect_stdio.emit(True) @Slot() def show_env(self): """Show environment variables""" get_func = self.shell.get_env set_func = self.shell.set_env self.dialog_manager.show(RemoteEnvDialog(get_func, set_func)) @Slot() def show_syspath(self): """Show sys.path contents""" editor = CollectionsEditor() editor.setup(self.shell.get_syspath(), title="sys.path", readonly=True, width=600, icon=ima.icon('syspath')) self.dialog_manager.show(editor)
class CondaProcess(QObject): """Conda API modified to work with QProcess instead of popen.""" # Signals sig_finished = Signal(str, object, str) sig_partial = Signal(str, object, str) sig_started = Signal() ENCODING = "ascii" ROOT_PREFIX = None def __init__(self, parent): QObject.__init__(self, parent) self._parent = parent self._output = None self._partial = None self._stdout = None self._error = None self._parse = False self._function_called = "" self._name = None self._process = QProcess() self.set_root_prefix() # Signals self._process.finished.connect(self._call_conda_ready) self._process.readyReadStandardOutput.connect(self._call_conda_partial) # --- Helpers # ------------------------------------------------------------------------- def _is_running(self): return self._process.state() != QProcess.NotRunning def _is_not_running(self): return self._process.state() == QProcess.NotRunning def _call_conda_partial(self): """ """ stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(stdout, CondaProcess.ENCODING) stderr = self._process.readAllStandardError() stderr = handle_qbytearray(stderr, CondaProcess.ENCODING) if self._parse: try: self._output = json.loads(stdout) except Exception: # Result is a partial json. Can only be parsed when finished self._output = stdout else: self._output = stdout self._partial = self._output self._stdout = self._output self._error = stderr self.sig_partial.emit(self._function_called, self._partial, self._error) def _call_conda_ready(self): """Function called when QProcess in _call_conda finishes task.""" function = self._function_called if self._stdout is None: stdout = to_text_string(self._process.readAllStandardOutput(), encoding=CondaProcess.ENCODING) else: stdout = self._stdout if self._error is None: stderr = to_text_string(self._process.readAllStandardError(), encoding=CondaProcess.ENCODING) else: stderr = self._error if function == "get_conda_version": pat = re.compile(r"conda:?\s+(\d+\.\d\S+|unknown)") m = pat.match(stderr.strip()) if m is None: m = pat.match(stdout.strip()) if m is None: raise Exception("output did not match: {0}".format(stderr)) self._output = m.group(1) elif function == "config_path": result = self._output self._output = result["rc_path"] elif function == "config_get": result = self._output self._output = result["get"] elif ( function == "config_delete" or function == "config_add" or function == "config_set" or function == "config_remove" ): result = self._output self._output = result.get("warnings", []) elif function == "pip": result = [] lines = self._output.split("\n") for line in lines: if "<pip>" in line: temp = line.split()[:-1] + ["pip"] result.append("-".join(temp)) self._output = result if stderr.strip(): self._error = stderr self._parse = False self.sig_finished.emit(self._function_called, self._output, self._error) def _get_prefix_envname_helper(self, name, envs): """ """ if name == "root": return CondaProcess.ROOT_PREFIX for prefix in envs: if basename(prefix) == name: return prefix return None def _abspath(self, abspath): """ """ if abspath: if sys.platform == "win32": python = join(CondaProcess.ROOT_PREFIX, "python.exe") conda = join(CondaProcess.ROOT_PREFIX, "Scripts", "conda-script.py") else: python = join(CondaProcess.ROOT_PREFIX, "bin/python") conda = join(CondaProcess.ROOT_PREFIX, "bin/conda") cmd_list = [python, conda] else: # Just use whatever conda is on the path cmd_list = ["conda"] return cmd_list def _call_conda(self, extra_args, abspath=True): """ """ if self._is_not_running(): cmd_list = self._abspath(abspath) cmd_list.extend(extra_args) self._error, self._output = None, None self._process.start(cmd_list[0], cmd_list[1:]) self.sig_started.emit() def _call_and_parse(self, extra_args, abspath=True): """ """ self._parse = True self._call_conda(extra_args, abspath=abspath) def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()): """ """ cmd_list = [] if kwargs.get("override_channels", False) and "channel" not in kwargs: raise TypeError("conda search: override_channels requires channel") if "env" in kwargs: cmd_list.extend(["--name", kwargs.pop("env")]) if "prefix" in kwargs: cmd_list.extend(["--prefix", kwargs.pop("prefix")]) if "channel" in kwargs: channel = kwargs.pop("channel") if isinstance(channel, str): cmd_list.extend(["--channel", channel]) else: cmd_list.append("--channel") cmd_list.extend(channel) for key in keys: if key in kwargs and kwargs[key]: cmd_list.append("--" + key.replace("_", "-")) return cmd_list # --- Public api # ------------------------------------------------------------------------ def linked(self, prefix): """ Return the (set of canonical names) of linked packages in `prefix`. """ if not isdir(prefix): raise Exception("no such directory: {0}".format(prefix)) meta_dir = join(prefix, "conda-meta") if not isdir(meta_dir): # we might have nothing in linked (and no conda-meta directory) result = set() result = set(fn[:-5] for fn in os.listdir(meta_dir) if fn.endswith(".json")) self.sig_finished.emit("linked", result, "") return result def split_canonical_name(self, cname): """ Split a canonical package name into (name, version, build) strings. """ result = tuple(cname.rsplit("-", 2)) self.sig_finished.emit("split_canonical_name", result, "") return result def set_root_prefix(self, prefix=None): """ Set the prefix to the root environment (default is /opt/anaconda). """ if prefix: CondaProcess.ROOT_PREFIX = prefix else: # Find conda instance, and then use info() to get 'root_prefix' if CondaProcess.ROOT_PREFIX is None: info = self.info(abspath=False) CondaProcess.ROOT_PREFIX = info["root_prefix"] def get_conda_version(self): """ Return the version of conda being used (invoked) as a string. """ if self._is_not_running(): self._function_called = "get_conda_version" self._call_conda(["--version"]) def get_envs(self): """ Return all of the (named) environment (this does not include the root environment), as a list of absolute path to their prefixes. """ if self._is_not_running(): info = self.info() result = info["envs"] self.sig_finished.emit("get_envs", result, "") return result def get_prefix_envname(self, name): """ Given the name of an environment return its full prefix path, or None if it cannot be found. """ if self._is_not_running(): if name == "root": pref = CondaProcess.ROOT_PREFIX for prefix in self.get_envs(): if basename(prefix) == name: pref = prefix break self.sig_finished.emit("get_prefix_envname", pref, "") return pref def info(self, abspath=True): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ if self._is_not_running(): qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(["info", "--json"]) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) self.sig_finished.emit("info", str(info), "") return info def package_info(self, package, abspath=True): """ Return a dictionary with package information. Structure is { 'package_name': [{ 'depends': list, 'version': str, 'name': str, 'license': str, 'fn': ..., ... }] } """ if self._is_not_running(): self._function_called = "package_info" self._call_and_parse(["info", package, "--json"], abspath=abspath) def search(self, regex=None, spec=None, **kwargs): """ Search for packages. """ cmd_list = ["search", "--json"] if regex and spec: raise TypeError("conda search: only one of regex or spec allowed") if regex: cmd_list.append(regex) if spec: cmd_list.extend(["--spec", spec]) if "platform" in kwargs: platform = kwargs.pop("platform") platforms = ("win-32", "win-64", "osx-64", "linux-32", "linux-64") if platform not in platforms: raise TypeError("conda search: platform must be one of " + ", ".join(platforms)) cmd_list.extend(["--platform", platform]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ("canonical", "unknown", "use_index_cache", "outdated", "override_channels") ) ) if self._is_not_running(): self._function_called = "search" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def share(self, prefix): """ Create a "share package" of the environment located in `prefix`, and return a dictionary with (at least) the following keys: - 'path': the full path to the created package - 'warnings': a list of warnings This file is created in a temp directory, and it is the callers responsibility to remove this directory (after the file has been handled in some way). """ if self._is_not_running: self._function_called = "share" self._call_and_parse(["share", "--json", "--prefix", prefix]) def create(self, name=None, path=None, pkgs=None): """ Create an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError("must specify a list of one or more packages to " "install into new environment") cmd_list = ["create", "--yes", "--quiet"] if name: ref = name search = [os.path.join(d, name) for d in self.info()["envs_dirs"]] cmd_list = ["create", "--yes", "--quiet", "--name", name] elif path: ref = path search = [path] cmd_list = ["create", "--yes", "--quiet", "--prefix", path] else: raise TypeError("must specify either an environment name or a path" " for new environment") if any(os.path.exists(path) for path in search): raise CondaEnvExistsError("Conda environment [%s] already exists" % ref) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = "create" self._call_conda(cmd_list) def install(self, name=None, path=None, pkgs=None, dep=True): """ Install packages into an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError("must specify a list of one or more packages to " "install into existing environment") cmd_list = ["install", "--yes", "--json", "--force-pscheck"] if name: cmd_list.extend(["--name", name]) elif path: cmd_list.extend(["--prefix", path]) else: # just install into the current environment, whatever that is pass cmd_list.extend(pkgs) if not dep: cmd_list.extend(["--no-deps"]) if self._is_not_running: self._function_called = "install" self._call_conda(cmd_list) def update(self, *pkgs, **kwargs): """ Update package(s) (in an environment) by name. """ cmd_list = ["update", "--json", "--quiet", "--yes"] if not pkgs and not kwargs.get("all"): raise TypeError( "Must specify at least one package to update, \ or all=True." ) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ( "dry_run", "no_deps", "override_channels", "no_pin", "force", "all", "use_index_cache", "use_local", "alt_hint", ), ) ) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = "update" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def remove(self, *pkgs, **kwargs): """ Remove a package (from an environment) by name. Returns { success: bool, (this is always true), (other information) } """ cmd_list = ["remove", "--json", "--quiet", "--yes", "--force-pscheck"] if not pkgs and not kwargs.get("all"): raise TypeError( "Must specify at least one package to remove, \ or all=True." ) if kwargs.get("name") and kwargs.get("path"): raise TypeError("conda remove: At most one of name, path allowed") if kwargs.get("name"): cmd_list.extend(["--name", kwargs.pop("name")]) if kwargs.get("path"): cmd_list.extend(["--prefix", kwargs.pop("path")]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ("dry_run", "features", "override_channels", "no_pin", "force", "all") ) ) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = "remove" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def remove_environment(self, name=None, path=None, **kwargs): """ Remove an environment entirely. See ``remove``. """ if self._is_not_running: self._function_called = "remove_environment" self.remove(name=name, path=path, all=True, **kwargs) def clone_environment(self, clone, name=None, path=None, **kwargs): """ Clone the environment ``clone`` into ``name`` or ``path``. """ cmd_list = ["create", "--json", "--quiet"] if (name and path) or not (name or path): raise TypeError( "conda clone_environment: exactly one of name or \ path required" ) if name: cmd_list.extend(["--name", name]) if path: cmd_list.extend(["--prefix", path]) cmd_list.extend(["--clone", clone]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ( "dry_run", "unknown", "use_index_cache", "use_local", "no_pin", "force", "all", "channel", "override_channels", "no_default_packages", ), ) ) if self._is_not_running(): self._function_called = "clone_environment" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def clone(self, path, prefix): """ Clone a "share package" (located at `path`) by creating a new environment at `prefix`, and return a dictionary with (at least) the following keys: - 'warnings': a list of warnings The directory `path` is located in, should be some temp directory or some other directory OUTSIDE /opt/anaconda. After calling this function, the original file (at `path`) may be removed (by the caller of this function). The return object is a list of warnings. """ if self._process.state() == QProcess.NotRunning: self._function_called = "clone" self._call_and_parse(["clone", "--json", "--prefix", prefix, path]) def _setup_config_from_kwargs(kwargs): cmd_list = ["--json", "--force"] if "file" in kwargs: cmd_list.extend(["--file", kwargs["file"]]) if "system" in kwargs: cmd_list.append("--system") return cmd_list def config_path(self, **kwargs): """ Get the path to the config file. """ cmd_list = ["config", "--get"] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_path" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_get(self, *keys, **kwargs): """ Get the values of configuration keys. Returns a dictionary of values. Note, the key may not be in the dictionary if the key wasn't set in the configuration file. """ cmd_list = ["config", "--get"] cmd_list.extend(keys) cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_get" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_set(self, key, value, **kwargs): """ Set a key to a (bool) value. Returns a list of warnings Conda may have emitted. """ cmd_list = ["config", "--set", key, str(value)] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_set" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_add(self, key, value, **kwargs): """ Add a value to a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ["config", "--add", key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_add" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_remove(self, key, value, **kwargs): """ Remove a value from a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ["config", "--remove", key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_remove" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_delete(self, key, **kwargs): """ Remove a key entirely. Returns a list of warnings Conda may have emitted. """ cmd_list = ["config", "--remove-key", key] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_delete" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def run(self, command, abspath=True): """ Launch the specified app by name or full package name. Returns a dictionary containing the key "fn", whose value is the full package (ending in ``.tar.bz2``) of the app. """ cmd_list = ["run", "--json", command] if self._is_not_running: self._function_called = "run" self._call_and_parse(cmd_list, abspath=abspath) # --- Additional methods not in conda-api # ------------------------------------------------------------------------ def pip(self, name): """Get list of pip installed packages.""" cmd_list = ["list", "-n", name] if self._is_not_running: self._function_called = "pip" self._call_conda(cmd_list) def dependencies(self, name=None, path=None, pkgs=None, dep=True): """ Install packages into an environment either by name or path with a specified set of packages. """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError("must specify a list of one or more packages to " "install into existing environment") cmd_list = ["install", "--dry-run", "--json"] cmd_list = ["install", "--dry-run", "--json", "--force-pscheck"] if not dep: cmd_list.extend(["--no-deps"]) if name: cmd_list.extend(["--name", name]) elif path: cmd_list.extend(["--prefix", path]) else: pass cmd_list.extend(pkgs) if self._is_not_running: self._function_called = "install_dry" self._call_and_parse(cmd_list)
class PylintWidget(QWidget): """ Pylint widget """ DATAPATH = get_conf_path('pylint.results') VERSION = '1.1.0' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100): QWidget.__init__(self, parent) self.setWindowTitle("Pylint") self.output = None self.error_output = None self.max_entries = max_entries self.rdata = [] if osp.isfile(self.DATAPATH): try: data = pickle.loads(open(self.DATAPATH, 'rb').read()) if data[0] == self.VERSION: self.rdata = data[1:] except (EOFError, ImportError): pass self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Analyze"), tip=_("Run analysis"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current analysis"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) self.filecombo.valid.connect(self.show_data) browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python file'), triggered=self.select_file) self.ratelabel = QLabel() self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Complete output"), triggered=self.show_log) self.treewidget = ResultsTree(self) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.ratelabel) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.treewidget) self.setLayout(layout) self.process = None self.set_running_state(False) self.show_data() if self.rdata: self.remove_obsolete_items() self.filecombo.addItems(self.get_filenames()) else: self.start_button.setEnabled(False) def analyze(self, filename): filename = to_text_string(filename) # filename is a QString instance self.kill_if_running() index, _data = self.get_data(filename) if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): self.start() @Slot() def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python file"), getcwd_or_home(), _("Python files") + " (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def remove_obsolete_items(self): """Removing obsolete items""" self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): return [filename for filename, _data in self.rdata] def get_data(self, filename): filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) self.save() def save(self): while len(self.rdata) > self.max_entries: self.rdata.pop(-1) pickle.dump([self.VERSION] + self.rdata, open(self.DATAPATH, 'wb'), 2) @Slot() def show_log(self): if self.output: TextEditor(self.output, title=_("Pylint output"), readonly=True, size=(700, 500)).exec_() @Slot() def start(self): filename = to_text_string(self.filecombo.currentText()) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(osp.dirname(filename)) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) self.output = '' self.error_output = '' plver = PYLINT_VER if plver is not None: p_args = ['-m', 'pylint', '--output-format=text'] if plver.split('.')[0] == '0': p_args += ['-i', 'yes'] else: # Option '-i' (alias for '--include-ids') was removed in pylint # 1.0 p_args += ["--msg-template='{msg_id}:{line:3d},"\ "{column}: {obj}: {msg}"] p_args += [osp.basename(filename)] else: p_args = [osp.basename(filename)] self.process.start(sys.executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(locale_codec.toUnicode(qba.data())) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) if not self.output: if self.error_output: QMessageBox.critical(self, _("Error"), self.error_output) print("pylint error:\n\n" + self.error_output, file=sys.stderr) return # Convention, Refactor, Warning, Error results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} txt_module = '************* Module ' module = '' # Should not be needed - just in case something goes wrong for line in self.output.splitlines(): if line.startswith(txt_module): # New module module = line[len(txt_module):] continue # Supporting option include-ids: ('R3873:' instead of 'R:') if not re.match('^[CRWE]+([0-9]{4})?:', line): continue i1 = line.find(':') if i1 == -1: continue msg_id = line[:i1] i2 = line.find(':', i1 + 1) if i2 == -1: continue line_nb = line[i1 + 1:i2].strip() if not line_nb: continue line_nb = int(line_nb.split(',')[0]) message = line[i2 + 1:] item = (module, line_nb, message, msg_id) results[line[0] + ':'].append(item) # Rate rate = None txt_rate = 'Your code has been rated at ' i_rate = self.output.find(txt_rate) if i_rate > 0: i_rate_end = self.output.find('/10', i_rate) if i_rate_end > 0: rate = self.output[i_rate + len(txt_rate):i_rate_end] # Previous run previous = '' if rate is not None: txt_prun = 'previous run: ' i_prun = self.output.find(txt_prun, i_rate_end) if i_prun > 0: i_prun_end = self.output.find('/10', i_prun) previous = self.output[i_prun + len(txt_prun):i_prun_end] filename = to_text_string(self.filecombo.currentText()) self.set_data(filename, (time.localtime(), rate, previous, results)) self.output = self.error_output + self.output self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return _index, data = self.get_data(filename) if data is None: text = _('Source code has not been rated yet.') self.treewidget.clear_results() date_text = '' else: datetime, rate, previous_rate, results = data if rate is None: text = _('Analysis did not succeed ' '(see output for more details).') self.treewidget.clear_results() date_text = '' else: text_style = "<span style=\'color: #444444\'><b>%s </b></span>" rate_style = "<span style=\'color: %s\'><b>%s</b></span>" prevrate_style = "<span style=\'color: #666666\'>%s</span>" color = "#FF0000" if float(rate) > 5.: color = "#22AA22" elif float(rate) > 3.: color = "#EE5500" text = _('Global evaluation:') text = (text_style % text) + (rate_style % (color, ('%s/10' % rate))) if previous_rate: text_prun = _('previous run:') text_prun = ' (%s %s/10)' % (text_prun, previous_rate) text += prevrate_style % text_prun self.treewidget.set_results(filename, results) date = to_text_string(time.strftime("%d %b %Y %H:%M", datetime), encoding='utf8') date_text = text_style % date self.ratelabel.setText(text) self.datelabel.setText(date_text)
class ExternalSystemShell(ExternalShellBase): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = TerminalWidget started = Signal() def __init__(self, parent=None, wdir=None, path=[], light_background=True, menu_actions=None, show_buttons_inside=True, show_elapsed_time=True): ExternalShellBase.__init__(self, parent=parent, fname=None, wdir=wdir, history_filename='.history', light_background=light_background, menu_actions=menu_actions, show_buttons_inside=show_buttons_inside, show_elapsed_time=show_elapsed_time) # Additional python path list self.path = path # For compatibility with the other shells that can live in the external # console self.is_ipykernel = False self.connection_file = None def get_icon(self): return ima.icon('cmdprompt') def finish_process(self): while not self.process.waitForFinished(100): self.process.kill() def create_process(self): self.shell.clear() self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.MergedChannels) # PYTHONPATH (in case we use Python in this terminal, e.g. py2exe) env = [ to_text_string(_path) for _path in self.process.systemEnvironment() ] processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) add_pathlist_to_PYTHONPATH(env, self.path) self.process.setProcessEnvironment(processEnvironment) # Working directory if self.wdir is not None: self.process.setWorkingDirectory(self.wdir) # Shell arguments if os.name == 'nt': p_args = ['/Q'] else: p_args = ['-i'] if self.arguments: p_args.extend(shell_split(self.arguments)) self.process.readyReadStandardOutput.connect(self.write_output) self.process.finished.connect(self.finished) self.kill_button.clicked.connect(self.process.kill) if os.name == 'nt': self.process.start('cmd.exe', p_args) else: # Using bash: self.process.start('bash', p_args) self.send_to_process('PS1="\\u@\\h:\\w> "\n') running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) else: self.shell.setFocus() self.started.emit() return self.process #=============================================================================== # Input/Output #=============================================================================== def transcode(self, qba): if os.name == 'nt': return to_text_string(CP850_CODEC.toUnicode(qba.data())) else: return ExternalShellBase.transcode(self, qba) def send_to_process(self, text): if not is_text_string(text): text = to_text_string(text) if text[:-1] in ["clear", "cls", "CLS"]: self.shell.clear() self.send_to_process(os.linesep) return if not text.endswith('\n'): text += '\n' if os.name == 'nt': self.process.write(text.encode('cp850')) else: self.process.write(LOCALE_CODEC.fromUnicode(text)) self.process.waitForBytesWritten(-1) def keyboard_interrupt(self): # This does not work on Windows: # (unfortunately there is no easy way to send a Ctrl+C to cmd.exe) self.send_ctrl_to_process('c')
class ProfilerWidget(PluginMainWidget): """ Profiler widget. """ ENABLE_SPINNER = True DATAPATH = get_conf_path('profiler.results') # --- Signals # ------------------------------------------------------------------------ sig_edit_goto_requested = Signal(str, int, str) """ This signal will request to open a file in a given row and column using a code editor. Parameters ---------- path: str Path to file. row: int Cursor starting row position. word: str Word to select on given row. """ sig_redirect_stdio_requested = Signal(bool) """ This signal is emitted to request the main application to redirect standard output/error when using Open/Save/Browse dialogs within widgets. Parameters ---------- redirect: bool Start redirect (True) or stop redirect (False). """ sig_started = Signal() """This signal is emitted to inform the profiling process has started.""" sig_finished = Signal() """This signal is emitted to inform the profile profiling has finished.""" def __init__(self, name=None, plugin=None, parent=None): super().__init__(name, plugin, parent) self.set_conf('text_color', MAIN_TEXT_COLOR) # Attributes self._last_wdir = None self._last_args = None self._last_pythonpath = None self.error_output = None self.output = None self.running = False self.text_color = self.get_conf('text_color') # Widgets self.process = None self.filecombo = PythonModulesComboBox( self, id_=ProfilerWidgetMainToolbarItems.FileCombo) self.datatree = ProfilerDataTree(self) self.datelabel = QLabel() self.datelabel.ID = ProfilerWidgetInformationToolbarItems.DateLabel # Layout layout = QVBoxLayout() layout.addWidget(self.datatree) self.setLayout(layout) # Signals self.datatree.sig_edit_goto_requested.connect( self.sig_edit_goto_requested) # --- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): return _('Profiler') def get_focus_widget(self): return self.datatree def setup(self): self.start_action = self.create_action( ProfilerWidgetActions.Run, text=_("Run profiler"), tip=_("Run profiler"), icon=self.create_icon('run'), triggered=self.run, ) browse_action = self.create_action( ProfilerWidgetActions.Browse, text='', tip=_('Select Python script'), icon=self.create_icon('fileopen'), triggered=lambda x: self.select_file(), ) self.log_action = self.create_action( ProfilerWidgetActions.ShowOutput, text=_("Output"), tip=_("Show program's output"), icon=self.create_icon('log'), triggered=self.show_log, ) self.collapse_action = self.create_action( ProfilerWidgetActions.Collapse, text=_('Collapse'), tip=_('Collapse one level up'), icon=self.create_icon('collapse'), triggered=lambda x=None: self.datatree.change_view(-1), ) self.expand_action = self.create_action( ProfilerWidgetActions.Expand, text=_('Expand'), tip=_('Expand one level down'), icon=self.create_icon('expand'), triggered=lambda x=None: self.datatree.change_view(1), ) self.save_action = self.create_action( ProfilerWidgetActions.SaveData, text=_("Save data"), tip=_('Save profiling data'), icon=self.create_icon('filesave'), triggered=self.save_data, ) self.load_action = self.create_action( ProfilerWidgetActions.LoadData, text=_("Load data"), tip=_('Load profiling data for comparison'), icon=self.create_icon('fileimport'), triggered=self.compare, ) self.clear_action = self.create_action( ProfilerWidgetActions.Clear, text=_("Clear comparison"), tip=_("Clear comparison"), icon=self.create_icon('editdelete'), triggered=self.clear, ) self.clear_action.setEnabled(False) # Main Toolbar toolbar = self.get_main_toolbar() for item in [self.filecombo, browse_action, self.start_action]: self.add_item_to_toolbar( item, toolbar=toolbar, section=ProfilerWidgetMainToolbarSections.Main, ) # Secondary Toolbar secondary_toolbar = self.create_toolbar( ProfilerWidgetToolbars.Information) for item in [ self.collapse_action, self.expand_action, self.create_stretcher( id_=ProfilerWidgetInformationToolbarItems.Stretcher1), self.datelabel, self.create_stretcher( id_=ProfilerWidgetInformationToolbarItems.Stretcher2), self.log_action, self.save_action, self.load_action, self.clear_action ]: self.add_item_to_toolbar( item, toolbar=secondary_toolbar, section=ProfilerWidgetInformationToolbarSections.Main, ) # Setup if not is_profiler_installed(): # This should happen only on certain GNU/Linux distributions # or when this a home-made Python build because the Python # profilers are included in the Python standard library for widget in (self.datatree, self.filecombo, self.start_action): widget.setDisabled(True) url = 'https://docs.python.org/3/library/profile.html' text = '%s <a href=%s>%s</a>' % (_('Please install'), url, _("the Python profiler modules")) self.datelabel.setText(text) def update_actions(self): if self.running: icon = self.create_icon('stop') else: icon = self.create_icon('run') self.start_action.setIcon(icon) self.start_action.setEnabled(bool(self.filecombo.currentText())) # --- Private API # ------------------------------------------------------------------------ def _kill_if_running(self): """Kill the profiling process if it is running.""" if self.process is not None: if self.process.state() == QProcess.Running: self.process.close() self.process.waitForFinished(1000) self.update_actions() def _finished(self, exit_code, exit_status): """ Parse results once the profiling process has ended. Parameters ---------- exit_code: int QProcess exit code. exit_status: str QProcess exit status. """ self.running = False self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output self.datelabel.setText('') self.show_data(justanalyzed=True) self.update_actions() def _read_output(self, error=False): """ Read otuput from QProcess. Parameters ---------- error: bool, optional Process QProcess output or error channels. Default is False. """ if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(qba.data(), encoding='utf-8') if error: self.error_output += text else: self.output += text # --- Public API # ------------------------------------------------------------------------ def save_data(self): """Save data.""" title = _("Save profiler result") filename, _selfilter = getsavefilename( self, title, getcwd_or_home(), _("Profiler result") + " (*.Result)", ) if filename: self.datatree.save_data(filename) def compare(self): """Compare previous saved run with last run.""" filename, _selfilter = getopenfilename( self, _("Select script to compare"), getcwd_or_home(), _("Profiler result") + " (*.Result)", ) if filename: self.datatree.compare(filename) self.show_data() self.clear_action.setEnabled(True) def clear(self): """Clear data in tree.""" self.datatree.compare(None) self.datatree.hide_diff_cols(True) self.show_data() self.clear_action.setEnabled(False) def analyze(self, filename, wdir=None, args=None, pythonpath=None): """ Start the profiling process. Parameters ---------- wdir: str Working directory path string. Default is None. args: list Arguments to pass to the profiling process. Default is None. pythonpath: str Python path string. Default is None. """ if not is_profiler_installed(): return self._kill_if_running() # TODO: storing data is not implemented yet # index, _data = self.get_data(filename) combo = self.filecombo items = [combo.itemText(idx) for idx in range(combo.count())] index = None if index is None and filename not in items: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self, filename=None): """ Select filename to profile. Parameters ---------- filename: str, optional Path to filename to profile. default is None. Notes ----- If no `filename` is provided an open filename dialog will be used. """ if filename is None: self.sig_redirect_stdio_requested.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python script"), getcwd_or_home(), _("Python scripts") + " (*.py ; *.pyw)") self.sig_redirect_stdio_requested.emit(True) if filename: self.analyze(filename) def show_log(self): """Show process output log.""" if self.output: output_dialog = TextEditor( self.output, title=_("Profiler output"), readonly=True, parent=self, ) output_dialog.resize(700, 500) output_dialog.exec_() def show_errorlog(self): """Show process error log.""" if self.error_output: output_dialog = TextEditor( self.error_output, title=_("Profiler output"), readonly=True, parent=self, ) output_dialog.resize(700, 500) output_dialog.exec_() def start(self, wdir=None, args=None, pythonpath=None): """ Start the profiling process. Parameters ---------- wdir: str Working directory path string. Default is None. args: list Arguments to pass to the profiling process. Default is None. pythonpath: str Python path string. Default is None. """ filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self._read_output) self.process.readyReadStandardError.connect( lambda: self._read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self._finished(ec, es)) self.process.finished.connect(self.stop_spinner) if pythonpath is not None: env = [ to_text_string(_pth) for _pth in self.process.systemEnvironment() ] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, __, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) processEnvironment.insert("PYTHONIOENCODING", "utf8") self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' self.running = True self.start_spinner() p_args = ['-m', 'cProfile', '-o', self.DATAPATH] if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): p_args.append(osp.normpath(filename).replace(os.sep, '/')) else: p_args.append(filename) if args: p_args.extend(shell_split(args)) executable = self.get_conf('executable', section='main_interpreter') self.process.start(executable, p_args) running = self.process.waitForStarted() if not running: QMessageBox.critical( self, _("Error"), _("Process failed to start"), ) self.update_actions() def stop(self): """Stop the running process.""" self.running = False self.process.close() self.process.waitForFinished(1000) self.stop_spinner() self.update_actions() def run(self): """Toggle starting or running the profiling process.""" if self.running: self.stop() else: self.start() def show_data(self, justanalyzed=False): """ Show analyzed data on results tree. Parameters ---------- justanalyzed: bool, optional Default is False. """ if not justanalyzed: self.output = None self.log_action.setEnabled(self.output is not None and len(self.output) > 0) self._kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.load_data(self.DATAPATH) self.datatree.show_tree() text_style = "<span style=\'color: %s\'><b>%s </b></span>" date_text = text_style % (self.text_color, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) self.datelabel.setText(date_text)
class InteractiveConsole(QTextEdit): """ An interactive console is a QTextEdit specialised to run a process interactively The user will see the process outputs and will be able to interact with the process by typing some text, this text will be forwarded to the process stdin. You can customize the colors using the following attributes: - stdout_color: color of the process stdout - stdin_color: color of the user inputs. Green by default - app_msg_color: color for custom application message ( process started, process finished) - stderr_color: color of the process stderr """ #: Signal emitted when the process has finished. process_finished = Signal(int) process_started = Signal() def __init__(self, parent=None): super(InteractiveConsole, self).__init__(parent) self.panels = PanelsManager(self) self.decorations = TextDecorationsManager(self) from pyqode.core.panels import SearchAndReplacePanel self.panels.append(SearchAndReplacePanel(), SearchAndReplacePanel.Position.TOP) self._stdout_col = QColor("#404040") self._app_msg_col = QColor("#4040FF") self._stdin_col = QColor("#22AA22") self._stderr_col = QColor("#FF0000") self._write_app_messages = True self._process_name = '' self.process = None self._args = None self._usr_buffer = "" self._clear_on_start = True self._merge_outputs = False self._running = False self._writer = self.write self._user_stop = False font = "monospace" if sys.platform == "win32": font = "Consolas" elif sys.platform == "darwin": font = 'Monaco' self._font_family = font self.setFont(QFont(font, 10)) self.setReadOnly(True) self._mask_user_input = False action = QAction(_('Copy'), self) action.setShortcut(QKeySequence.Copy) action.triggered.connect(self.copy) self.add_action(action) action = QAction(_('Paste'), self) action.setShortcut(QKeySequence.Paste) action.triggered.connect(self.paste) self.add_action(action) def showEvent(self, event): super(InteractiveConsole, self).showEvent(event) self.panels.refresh() def resizeEvent(self, e): super(InteractiveConsole, self).resizeEvent(e) self.panels.resize() def add_action(self, action): self.addAction(action) action.setShortcutContext(Qt.WidgetShortcut) def set_writer(self, writer): """ Changes the writer function to handle writing to the text edit. A writer function must have the following prototype: .. code-block:: python def write(text_edit, text, color) :param writer: write function as described above. """ if self._writer != writer and self._writer: self._writer = None if writer: self._writer = writer def _on_stdout(self): raw = self.process.readAllStandardOutput() txt = bytes(raw).decode(locale.getpreferredencoding()) self._writer(self, txt, self.stdout_color) def _on_stderr(self): txt = bytes(self.process.readAllStandardError()).decode( locale.getpreferredencoding()) _logger().debug('%s', txt) self._writer(self, txt, self.stderr_color) @property def exit_code(self): if self.is_running: return None exit_status = self.process.exitStatus() if exit_status == self.process.Crashed: exit_code = 139 else: exit_code = self.process.exitCode() return exit_code @property def write_app_messages(self): return self._write_app_messages @write_app_messages.setter def write_app_messages(self, value): self._write_app_messages = value @property def background_color(self): """ The console background color. Default is white. """ pal = self.palette() return pal.color(pal.Base) @background_color.setter def background_color(self, color): pal = self.palette() pal.setColor(pal.Base, color) pal.setColor(pal.Text, self.stdout_color) self.setPalette(pal) @property def stdout_color(self): """ STDOUT color. Default is black. """ return self._stdout_col @stdout_color.setter def stdout_color(self, color): self._stdout_col = color pal = self.palette() pal.setColor(pal.Text, self._stdout_col) self.setPalette(pal) @property def stderr_color(self): """ Color for stderr output if :attr:`pyqode.core.widgets.InteractiveConsole.merge_outputs` is False. Default is Red. """ return self._stderr_col @stderr_color.setter def stderr_color(self, color): self._stderr_col = color @property def stdin_color(self): """ STDIN color. Default is green. """ return self._stdin_col @stdin_color.setter def stdin_color(self, color): self._stdin_col = color @property def app_msg_color(self): """ Color of the application messages (e.g.: 'Process started', 'Process finished with status %d') """ return self._app_msg_col @app_msg_color.setter def app_msg_color(self, color): self._app_msg_col = color @property def clear_on_start(self): """ True to clear window when starting a new process. False to accumulate outputs. """ return self._clear_on_start @clear_on_start.setter def clear_on_start(self, value): self._clear_on_start = value @property def merge_outputs(self): """ Merge stderr with stdout. Default is False. If set to true, stderr and stdin will use the same color: stdin_color. """ return self._merge_outputs @merge_outputs.setter def merge_outputs(self, value): self._merge_outputs = value if value: self.process.setProcessChannelMode(QProcess.MergedChannels) else: self.process.setProcessChannelMode(QProcess.SeparateChannels) @property def is_running(self): """ Checks if the process is running. :return: """ return self._running @property def mask_user_input(self): return self._mask_user_input @mask_user_input.setter def mask_user_input(self, value): """ If true, user input will be replaced by "*". Could be useful to run commands as root. """ self._mask_user_input = value def closeEvent(self, *args, **kwargs): if self.process and self.process.state() == QProcess.Running: self.process.terminate() def start_process(self, process, args=None, cwd=None, env=None): """ Starts a process interactively. :param process: Process to run :type process: str :param args: List of arguments (list of str) :type args: list :param cwd: Working directory :type cwd: str :param env: environment variables (dict). """ self.setReadOnly(False) if env is None: env = {} if args is None: args = [] if not self._running: self.process = QProcess() self.process.finished.connect(self._on_process_finished) self.process.started.connect(self.process_started.emit) self.process.error.connect(self._write_error) self.process.readyReadStandardError.connect(self._on_stderr) self.process.readyReadStandardOutput.connect(self._on_stdout) if cwd: self.process.setWorkingDirectory(cwd) e = self.process.systemEnvironment() ev = QProcessEnvironment() for v in e: values = v.split('=') ev.insert(values[0], '='.join(values[1:])) for k, v in env.items(): ev.insert(k, v) self.process.setProcessEnvironment(ev) self._running = True self._process_name = process self._args = args if self._clear_on_start: self.clear() self._user_stop = False self._write_started() self.process.start(process, args) self.process.waitForStarted() else: _logger().warning('a process is already running') def stop_process(self): """ Stop the process (by killing it). """ if self.process is not None: self._user_stop = True self.process.kill() self.setReadOnly(True) self._running = False def get_user_buffer_as_bytes(self): """ Returns the user buffer as a bytes. """ return bytes(self._usr_buffer, locale.getpreferredencoding()) def keyPressEvent(self, event): ctrl = event.modifiers() & Qt.ControlModifier != 0 if not self.is_running or self.textCursor().hasSelection(): if event.key() == Qt.Key_C and ctrl: self.copy() return propagate_to_parent = True delete = event.key() in [Qt.Key_Backspace, Qt.Key_Delete] if delete and not self._usr_buffer: return if event.key() == Qt.Key_V and ctrl: # Paste to usr buffer text = QApplication.clipboard().text() self._usr_buffer += text self.setTextColor(self._stdin_col) if self._mask_user_input: text = len(text) * '*' self.insertPlainText(text) return if event.key() in [Qt.Key_Return, Qt.Key_Enter]: # send the user input to the child process if sys.platform == 'win32': self._usr_buffer += "\r" self._usr_buffer += "\n" self.process.write(self.get_user_buffer_as_bytes()) self._usr_buffer = "" else: if not delete and len(event.text()): txt = event.text() self._usr_buffer += txt if self._mask_user_input: txt = '*' self.setTextColor(self._stdin_col) self.insertPlainText(txt) propagate_to_parent = False elif delete: self._usr_buffer = self._usr_buffer[:len(self._usr_buffer) - 1] # text is inserted here, the text color must be defined before this # line if propagate_to_parent: super(InteractiveConsole, self).keyPressEvent(event) self.setTextColor(self._stdout_col) def _on_process_finished(self, exit_code, exit_status): if self is None: return self._running = False if not self._user_stop: if self._write_app_messages: self._writer( self, "\nProcess finished with exit code %d" % self.exit_code, self._app_msg_col) _logger().debug('process finished (exit_code=%r, exit_status=%r)', exit_code, exit_status) try: self.process_finished.emit(exit_code) except TypeError: # Signal must be bound to a QObject, not 'InteractiveConsole' pass else: self.setReadOnly(True) def _write_started(self): if not self._write_app_messages: return self._writer(self, "{0} {1}\n".format( self._process_name, " ".join(self._args)), self._app_msg_col) self._running = True def _write_error(self, error): if self is None: return if self._user_stop: self._writer(self, '\nProcess stopped by the user', self.app_msg_color) else: err = PROCESS_ERROR_STRING[error] self._writer(self, "Error: %s" % err, self.stderr_color) _logger().warn('process error: %s', err) self._running = False @staticmethod def write(text_edit, text, color): """ Default write function. Move the cursor to the end and insert text with the specified color. :param text_edit: QInteractiveConsole instance :type text_edit: pyqode.widgets.QInteractiveConsole :param text: Text to write :type text: str :param color: Desired text color :type color: QColor """ try: text_edit.moveCursor(QTextCursor.End) text_edit.setTextColor(color) text_edit.insertPlainText(text) text_edit.moveCursor(QTextCursor.End) except RuntimeError: pass def apply_color_scheme(self, color_scheme): """ Apply a pygments color scheme to the console. As there is not a 1 to 1 mapping between color scheme formats and console formats, we decided to make the following mapping (it usually looks good for most of the available pygments styles): - stdout_color = normal color - stderr_color = red (lighter if background is dark) - stdin_color = numbers color - app_msg_color = string color - bacgorund_color = background :param color_scheme: pyqode.core.api.ColorScheme to apply """ self.stdout_color = color_scheme.formats['normal'].foreground().color() self.stdin_color = color_scheme.formats['number'].foreground().color() self.app_msg_color = color_scheme.formats[ 'string'].foreground().color() self.background_color = color_scheme.background if self.background_color.lightness() < 128: self.stderr_color = QColor('#FF8080') else: self.stderr_color = QColor('red')
class AsyncClient(QObject): """ A class which handles a connection to a client through a QProcess. """ # Emitted when the client has initialized. initialized = Signal() # Emitted when the client errors. errored = Signal() # Emitted when a request response is received. received = Signal(object) def __init__(self, target, executable=None, name=None, extra_args=None, libs=None, cwd=None, env=None): super(AsyncClient, self).__init__() self.executable = executable or sys.executable self.extra_args = extra_args self.target = target self.name = name or self self.libs = libs self.cwd = cwd self.env = env self.is_initialized = False self.closing = False self.context = zmq.Context() QApplication.instance().aboutToQuit.connect(self.close) # Set up the heartbeat timer. self.timer = QTimer(self) self.timer.timeout.connect(self._heartbeat) def run(self): """Handle the connection with the server. """ # Set up the zmq port. self.socket = self.context.socket(zmq.PAIR) self.port = self.socket.bind_to_random_port('tcp://*') # Set up the process. self.process = QProcess(self) if self.cwd: self.process.setWorkingDirectory(self.cwd) p_args = ['-u', self.target, str(self.port)] if self.extra_args is not None: p_args += self.extra_args # Set up environment variables. processEnvironment = QProcessEnvironment() env = self.process.systemEnvironment() if (self.env and 'PYTHONPATH' not in self.env) or DEV: python_path = osp.dirname(get_module_path('spyderlib')) # Add the libs to the python path. for lib in self.libs: try: path = osp.dirname(imp.find_module(lib)[1]) python_path = osp.pathsep.join([python_path, path]) except ImportError: pass env.append("PYTHONPATH=%s" % python_path) if self.env: env.update(self.env) for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) # Start the process and wait for started. self.process.start(self.executable, p_args) self.process.finished.connect(self._on_finished) running = self.process.waitForStarted() if not running: raise IOError('Could not start %s' % self) # Set up the socket notifer. fid = self.socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self._on_msg_received) def request(self, func_name, *args, **kwargs): """Send a request to the server. The response will be a dictionary the 'request_id' and the 'func_name' as well as a 'result' field with the object returned by the function call or or an 'error' field with a traceback. """ if not self.is_initialized: return request_id = uuid.uuid4().hex request = dict(func_name=func_name, args=args, kwargs=kwargs, request_id=request_id) self._send(request) return request_id def close(self): """Cleanly close the connection to the server. """ self.closing = True self.is_initialized = False self.timer.stop() self.notifier.activated.disconnect(self._on_msg_received) self.notifier.setEnabled(False) del self.notifier self.request('server_quit') self.process.waitForFinished(1000) self.process.close() self.context.destroy() def _on_finished(self): """Handle a finished signal from the process. """ if self.closing: return if self.is_initialized: debug_print('Restarting %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.is_initialized = False self.notifier.setEnabled(False) self.run() else: debug_print('Errored %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.errored.emit() def _on_msg_received(self): """Handle a message trigger from the socket. """ self.notifier.setEnabled(False) while 1: try: resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) except zmq.ZMQError: self.notifier.setEnabled(True) return if not self.is_initialized: self.is_initialized = True debug_print('Initialized %s' % self.name) self.initialized.emit() self.timer.start(HEARTBEAT) continue resp['name'] = self.name self.received.emit(resp) def _heartbeat(self): """Send a heartbeat to keep the server alive. """ self._send(dict(func_name='server_heartbeat')) def _send(self, obj): """Send an object to the server. """ try: self.socket.send_pyobj(obj) except Exception as e: debug_print(e) self.is_initialized = False self._on_finished()
class ExternalPythonShell(ExternalShellBase): """External Shell widget: execute Python script in a separate process""" SHELL_CLASS = ExtPythonShellWidget sig_pdb = Signal(str, int) open_file = Signal(str, int) ipython_kernel_start_error = Signal(str) create_ipython_client = Signal(str) started = Signal() sig_finished = Signal() def __init__(self, parent=None, fname=None, wdir=None, interact=False, debug=False, post_mortem=False, path=[], python_args='', ipykernel=False, arguments='', stand_alone=None, umr_enabled=True, umr_namelist=[], umr_verbose=True, pythonstartup=None, pythonexecutable=None, external_interpreter=False, monitor_enabled=True, mpl_backend=None, ets_backend='qt4', qt_api=None, merge_output_channels=False, colorize_sys_stderr=False, autorefresh_timeout=3000, autorefresh_state=True, light_background=True, menu_actions=None, show_buttons_inside=True, show_elapsed_time=True): assert qt_api in (None, 'pyqt', 'pyside', 'pyqt5') self.namespacebrowser = None # namespace browser widget! self.dialog_manager = DialogManager() self.stand_alone = stand_alone # stand alone settings (None: plugin) self.interact = interact self.is_ipykernel = ipykernel self.pythonstartup = pythonstartup self.pythonexecutable = pythonexecutable self.external_interpreter = external_interpreter self.monitor_enabled = monitor_enabled self.mpl_backend = mpl_backend self.ets_backend = ets_backend self.qt_api = qt_api self.merge_output_channels = merge_output_channels self.colorize_sys_stderr = colorize_sys_stderr self.umr_enabled = umr_enabled self.umr_namelist = umr_namelist self.umr_verbose = umr_verbose self.autorefresh_timeout = autorefresh_timeout self.autorefresh_state = autorefresh_state self.namespacebrowser_button = None self.cwd_button = None self.env_button = None self.syspath_button = None self.terminate_button = None self.notification_thread = None ExternalShellBase.__init__(self, parent=parent, fname=fname, wdir=wdir, history_filename='history.py', light_background=light_background, menu_actions=menu_actions, show_buttons_inside=show_buttons_inside, show_elapsed_time=show_elapsed_time) if self.pythonexecutable is None: self.pythonexecutable = get_python_executable() self.python_args = None if python_args: assert is_text_string(python_args) self.python_args = python_args assert is_text_string(arguments) self.arguments = arguments self.connection_file = None if self.is_ipykernel: self.interact = False # Running our custom startup script for IPython kernels: # (see spyderlib/widgets/externalshell/start_ipython_kernel.py) self.fname = get_module_source_path( 'spyderlib.widgets.externalshell', 'start_ipython_kernel.py') self.shell.set_externalshell(self) self.toggle_globals_explorer(False) self.interact_action.setChecked(self.interact) self.debug_action.setChecked(debug) self.introspection_socket = None self.is_interpreter = fname is None if self.is_interpreter: self.terminate_button.hide() self.post_mortem_action.setChecked(post_mortem and not self.is_interpreter) # Additional python path list self.path = path self.shell.path = path def set_introspection_socket(self, introspection_socket): self.introspection_socket = introspection_socket if self.namespacebrowser is not None: settings = self.namespacebrowser.get_view_settings() communicate(introspection_socket, 'set_remote_view_settings()', settings=[settings]) def set_autorefresh_timeout(self, interval): if self.introspection_socket is not None: try: communicate(self.introspection_socket, "set_monitor_timeout(%d)" % interval) except socket.error: pass def closeEvent(self, event): self.quit_monitor() ExternalShellBase.closeEvent(self, event) def get_toolbar_buttons(self): ExternalShellBase.get_toolbar_buttons(self) if self.namespacebrowser_button is None \ and self.stand_alone is not None: self.namespacebrowser_button = create_toolbutton(self, text=_("Variables"), icon=ima.icon('dictedit'), tip=_("Show/hide global variables explorer"), toggled=self.toggle_globals_explorer, text_beside_icon=True) if self.terminate_button is None: self.terminate_button = create_toolbutton(self, text=_("Terminate"), icon=ima.icon('stop'), tip=_("Attempts to stop the process. The process\n" "may not exit as a result of clicking this\n" "button (it is given the chance to prompt\n" "the user for any unsaved files, etc).")) buttons = [] if self.namespacebrowser_button is not None: buttons.append(self.namespacebrowser_button) buttons += [self.run_button, self.terminate_button, self.kill_button, self.options_button] return buttons def get_options_menu(self): ExternalShellBase.get_options_menu(self) self.interact_action = create_action(self, _("Interact")) self.interact_action.setCheckable(True) self.debug_action = create_action(self, _("Debug")) self.debug_action.setCheckable(True) self.args_action = create_action(self, _("Arguments..."), triggered=self.get_arguments) self.post_mortem_action = create_action(self, _("Post Mortem Debug")) self.post_mortem_action.setCheckable(True) run_settings_menu = QMenu(_("Run settings"), self) add_actions(run_settings_menu, (self.interact_action, self.debug_action, self.args_action, self.post_mortem_action)) self.cwd_button = create_action(self, _("Working directory"), icon=ima.icon('DirOpenIcon'), tip=_("Set current working directory"), triggered=self.set_current_working_directory) self.env_button = create_action(self, _("Environment variables"), icon=ima.icon('environ'), triggered=self.show_env) self.syspath_button = create_action(self, _("Show sys.path contents"), icon=ima.icon('syspath'), triggered=self.show_syspath) actions = [run_settings_menu, self.show_time_action, None, self.cwd_button, self.env_button, self.syspath_button] if self.menu_actions is not None: actions += [None]+self.menu_actions return actions def is_interpreter(self): """Return True if shellwidget is a Python interpreter""" return self.is_interpreter def get_shell_widget(self): if self.stand_alone is None: return self.shell else: self.namespacebrowser = NamespaceBrowser(self) settings = self.stand_alone self.namespacebrowser.set_shellwidget(self) self.namespacebrowser.setup(**settings) self.namespacebrowser.sig_collapse.connect( lambda: self.toggle_globals_explorer(False)) # Shell splitter self.splitter = splitter = QSplitter(Qt.Vertical, self) self.splitter.splitterMoved.connect(self.splitter_moved) splitter.addWidget(self.shell) splitter.setCollapsible(0, False) splitter.addWidget(self.namespacebrowser) splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 0) splitter.setHandleWidth(5) splitter.setSizes([2, 1]) return splitter def get_icon(self): return ima.icon('python') def set_buttons_runnning_state(self, state): ExternalShellBase.set_buttons_runnning_state(self, state) self.interact_action.setEnabled(not state and not self.is_interpreter) self.debug_action.setEnabled(not state and not self.is_interpreter) self.args_action.setEnabled(not state and not self.is_interpreter) self.post_mortem_action.setEnabled(not state and not self.is_interpreter) if state: if self.arguments: argstr = _("Arguments: %s") % self.arguments else: argstr = _("No argument") else: argstr = _("Arguments...") self.args_action.setText(argstr) self.terminate_button.setVisible(not self.is_interpreter and state) if not state: self.toggle_globals_explorer(False) for btn in (self.cwd_button, self.env_button, self.syspath_button): btn.setEnabled(state and self.monitor_enabled) if self.namespacebrowser_button is not None: self.namespacebrowser_button.setEnabled(state) def set_namespacebrowser(self, namespacebrowser): """ Set namespace browser *widget* Note: this method is not used in stand alone mode """ self.namespacebrowser = namespacebrowser self.configure_namespacebrowser() def configure_namespacebrowser(self): """Connect the namespace browser to the notification thread""" if self.notification_thread is not None: self.notification_thread.refresh_namespace_browser.connect( self.namespacebrowser.refresh_table) signal = self.notification_thread.sig_process_remote_view signal.connect(lambda data: self.namespacebrowser.process_remote_view(data)) def create_process(self): self.shell.clear() self.process = QProcess(self) if self.merge_output_channels: self.process.setProcessChannelMode(QProcess.MergedChannels) else: self.process.setProcessChannelMode(QProcess.SeparateChannels) self.shell.wait_for_ready_read.connect( lambda: self.process.waitForReadyRead(250)) # Working directory if self.wdir is not None: self.process.setWorkingDirectory(self.wdir) #-------------------------Python specific------------------------------ # Python arguments p_args = ['-u'] if DEBUG >= 3: p_args += ['-v'] p_args += get_python_args(self.fname, self.python_args, self.interact_action.isChecked(), self.debug_action.isChecked(), self.arguments) env = [to_text_string(_path) for _path in self.process.systemEnvironment()] if self.pythonstartup: env.append('PYTHONSTARTUP=%s' % self.pythonstartup) #-------------------------Python specific------------------------------- # Post mortem debugging if self.post_mortem_action.isChecked(): env.append('SPYDER_EXCEPTHOOK=True') # Set standard input/output encoding for Python consoles # (IPython handles it on its own) # See http://stackoverflow.com/q/26312400/438386, specifically # the comments of Martijn Pieters if not self.is_ipykernel: env.append('PYTHONIOENCODING=UTF-8') # Monitor if self.monitor_enabled: env.append('SPYDER_SHELL_ID=%s' % id(self)) env.append('SPYDER_AR_TIMEOUT=%d' % self.autorefresh_timeout) env.append('SPYDER_AR_STATE=%r' % self.autorefresh_state) from spyderlib.widgets.externalshell import introspection introspection_server = introspection.start_introspection_server() introspection_server.register(self) notification_server = introspection.start_notification_server() self.notification_thread = notification_server.register(self) self.notification_thread.sig_pdb.connect( lambda fname, lineno: self.sig_pdb.emit(fname, lineno)) self.notification_thread.new_ipython_kernel.connect( lambda args: self.create_ipython_client.emit(args)) self.notification_thread.open_file.connect( lambda fname, lineno: self.open_file.emit(fname, lineno)) if self.namespacebrowser is not None: self.configure_namespacebrowser() env.append('SPYDER_I_PORT=%d' % introspection_server.port) env.append('SPYDER_N_PORT=%d' % notification_server.port) # External modules options if not self.is_ipykernel: env.append('ETS_TOOLKIT=%s' % self.ets_backend) if self.mpl_backend is not None: backends = {0: 'Automatic', 1: 'None', 2: 'TkAgg'} env.append('SPY_MPL_BACKEND=%s' % backends[self.mpl_backend]) if self.qt_api and not self.is_ipykernel: env.append('QT_API=%s' % self.qt_api) env.append('COLORIZE_SYS_STDERR=%s' % self.colorize_sys_stderr) # # Socket-based alternative (see input hook in sitecustomize.py): # if self.install_qt_inputhook: # from PyQt4.QtNetwork import QLocalServer # self.local_server = QLocalServer() # self.local_server.listen(str(id(self))) # User Module Deleter if self.is_interpreter: env.append('UMR_ENABLED=%r' % self.umr_enabled) env.append('UMR_NAMELIST=%s' % ','.join(self.umr_namelist)) env.append('UMR_VERBOSE=%r' % self.umr_verbose) env.append('MATPLOTLIB_ION=True') else: if self.interact: env.append('MATPLOTLIB_ION=True') else: env.append('MATPLOTLIB_ION=False') # IPython kernel env.append('IPYTHON_KERNEL=%r' % self.is_ipykernel) # External interpreter env.append('EXTERNAL_INTERPRETER=%r' % self.external_interpreter) # Add sitecustomize path to path list pathlist = [] scpath = osp.dirname(osp.abspath(__file__)) pathlist.append(scpath) # Adding Spyder path pathlist += self.path # Adding path list to PYTHONPATH environment variable add_pathlist_to_PYTHONPATH(env, pathlist) #-------------------------Python specific------------------------------ self.process.readyReadStandardOutput.connect(self.write_output) self.process.readyReadStandardError.connect(self.write_error) self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.sig_finished.connect(self.dialog_manager.close_all) self.terminate_button.clicked.connect(self.process.terminate) self.kill_button.clicked.connect(self.process.kill) #-------------------------Python specific------------------------------ # Fixes for our Mac app: # 1. PYTHONPATH and PYTHONHOME are set while bootstrapping the app, # but their values are messing sys.path for external interpreters # (e.g. EPD) so we need to remove them from the environment. # 2. Set PYTHONPATH again but without grabbing entries defined in the # environment (Fixes Issue 1321) # 3. Remove PYTHONOPTIMIZE from env so that we can have assert # statements working with our interpreters (See Issue 1281) if running_in_mac_app(): if MAC_APP_NAME not in self.pythonexecutable: env = [p for p in env if not (p.startswith('PYTHONPATH') or \ p.startswith('PYTHONHOME'))] # 1. add_pathlist_to_PYTHONPATH(env, pathlist, drop_env=True) # 2. env = [p for p in env if not p.startswith('PYTHONOPTIMIZE')] # 3. processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.process.start(self.pythonexecutable, p_args) #-------------------------Python specific------------------------------ running = self.process.waitForStarted(3000) self.set_running_state(running) if not running: if self.is_ipykernel: self.ipython_kernel_start_error.emit( _("The kernel failed to start!! That's all we know... " "Please close this console and open a new one.")) else: QMessageBox.critical(self, _("Error"), _("A Python console failed to start!")) else: self.shell.setFocus() self.started.emit() return self.process def finished(self, exit_code, exit_status): """Reimplement ExternalShellBase method""" if self.is_ipykernel and exit_code == 1: self.ipython_kernel_start_error.emit(self.shell.get_text_with_eol()) ExternalShellBase.finished(self, exit_code, exit_status) self.introspection_socket = None #============================================================================== # Input/Output #============================================================================== def write_error(self): if os.name == 'nt': #---This is apparently necessary only on Windows (not sure though): # emptying standard output buffer before writing error output self.process.setReadChannel(QProcess.StandardOutput) if self.process.waitForReadyRead(1): self.write_output() self.shell.write_error(self.get_stderr()) QApplication.processEvents() def send_to_process(self, text): if not self.is_running(): return if not is_text_string(text): text = to_text_string(text) if self.mpl_backend == 0 and os.name == 'nt' and \ self.introspection_socket is not None: communicate(self.introspection_socket, "toggle_inputhook_flag(True)") # # Socket-based alternative (see input hook in sitecustomize.py): # while self.local_server.hasPendingConnections(): # self.local_server.nextPendingConnection().write('go!') if any([text == cmd for cmd in ['%ls', '%pwd', '%scientific']]) or \ any([text.startswith(cmd) for cmd in ['%cd ', '%clear ']]): text = 'evalsc(r"%s")\n' % text if not text.endswith('\n'): text += '\n' self.process.write(to_binary_string(text, 'utf8')) self.process.waitForBytesWritten(-1) # Eventually write prompt faster (when hitting Enter continuously) # -- necessary/working on Windows only: if os.name == 'nt': self.write_error() def keyboard_interrupt(self): if self.introspection_socket is not None: communicate(self.introspection_socket, "thread.interrupt_main()") def quit_monitor(self): if self.introspection_socket is not None: try: write_packet(self.introspection_socket, "thread.exit()") except socket.error: pass #============================================================================== # Globals explorer #============================================================================== @Slot(bool) def toggle_globals_explorer(self, state): if self.stand_alone is not None: self.splitter.setSizes([1, 1 if state else 0]) self.namespacebrowser_button.setChecked(state) if state and self.namespacebrowser is not None: self.namespacebrowser.refresh_table() def splitter_moved(self, pos, index): self.namespacebrowser_button.setChecked( self.splitter.sizes()[1] ) #============================================================================== # Misc. #============================================================================== @Slot() def set_current_working_directory(self): """Set current working directory""" cwd = self.shell.get_cwd() self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), cwd) if directory: self.shell.set_cwd(directory) self.redirect_stdio.emit(True) @Slot() def show_env(self): """Show environment variables""" get_func = self.shell.get_env set_func = self.shell.set_env self.dialog_manager.show(RemoteEnvDialog(get_func, set_func)) @Slot() def show_syspath(self): """Show sys.path contents""" editor = CollectionsEditor() editor.setup(self.shell.get_syspath(), title="sys.path", readonly=True, width=600, icon=ima.icon('syspath')) self.dialog_manager.show(editor)
class ProfilerWidget(QWidget): """ Profiler widget """ DATAPATH = get_conf_path('profiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100): QWidget.__init__(self, parent) self.setWindowTitle("Profiler") self.output = None self.error_output = None self._last_wdir = None self._last_args = None self._last_pythonpath = None self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Profile"), tip=_("Run profiler"), triggered=lambda : self.start(), text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current profiling"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) # FIXME: The combobox emits this signal on almost any event # triggering show_data() too early, too often. browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python script'), triggered=self.select_file) self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Show program's output"), triggered=self.show_log) self.datatree = ProfilerDataTree(self) self.collapse_button = create_toolbutton(self, icon=ima.icon('collapse'), triggered=lambda dD: self.datatree.change_view(-1), tip=_('Collapse one level up')) self.expand_button = create_toolbutton(self, icon=ima.icon('expand'), triggered=lambda dD: self.datatree.change_view(1), tip=_('Expand one level down')) self.save_button = create_toolbutton(self, text_beside_icon=True, text=_("Save data"), icon=ima.icon('filesave'), triggered=self.save_data, tip=_('Save profiling data')) self.load_button = create_toolbutton(self, text_beside_icon=True, text=_("Load data"), icon=ima.icon('fileimport'), triggered=self.compare, tip=_('Load profiling data for comparison')) self.clear_button = create_toolbutton(self, text_beside_icon=True, text=_("Clear comparison"), icon=ima.icon('editdelete'), triggered=self.clear) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.collapse_button) hlayout2.addWidget(self.expand_button) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) hlayout2.addWidget(self.save_button) hlayout2.addWidget(self.load_button) hlayout2.addWidget(self.clear_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.datatree) self.setLayout(layout) self.process = None self.set_running_state(False) self.start_button.setEnabled(False) self.clear_button.setEnabled(False) if not is_profiler_installed(): # This should happen only on certain GNU/Linux distributions # or when this a home-made Python build because the Python # profilers are included in the Python standard library for widget in (self.datatree, self.filecombo, self.start_button, self.stop_button): widget.setDisabled(True) url = 'http://docs.python.org/library/profile.html' text = '%s <a href=%s>%s</a>' % (_('Please install'), url, _("the Python profiler modules")) self.datelabel.setText(text) else: pass # self.show_data() def save_data(self): """Save data""" title = _( "Save profiler result") filename, _selfilter = getsavefilename(self, title, getcwd(), _("Profiler result")+" (*.Result)") if filename: self.datatree.save_data(filename) def compare(self): filename, _selfilter = getopenfilename(self, _("Select script to compare"), getcwd(), _("Profiler result")+" (*.Result)") if filename: self.datatree.compare(filename) self.show_data() self.clear_button.setEnabled(True) def clear(self): self.datatree.compare(None) self.datatree.hide_diff_cols(True) self.show_data() self.clear_button.setEnabled(False) def analyze(self, filename, wdir=None, args=None, pythonpath=None): if not is_profiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) index = None # FIXME: storing data is not implemented yet if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename(self, _("Select Python script"), getcwd(), _("Python scripts")+" (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def show_log(self): if self.output: TextEditor(self.output, title=_("Profiler output"), readonly=True, size=(700, 500)).exec_() def show_errorlog(self): if self.error_output: TextEditor(self.error_output, title=_("Profiler output"), readonly=True, size=(700, 500)).exec_() def start(self, wdir=None, args=None, pythonpath=None): filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) if pythonpath is not None: env = [to_text_string(_pth) for _pth in self.process.systemEnvironment()] baseshell.add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' p_args = ['-m', 'cProfile', '-o', self.DATAPATH] if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): p_args.append(osp.normpath(filename).replace(os.sep, '/')) else: p_args.append(filename) if args: p_args.extend(shell_split(args)) executable = sys.executable if executable.endswith("spyder.exe"): # py2exe distribution executable = "python.exe" self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string( locale_codec.toUnicode(qba.data()) ) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output # FIXME: figure out if show_data should be called here or # as a signal from the combobox self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.load_data(self.DATAPATH) self.datatree.show_tree() text_style = "<span style=\'color: #444444\'><b>%s </b></span>" date_text = text_style % time.strftime("%d %b %Y %H:%M", time.localtime()) self.datelabel.setText(date_text)
class PylintWidget(QWidget): """ Pylint widget """ DATAPATH = get_conf_path('pylint.results') VERSION = '1.1.0' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100): QWidget.__init__(self, parent) self.setWindowTitle("Pylint") self.output = None self.error_output = None self.max_entries = max_entries self.rdata = [] if osp.isfile(self.DATAPATH): try: data = pickle.loads(open(self.DATAPATH, 'rb').read()) if data[0] == self.VERSION: self.rdata = data[1:] except (EOFError, ImportError): pass self.filecombo = PythonModulesComboBox(self) if self.rdata: self.remove_obsolete_items() self.filecombo.addItems(self.get_filenames()) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Analyze"), tip=_("Run analysis"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current analysis"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) self.filecombo.valid.connect(self.show_data) browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python file'), triggered=self.select_file) self.ratelabel = QLabel() self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Complete output"), triggered=self.show_log) self.treewidget = ResultsTree(self) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.ratelabel) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.treewidget) self.setLayout(layout) self.process = None self.set_running_state(False) self.show_data() def analyze(self, filename): filename = to_text_string(filename) # filename is a QString instance self.kill_if_running() index, _data = self.get_data(filename) if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): self.start() @Slot() def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename(self, _("Select Python file"), getcwd(), _("Python files")+" (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def remove_obsolete_items(self): """Removing obsolete items""" self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): return [filename for filename, _data in self.rdata] def get_data(self, filename): filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) self.save() def save(self): while len(self.rdata) > self.max_entries: self.rdata.pop(-1) pickle.dump([self.VERSION]+self.rdata, open(self.DATAPATH, 'wb'), 2) @Slot() def show_log(self): if self.output: TextEditor(self.output, title=_("Pylint output"), readonly=True, size=(700, 500)).exec_() @Slot() def start(self): filename = to_text_string(self.filecombo.currentText()) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(osp.dirname(filename)) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) self.output = '' self.error_output = '' plver = PYLINT_VER if plver is not None: p_args = ['-m', 'pylint', '--output-format=text'] if plver.split('.')[0] == '0': p_args += ['-i', 'yes'] else: # Option '-i' (alias for '--include-ids') was removed in pylint # 1.0 p_args += ["--msg-template='{msg_id}:{line:3d},"\ "{column}: {obj}: {msg}"] p_args += [osp.basename(filename)] else: p_args = [osp.basename(filename)] self.process.start(sys.executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string( locale_codec.toUnicode(qba.data()) ) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) if not self.output: if self.error_output: QMessageBox.critical(self, _("Error"), self.error_output) print("pylint error:\n\n" + self.error_output, file=sys.stderr) return # Convention, Refactor, Warning, Error results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} txt_module = '************* Module ' module = '' # Should not be needed - just in case something goes wrong for line in self.output.splitlines(): if line.startswith(txt_module): # New module module = line[len(txt_module):] continue # Supporting option include-ids: ('R3873:' instead of 'R:') if not re.match('^[CRWE]+([0-9]{4})?:', line): continue i1 = line.find(':') if i1 == -1: continue msg_id = line[:i1] i2 = line.find(':', i1+1) if i2 == -1: continue line_nb = line[i1+1:i2].strip() if not line_nb: continue line_nb = int(line_nb.split(',')[0]) message = line[i2+1:] item = (module, line_nb, message, msg_id) results[line[0]+':'].append(item) # Rate rate = None txt_rate = 'Your code has been rated at ' i_rate = self.output.find(txt_rate) if i_rate > 0: i_rate_end = self.output.find('/10', i_rate) if i_rate_end > 0: rate = self.output[i_rate+len(txt_rate):i_rate_end] # Previous run previous = '' if rate is not None: txt_prun = 'previous run: ' i_prun = self.output.find(txt_prun, i_rate_end) if i_prun > 0: i_prun_end = self.output.find('/10', i_prun) previous = self.output[i_prun+len(txt_prun):i_prun_end] filename = to_text_string(self.filecombo.currentText()) self.set_data(filename, (time.localtime(), rate, previous, results)) self.output = self.error_output + self.output self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return _index, data = self.get_data(filename) if data is None: text = _('Source code has not been rated yet.') self.treewidget.clear_results() date_text = '' else: datetime, rate, previous_rate, results = data if rate is None: text = _('Analysis did not succeed ' '(see output for more details).') self.treewidget.clear_results() date_text = '' else: text_style = "<span style=\'color: #444444\'><b>%s </b></span>" rate_style = "<span style=\'color: %s\'><b>%s</b></span>" prevrate_style = "<span style=\'color: #666666\'>%s</span>" color = "#FF0000" if float(rate) > 5.: color = "#22AA22" elif float(rate) > 3.: color = "#EE5500" text = _('Global evaluation:') text = (text_style % text)+(rate_style % (color, ('%s/10' % rate))) if previous_rate: text_prun = _('previous run:') text_prun = ' (%s %s/10)' % (text_prun, previous_rate) text += prevrate_style % text_prun self.treewidget.set_results(filename, results) date = to_text_string(time.strftime("%d %b %Y %H:%M", datetime), encoding='utf8') date_text = text_style % date self.ratelabel.setText(text) self.datelabel.setText(date_text)
class ProcessWorker(QObject): """ """ sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs={}): super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs self._timer = QTimer() self._process = QProcess() self._timer.setInterval(50) self._timer.timeout.connect(self._communicate) self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """ """ if not self._communicate_first: if self._process.state() == QProcess.NotRunning: self.communicate() elif self._fired: self._timer.stop() def communicate(self): """ """ self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda cloud api site' not in stderr.lower(): if stderr.strip() and self._conda: raise Exception('{0}:\n' 'STDERR:\n{1}\nEND' ''.format(' '.join(self._cmd_list), stderr)) # elif stderr.strip() and self._pip: # raise PipError(self._cmd_list) else: result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except ValueError as error: result = stdout, error if 'error' in result[0]: error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """ """ self._process.close() def is_finished(self): """ """ return self._process.state() == QProcess.NotRunning and self._fired def start(self): """ """ logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class MemoryProfilerWidget(QWidget): """ Memory profiler widget. """ DATAPATH = get_conf_path('memoryprofiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) sig_finished = Signal() def __init__(self, parent): QWidget.__init__(self, parent) self.setWindowTitle("Memory profiler") self.output = None self.error_output = None self.use_colors = True self._last_wdir = None self._last_args = None self._last_pythonpath = None self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=get_icon('run.png'), text=_("Profile memory usage"), tip=_("Run memory profiler"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=get_icon('terminate.png'), text=_("Stop"), tip=_("Stop current profiling"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) # FIXME: The combobox emits this signal on almost any event # triggering show_data() too early, too often. browse_button = create_toolbutton(self, icon=get_icon('fileopen.png'), tip=_('Select Python script'), triggered=self.select_file) self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=get_icon('log.png'), text=_("Output"), text_beside_icon=True, tip=_("Show program's output"), triggered=self.show_log) self.datatree = MemoryProfilerDataTree(self) self.collapse_button = create_toolbutton( self, icon=get_icon('collapse.png'), triggered=lambda dD=-1: self.datatree.collapseAll(), tip=_('Collapse all')) self.expand_button = create_toolbutton( self, icon=get_icon('expand.png'), triggered=lambda dD=1: self.datatree.expandAll(), tip=_('Expand all')) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.collapse_button) hlayout2.addWidget(self.expand_button) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.datatree) self.setLayout(layout) self.process = None self.set_running_state(False) self.start_button.setEnabled(False) if not is_memoryprofiler_installed(): for widget in (self.datatree, self.filecombo, self.log_button, self.start_button, self.stop_button, browse_button, self.collapse_button, self.expand_button): widget.setDisabled(True) text = _( '<b>Please install the <a href="%s">memory_profiler module</a></b>' ) % WEBSITE_URL self.datelabel.setText(text) self.datelabel.setOpenExternalLinks(True) else: pass # self.show_data() def analyze(self, filename, wdir=None, args=None, pythonpath=None, use_colors=True): self.use_colors = use_colors if not is_memoryprofiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) index = None # FIXME: storing data is not implemented yet if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python script"), getcwd(), _("Python scripts") + " (*.py ; *.pyw)") self.redirect_stdio.emit(False) if filename: self.analyze(filename) def show_log(self): if self.output: TextEditor(self.output, title=_("Memory profiler output"), readonly=True, size=(700, 500)).exec_() def show_errorlog(self): if self.error_output: TextEditor(self.error_output, title=_("Memory profiler output"), readonly=True, size=(700, 500)).exec_() def start(self, wdir=None, args=None, pythonpath=None): filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(self.finished) self.stop_button.clicked.connect(self.process.kill) if pythonpath is not None: env = [ to_text_string(_pth) for _pth in self.process.systemEnvironment() ] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' # remove previous results, since memory_profiler appends to output file # instead of replacing if osp.isfile(self.DATAPATH): os.remove(self.DATAPATH) if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): filename = osp.normpath(filename).replace(os.sep, '/') p_args = [ '-m', 'memory_profiler', '-o', '"' + self.DATAPATH + '"', '"' + filename + '"' ] if args: p_args.extend(programs.shell_split(args)) executable = get_python_executable() executable += ' ' + ' '.join(p_args) executable = executable.replace(os.sep, '/') self.process.start(executable) else: p_args = ['-m', 'memory_profiler', '-o', self.DATAPATH, filename] if args: p_args.extend(programs.shell_split(args)) executable = get_python_executable() self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(locale_codec.toUnicode(qba.data())) if error: self.error_output += text else: self.output += text def finished(self): self.set_running_state(False) self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output # FIXME: figure out if show_data should be called here or # as a signal from the combobox self.show_data(justanalyzed=True) self.sig_finished.emit() def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return self.datatree.load_data(self.DATAPATH) self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.show_tree() text_style = "<span style=\'color: #444444\'><b>%s </b></span>" date_text = text_style % time.strftime("%d %b %Y %H:%M", time.localtime()) self.datelabel.setText(date_text)
class ProfilerWidget(QWidget): """ Profiler widget """ DATAPATH = get_conf_path('profiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100): QWidget.__init__(self, parent) self.setWindowTitle("Profiler") self.output = None self.error_output = None self._last_wdir = None self._last_args = None self._last_pythonpath = None self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Profile"), tip=_("Run profiler"), triggered=lambda: self.start(), text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current profiling"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) # FIXME: The combobox emits this signal on almost any event # triggering show_data() too early, too often. browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python script'), triggered=self.select_file) self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Show program's output"), triggered=self.show_log) self.datatree = ProfilerDataTree(self) self.collapse_button = create_toolbutton( self, icon=ima.icon('collapse'), triggered=lambda dD: self.datatree.change_view(-1), tip=_('Collapse one level up')) self.expand_button = create_toolbutton( self, icon=ima.icon('expand'), triggered=lambda dD: self.datatree.change_view(1), tip=_('Expand one level down')) self.save_button = create_toolbutton(self, text_beside_icon=True, text=_("Save data"), icon=ima.icon('filesave'), triggered=self.save_data, tip=_('Save profiling data')) self.load_button = create_toolbutton( self, text_beside_icon=True, text=_("Load data"), icon=ima.icon('fileimport'), triggered=self.compare, tip=_('Load profiling data for comparison')) self.clear_button = create_toolbutton(self, text_beside_icon=True, text=_("Clear comparison"), icon=ima.icon('editdelete'), triggered=self.clear) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.collapse_button) hlayout2.addWidget(self.expand_button) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) hlayout2.addWidget(self.save_button) hlayout2.addWidget(self.load_button) hlayout2.addWidget(self.clear_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.datatree) self.setLayout(layout) self.process = None self.set_running_state(False) self.start_button.setEnabled(False) self.clear_button.setEnabled(False) if not is_profiler_installed(): # This should happen only on certain GNU/Linux distributions # or when this a home-made Python build because the Python # profilers are included in the Python standard library for widget in (self.datatree, self.filecombo, self.start_button, self.stop_button): widget.setDisabled(True) url = 'http://docs.python.org/library/profile.html' text = '%s <a href=%s>%s</a>' % (_('Please install'), url, _("the Python profiler modules")) self.datelabel.setText(text) else: pass # self.show_data() def save_data(self): """Save data""" title = _("Save profiler result") filename, _selfilter = getsavefilename( self, title, getcwd_or_home(), _("Profiler result") + " (*.Result)") if filename: self.datatree.save_data(filename) def compare(self): filename, _selfilter = getopenfilename( self, _("Select script to compare"), getcwd_or_home(), _("Profiler result") + " (*.Result)") if filename: self.datatree.compare(filename) self.show_data() self.clear_button.setEnabled(True) def clear(self): self.datatree.compare(None) self.datatree.hide_diff_cols(True) self.show_data() self.clear_button.setEnabled(False) def analyze(self, filename, wdir=None, args=None, pythonpath=None): if not is_profiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) index = None # FIXME: storing data is not implemented yet if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python script"), getcwd_or_home(), _("Python scripts") + " (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def show_log(self): if self.output: TextEditor(self.output, title=_("Profiler output"), readonly=True, size=(700, 500)).exec_() def show_errorlog(self): if self.error_output: TextEditor(self.error_output, title=_("Profiler output"), readonly=True, size=(700, 500)).exec_() def start(self, wdir=None, args=None, pythonpath=None): filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) if pythonpath is not None: env = [ to_text_string(_pth) for _pth in self.process.systemEnvironment() ] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' p_args = ['-m', 'cProfile', '-o', self.DATAPATH] if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): p_args.append(osp.normpath(filename).replace(os.sep, '/')) else: p_args.append(filename) if args: p_args.extend(shell_split(args)) executable = sys.executable if executable.endswith("spyder.exe"): # py2exe distribution executable = "python.exe" self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(locale_codec.toUnicode(qba.data())) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output # FIXME: figure out if show_data should be called here or # as a signal from the combobox self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.load_data(self.DATAPATH) self.datatree.show_tree() text_style = "<span style=\'color: #444444\'><b>%s </b></span>" date_text = text_style % time.strftime("%d %b %Y %H:%M", time.localtime()) self.datelabel.setText(date_text)
class CoalaWidget(QWidget): """ coala Widget """ DATAPATH = get_conf_path('coala.results') VERSION = '' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100, options_button=None, text_color=None, prevrate_color=None): QWidget.__init__(self, parent) self.setWindowTitle('coala') self.output = None self.error_output = None self.text_color = text_color self.prevrate_color = prevrate_color self.max_entries = max_entries self.rdata = [] if osp.isfile(self.DATAPATH): try: data = pickle.loads(open(self.DATAPATH, 'rb').read()) self.rdata = data[:] except (EOFError, ImportError): print('error!!') pass self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Analyze"), tip=_("Run analysis"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current analysis"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) self.filecombo.valid.connect(self.show_data) browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python file'), triggered=self.select_file) self.ratelabel = QLabel() self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Complete output"), triggered=self.show_log) self.treewidget = ResultsTree(self) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) if options_button: hlayout1.addWidget(options_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.ratelabel) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.treewidget) self.setLayout(layout) self.process = None self.set_running_state(False) self.show_data() if self.rdata: self.remove_obsolete_items() self.filecombo.addItems(self.get_filenames()) self.start_button.setEnabled(self.filecombo.is_valid()) else: self.start_button.setEnabled(False) def analyze(self, filename): filename = to_text_string(filename) # filename is a QString instance self.kill_if_running() index, _data = self.get_data(filename) if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): self.start() @Slot() def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python file"), getcwd_or_home(), _("Python files") + " (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def remove_obsolete_items(self): """Removing obsolete items""" self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): return [filename for filename, _data in self.rdata] def get_data(self, filename): filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) self.save() def save(self): while len(self.rdata) > self.max_entries: self.rdata.pop(-1) pickle.dump([self.VERSION] + self.rdata, open(self.DATAPATH, 'wb'), 2) @Slot() def show_log(self): if self.output: TextEditor(self.output, title=_("coala output"), readonly=True, size=(700, 500)).exec_() @Slot() def start(self): filename = to_text_string(self.filecombo.currentText()) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(osp.dirname(filename)) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) self.output = '' self.error_output = '' clver = COALA_VER if clver is not None: c_args = ['-m', 'run_coala'] self.process.start(sys.executable, c_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(locale_codec.toUnicode(qba.data())) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) if not self.output: if self.error_output: QMessageBox.critical(self, _("Error"), self.error_output) print("coala error:\n\n" + self.error_output, file=sys.stderr) return results = {'C:': []} literal_dict = ast.literal_eval(self.output) line_numbers = [] char_numbers = [] bear_values = [] msg_values = [] for line in literal_dict['C']: print(line) for i in line: line_num = re.compile('(.+)~') val = line_num.findall(i) for line_nb in val: if line_nb: line_numbers.append(line_nb) for j in line: char_num = re.compile('(.*);') val = char_num.findall(j) for char_nm in val: if char_nm: char_numbers.append(char_nm) for k in line: bear_val = re.compile('(.*):') val = bear_val.findall(k) for bear_val in val: if bear_val: bear_values.append(bear_val) for m in line: msg_val = re.compile(':(.*)') val = msg_val.findall(m) for msg_val in val: if msg_val: msg_values.append(msg_val) item = list(zip(line_numbers, char_numbers, bear_values, msg_values)) for i in item: results['C:'].append(i) filename = to_text_string(self.filecombo.currentText()) self.set_data(filename, results) self.output = self.error_output + self.output self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return _index, data = self.get_data(filename) if data is None: self.treewidget.clear_results() else: results = data self.treewidget.set_results(filename, results)
class RunPopup(QDialog): def __init__(self, solver, parent): super(RunPopup, self).__init__(parent) self.commandline_option_exe = solver if solver else None self.mfix_available = False self.mfix_exe_cache = {} self.solver_list = {} self.template_values = {} self.cmdline = [] # List of strings self.parent = parent self.project = parent.project self.settings = parent.settings self.project_dir = parent.get_project_dir() self.gui_comments = self.project.mfix_gui_comments self.flag_processes = {} self.title = 'Run Solver' # load ui ui = self.ui = get_ui('run_popup.ui', self) ui.layout.setSizeConstraint(ui.layout.SetFixedSize) ui.toolbutton_browse.clicked.connect(self.handle_browse_exe) ui.toolbutton_browse.setIcon(get_icon('add.svg')) ui.toolbutton_browse.setIconSize(sub_icon_size()) ui.toolbutton_remove.clicked.connect(self.handle_remove_exe) ui.toolbutton_remove.setIcon(get_icon('remove.svg')) ui.toolbutton_remove.setIconSize(sub_icon_size()) ui.toolbutton_view_error.clicked.connect(self.show_solver_error) ui.listwidget_solver_list.itemSelectionChanged.connect( self.update_dialog_options) ui.combobox_restart.addItems(RESTART_TYPES.keys()) ui.combobox_restart.hide() ui.button_run.clicked.connect(self.handle_run) ui.button_cancel.clicked.connect(self.handle_abort) ui.pushbutton_browse_template.clicked.connect( self.handle_browse_template) n_cpus = multiprocessing.cpu_count() ui.groupbox_smp_options.setTitle("SMP Options (%s available locally)" % plural(n_cpus, "core")) ui.groupbox_queue.toggled.connect(self.toggle_run_btn_text) ui.widget_queue_options.hide() self.initialize_ui() self.init_templates() @property def solver(self): """The currently selected solver""" item = self.ui.listwidget_solver_list.currentItem() if item is None: solver = None else: solver = item.text() if not solver: solver = None return solver def toggle_run_btn_text(self): ui = self.ui ui.button_run.setText( 'Submit' if ui.groupbox_queue.isChecked() else 'Run') def update_gui_comment(self, key, val): if self.gui_comments.get(key) == val: return self.gui_comments[key] = val self.parent.set_unsaved_flag() # UI update functions def initialize_ui(self): ui = self.ui self.setWindowTitle(self.title) get_value = self.parent.project.get_value update_keyword = self.parent.update_keyword # restart spx_files = self.parent.get_output_files(SPX_GLOB) res = self.parent.get_res_files() enable = bool(spx_files) or bool(res) ui.groupbox_restart.setEnabled(enable) if not enable: ui.groupbox_restart.setChecked(False) ui.groupbox_restart.setTitle('Restart - no restart files found') restart_1 = bool(spx_files) and bool(res) if not restart_1: ui.combobox_restart.setCurrentIndex(1) self.enable_restart_item(RESTART_TYPES_INVS['restart_1'], restart_1) self.enable_restart_item(RESTART_TYPES_INVS['restart_2'], bool(res)) # set OMP_NUM_THREADS project_threads = self.gui_comments.get('OMP_NUM_THREADS', '1') env_threads = os.environ.get('OMP_NUM_THREADS', None) if env_threads: project_threads = env_threads ui.spinbox_threads.setValue(safe_int(project_threads, default=1)) # migrate from comments back to keywords, undo issues 149 gui_comment_nodes = self.gui_comments.pop('OMP_NODES', None) if gui_comment_nodes: self.parent.set_unsaved_flag() # modified project nodes = gui_comment_nodes.split(os.path.pathsep) if len(nodes) == 3: nodes = (nodesi, nodesj, nodesk) = [safe_int(n, default=1) for n in nodes] else: nodes = (nodesi, nodesj, nodesk) = 1, 1, 1 update_keyword('nodesi', nodesi) update_keyword('nodesj', nodesj) update_keyword('nodesk', nodesk) else: nodes = (nodesi, nodesj, nodesk) = [ get_value('nodes' + c, default=1) for c in 'ijk' ] for val, spin in zip( nodes, [ui.spinbox_nodesi, ui.spinbox_nodesj, ui.spinbox_nodesk]): spin.setValue(val) # local/queue ui.groupbox_queue.setChecked( int(self.gui_comments.get('submit_to_queue', 0))) # create initial executable list self.get_solver_list() if self.solver_list: self.mfix_available = True self.populate_combobox_solver() for exe in self.solver_list.keys(): self.get_exe_flags(exe) # select solver self.ui.listwidget_solver_list.setCurrentRow(0) self.update_dialog_options() def init_templates(self): # look for templates in MFIX_HOME/queue_templates search_p = os.path.join(get_mfix_templates(), 'queue_templates') self.templates = {} for root, _, files in os.walk(search_p): for f in files: p = os.path.join(root, f) self.add_queue_template(p) # look for recent templates temp_paths = self.settings.value('queue_templates') if temp_paths: for temp_path in temp_paths.split('|'): if os.path.exists(temp_path): self.add_queue_template(temp_path) self.ui.combobox_template.currentIndexChanged.connect( self.update_queue_widgets) temp = self.gui_comments.get('queue_template') if temp: self.template_values = json.loads(temp) t_name = self.template_values.get('template') if t_name: self.set_current_template(t_name) self.update_queue_widgets() def save_template(self): '''Save the current template data''' self.collect_template_values() template_txt = self.ui.combobox_template.currentText() self.template_values['template'] = template_txt self.update_gui_comment('queue_template', json.dumps(self.template_values)) def collect_template_values(self): template_txt = self.ui.combobox_template.currentText() template = self.templates.get(template_txt, {}) replace_dict = {} for name, wid in template.items(): if not isinstance(wid, dict): continue if 'widget_obj' in wid: wid_obj = wid['widget_obj'] if isinstance(wid_obj, (QSpinBox, QDoubleSpinBox)): self.template_values[name] = v = wid_obj.value() elif isinstance(wid_obj, QCheckBox): self.template_values[name] = v = wid_obj.value if v: v = wid.get('true', '') else: v = wid.get('false', '') elif isinstance(wid_obj, QListWidget): self.template_values[name] = v = wid_obj.value v = ' '.join(v) else: self.template_values[name] = v = wid_obj.value replace_dict[name] = v return replace_dict def set_current_template(self, name): '''set the template file combobox''' cb = self.ui.combobox_template for itm in range(cb.count()): if str(name).lower() == str(cb.itemText(itm)).lower(): cb.setCurrentIndex(itm) break def add_queue_template(self, path, select=False): config, script = extract_config(path) c = configparser.ConfigParser() c.readfp(StringIO(config)) d = OrderedDict([(s, dict(c.items(s))) for s in c.sections()]) d['path'] = path d['script'] = script name = os.path.basename(path) if 'options' in d: name = d['options'].get('name', name) self.templates[name] = d self.ui.combobox_template.clear() self.ui.combobox_template.addItems(list(self.templates.keys())) if select: self.set_current_template(name) def update_queue_widgets(self): l = self.ui.groupbox_queue_options_gridlayout clear_layout(l) tp = self.ui.combobox_template.currentText() wids_data = self.templates.get(tp, None) if wids_data is None: return # check to see if qsub command present cmd = wids_data.get('options', {}).get('submit', None) if cmd is not None: cmd = cmd.strip().split()[0] if spawn.find_executable(cmd) is None: label = QLabel( 'The submission command "{}" does not exist in ' 'the current environment. Please select another ' 'template, edit the template, and/or check your ' 'environment.'.format(cmd)) label.setStyleSheet('color:red') label.setWordWrap(True) l.addWidget(label, 0, 0) return # add the widgets for i, wid in enumerate(list(wids_data.keys())): wd = wids_data[wid] if not isinstance(wd, dict) or wid == 'options': continue label = QLabel(wd.get('label', wid)) l.addWidget(label, i, 0) widget = BASE_WIDGETS.get(wd.get('widget', 'lineedit'), BASE_WIDGETS['lineedit'])() items = [it.strip() for it in wd.get('items', '').split('|')] v = self.template_values.get(wid) if not v or self.template_values.get('template') != tp: v = wd.get('value') if isinstance(widget, QComboBox) and items: widget.addItems(items) if v not in items: v = items[0] elif isinstance(widget, QListWidget) and items: widget.add_items(items) widget.setMaximumHeight(100) widget.updateValue('', v) widget.help_text = wd.get('help', 'No help available.') l.addWidget(widget, i, 1) wd['widget_obj'] = widget def populate_combobox_solver(self): """ Add items from self.solver_list to combobox, select the first item """ ui = self.ui ui.listwidget_solver_list.clear() ui.listwidget_solver_list.addItems(self.solver_list.keys()) def enable_restart_item(self, text, enable): cb = self.ui.combobox_restart model = cb.model() index = cb.findText(text) item = model.item(index) if not enable: flags = Qt.NoItemFlags else: flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled item.setFlags(flags) def update_dialog_options(self): """ Enable or disable options based on self.solver features, local or remote settings """ ui = self.ui status_text = '' get_value = self.parent.project.get_value # show status/errors error_msg = self.get_error() status = self.get_status() error = status == ERROR ui.toolbutton_view_error.setVisible(error) if error and error_msg: status_text = 'Solver Error:' # Enable/disable widgets enable = self.mfix_available and self.solver is not None and not error ui.button_run.setEnabled(enable) ui.widget_queue_options.setEnabled(enable) dmp = self.dmp_enabled() smp = self.smp_enabled() ui.groupbox_smp_options.setEnabled(enable and smp) ui.groupbox_dmp_options.setEnabled(enable and dmp) python = self.python_enabled() if status == LOOKING: status_text = 'Checking Solver.' elif not python and not error: status_text = 'Warning: Can not talk to selected solver (not python enabled).' ui.spinbox_nodesi.setEnabled(dmp) ui.spinbox_nodesj.setEnabled(dmp) ui.spinbox_nodesk.setEnabled(dmp and not get_value('no_k')) ui.spinbox_threads.setEnabled(smp) ui.label_solver_error.setText(status_text) def show_solver_error(self): error_msg = '\n'.join(self.get_error()) self.parent.message( text='The solver test failed with the following error:', info_text=error_msg) def popup(self): self.show() self.raise_() self.activateWindow() def closeEvent(self, event): """save information on close""" ui = self.ui # save solver list self.save_selected_exe() # queue self.save_template() self.update_gui_comment('submit_to_queue', int(ui.groupbox_queue.isChecked())) self.update_gui_comment('OMP_NUM_THREADS', str(ui.spinbox_threads.value())) self.parent.save_project() # event handlers def handle_abort(self): self.close() def finish_with_dialog(self): """ save run options in project file, then emit run signal """ ui = self.ui update_keyword = self.parent.update_keyword get_value = self.parent.project.get_value project_dir = self.parent.get_project_dir() udfs = glob(os.path.join(project_dir, "*.f")) udf_msg = ("Warning: Fortran source files exist for this project, but" " the selected mfixsolver is not in the project directory." " This case probably won't run correctly unless this" " project's custom mfixsolver is selected. Proceed anyway?") if udfs and "[project]" + os.sep + "mfixsolver" not in self.solver: response = self.parent.message(title='Warning', icon='question', text=udf_msg, buttons=['yes', 'no'], default='no') if response != 'yes': return thread_count = str(ui.spinbox_threads.value()) # FIXME: should not pollute local env (see saved_env) os.environ['OMP_NUM_THREADS'] = thread_count log.info('SMP enabled with OMP_NUM_THREADS=%s', os.environ["OMP_NUM_THREADS"]) self.update_gui_comment('OMP_NUM_THREADS', thread_count) # restart if ui.groupbox_restart.isChecked(): restart_type = RESTART_TYPES.get(ui.combobox_restart.currentText(), 'restart_1') update_keyword('run_type', restart_type) # restart_2 if restart_type == 'restart_2': spx_files = self.parent.get_output_files(RESTART_2_REMOVE_GLOB) if not self.parent.remove_output_files(spx_files, force_remove=True): log.debug('SP* files exist and run was canceled') return False # normal run else: update_keyword('run_type', 'new') output_files = self.parent.get_output_files() if output_files: message = 'Starting a new run requires the following files to be deleted from the run directory.' if not self.parent.remove_output_files( output_files, message_text=message, force_remove=True): log.info('output files exist and run was canceled') return False # collect nodes[ijk] nodesi = ui.spinbox_nodesi.value() nodesj = ui.spinbox_nodesj.value() nodesk = ui.spinbox_nodesk.value() if not self.dmp_enabled(): nodesi = nodesj = nodesk = 1 # write the correct nodes[ijk] to project file update_keyword('nodesi', nodesi) update_keyword('nodesj', nodesj) if not get_value('no_k'): update_keyword('nodesk', nodesk) else: update_keyword('nodesk', 1) if self.parent.unsaved_flag: # run_type keyword updated and/or nodesi/nodesj/nodesk self.parent.save_project() self.parent.update_source_view() else: stl = os.path.join(self.parent.get_project_dir(), 'geometry.stl') # is this needed? self.parent.vtkwidget.export_stl(stl) self.close() self.parent.signal_update_runbuttons.emit('') return True def handle_run(self): if not self.finish_with_dialog(): self.parent.slot_update_runbuttons() return # reset plots self.parent.reset_plots() self.parent.job_manager.stopping = False self.parent.job_manager.pausing = False self.parent.last_run_msg_time = 0.0 if self.ui.groupbox_queue.isChecked(): self.submit() else: self.run() self.parent.slot_update_runbuttons() def run(self): self.run_cmd = self.get_run_command() msg = 'Starting %s' % ' '.join(self.run_cmd) self.parent.print_internal(msg, color='blue') self.start_command(cmd=self.run_cmd, cwd=self.parent.get_project_dir(), env=os.environ) def submit(self): msg = 'Submitting to queue' self.parent.print_internal(msg, color='blue') self.submit_command(*self.get_submit_command()) def handle_remove_exe(self): ui = self.ui row = ui.listwidget_solver_list.currentRow() item = ui.listwidget_solver_list.currentItem() path = item.text() if not path.startswith('[default]') and not path.startswith( '[project'): ui.listwidget_solver_list.takeItem(row) def handle_browse_exe(self): """ Handle file open dialog for user specified exe """ new_exe, ignore = QFileDialog.getOpenFileName( self, "Select Executable", directory=self.project_dir, options=QFileDialog.DontResolveSymlinks) if not new_exe: return key = self.replace_solver_path(new_exe) lw = self.ui.listwidget_solver_list items = [lw.item(i).text() for i in range(0, lw.count())] if key in items: self.parent.message( text='The selected solver is already in the list of ' 'available solvers.') return # check solver ok, message = self.check_exe(new_exe) if not ok: self.parent.message(text=message) return self.save_selected_exe(new_exe) self.mfix_available = True lw.insertItem(0, key) lw.setCurrentRow(0) log.debug('selected new exe %s', key) def handle_browse_template(self): """ Handle file open dialog for user specified exe """ new_temp, ignore = QFileDialog.getOpenFileName( self, "Select a Template", directory=self.project_dir) if not new_temp: return self.add_queue_template(new_temp, select=True) # add it to the recent settings temp_paths = self.settings.value('queue_templates') good_paths = [os.path.realpath(new_temp)] if temp_paths: for temp_path in temp_paths.split('|'): if os.path.exists(temp_path): good_paths.append(temp_path) self.settings.setValue( 'queue_templates', '|'.join(list(set(good_paths))[:RECENT_EXE_LIMIT])) # utils def save_selected_exe(self, new_solver=None): """ add new executable to recent list, save in project file and config, send signal(s) """ if new_solver is None: new_solver = self.solver if new_solver is None: self.parent.warn('No solver selected') return key = self.replace_solver_path(new_solver) self.settings.setValue('mfix_exe', key) self.update_gui_comment('mfix_exe', key) lw = self.ui.listwidget_solver_list recent_list = [lw.item(i).text() for i in range(0, lw.count())] # truncate to maximum recent_list = recent_list[:RECENT_EXE_LIMIT] # make new solver in front if new_solver is not None: if new_solver in recent_list: recent_list.pop(recent_list.index(new_solver)) recent_list.insert(0, new_solver) # add to solver list nl = OrderedDict([(key, new_solver)]) for key in recent_list: val = self.solver_list.get(key, None) if val is not None: nl[key] = val self.solver_list = nl # save self.settings.setValue('recent_executables', str(os.pathsep).join(recent_list)) def get_solver_list(self): """ assemble list of executables from: - command line - project file 'mfix_exe' - project dir - config item 'recent_executables' - default install location """ def recently_used_executables(): recent_list = self.settings.value('recent_executables') if recent_list: # limit recently used exes to RECENT_EXE_LIMIT recent_lim = recent_list.split(os.pathsep)[:RECENT_EXE_LIMIT] recent_list = [ exe for exe in recent_lim if os.path.exists(exe) ] for recent_exe in recent_list: yield recent_exe def project_directory_executables(): for name in MFIXSOLVER_GLOB_NAMES: for exe in glob(os.path.join(self.project_dir, name)): yield os.path.realpath(exe) def project_file_executable(): project_exe = self.gui_comments.get('mfix_exe', None) if project_exe: yield project_exe def python_path(): for d in sys.path: # filter out empty strings and current directory from $PATH if d and d != os.path.curdir and os.path.isdir(d): for name in MFIXSOLVER_GLOB_NAMES: for exe in glob(os.path.join(d, name)): yield exe def os_path(): PATH = os.environ.get("PATH") if PATH: # using OrderedDict to preserve PATH order dirs = OrderedDict.fromkeys(PATH.split(os.pathsep)) else: dirs = OrderedDict() for d in dirs.keys(): # filter out empty strings and current directory from $PATH if d and d != os.path.curdir and os.path.isdir(d): for name in MFIXSOLVER_GLOB_NAMES: for exe in glob(os.path.join(d, name)): yield exe def mfix_build_directories(): for d in set([get_mfix_home()]): for name in MFIXSOLVER_GLOB_NAMES: for exe in glob(os.path.join(d, name)): yield exe def get_saved_exe(): last_exe = self.settings.value('mfix_exe') if last_exe and os.path.exists(last_exe): yield last_exe def command_line_option(): if self.commandline_option_exe and os.path.exists( self.commandline_option_exe): yield self.commandline_option_exe # Why this order? Shouldn't command_line_option or proj* be first? exe_list_order = [ command_line_option, project_file_executable, project_directory_executables, python_path, os_path, recently_used_executables, mfix_build_directories, get_saved_exe, ] # use an ordered dict because it acts like an ordered set self.solver_list = od = OrderedDict() # look for executables in the order listed in exe_list_order for exe_spec in exe_list_order: for exe in exe_spec(): # expand short hand if '[project]' in exe: exe = exe.replace('[project]', self.prj_dir()) elif '[default]' in exe: exe = exe.replace('[default]', self.def_dir()) # make sure it is an abs path exe = os.path.realpath(exe) # simple checking ok, message = self.check_exe(exe) # truncate paths to [project]/mfixsolver etc. key = self.replace_solver_path(exe) if not ok: self.parent.warn(message) elif key not in od: od[key] = exe def prj_dir(self): return os.path.realpath(self.project_dir) def def_dir(self): return os.path.dirname(os.path.realpath(sys.executable)) def replace_solver_path(self, path): if path.startswith(self.prj_dir()): path = path.replace(self.prj_dir(), '[project]') elif path.startswith(self.def_dir()): path = path.replace(self.def_dir(), '[default]') return path def check_exe(self, path): if not os.path.isfile(path): return False, '{} is not a file.'.format(path) # try executable if not os.access(path, os.X_OK): return False, '{} is not a executable.'.format(path) # windows, check extension if os.name == 'nt': ext = os.path.splitext(path)[-1] if not (ext.endswith('.exe') or ext.endswith('.bat')): return False, 'Extension {} is not recognized, must be .exe or .bat'.format( ext) return True, 'ok' def get_solver_key(self, solver): if solver is None: return None try: stat = os.stat(self.solver_list.get(solver)) except OSError as e: log.debug(str(e)) return None key = (stat, solver) return key def get_exe_flags(self, solver): """ get and cache (and update) executable features """ key = self.get_solver_key(solver) if key is None: return None # stat will have changed if the exe has been modified since last check if key in self.mfix_exe_cache: info = self.mfix_exe_cache[key] return info.get('flags') # spawn process to get flags else: self.set_solver_icon(solver, LOOKING) self.mfix_exe_cache[key] = { 'status': LOOKING, 'flags': None, 'stdout': [], 'stderror': [], 'error': [], } self.spawn_flag_process(*key) return LOOKING def spawn_flag_process(self, stat, solver): log.debug('Feature testing MFiX %s', solver) key = (stat, solver) proc = self.flag_processes.get(key, None) if proc is not None: proc.kill() exe = self.solver_list.get(solver) exe_dir = os.path.dirname(exe) proc = self.flag_processes[key] = QProcess() proc.setProcessEnvironment(QProcessEnvironment.systemEnvironment()) proc.readyReadStandardOutput.connect( lambda k=key: self.flag_process_out(k)) proc.readyReadStandardError.connect( lambda k=key: self.flag_process_error(k)) proc.finished.connect(lambda ecode, estat, k=key: self. flag_process_finished(k, ecode, estat)) proc.error.connect(lambda e, k=key: self.flag_process_error(k, e)) proc.setWorkingDirectory(exe_dir) proc.start(exe, ["--print-flags"]) def flag_process_error(self, key, error=None): info = self.mfix_exe_cache.get(key) self.set_solver_icon(key[1], ERROR) info['status'] = ERROR if error is None: error = bytes( self.flag_processes[key].readAllStandardError()).decode( 'utf-8', errors='ignore') info['stderror'].append(error) else: info['error'].append(error) log.debug("could not run {} --print-flags: {}".format(key[1], error)) def flag_process_out(self, key): info = self.mfix_exe_cache.get(key) out = bytes(self.flag_processes[key].readAllStandardOutput()).decode( 'utf-8', errors='ignore') info['stdout'].append(out) info['flags'] = str('\n'.join(info['stdout'])).strip() log.debug("stdout: {} --print-flags: {}".format(key[1], out)) def flag_process_finished(self, key, exit_code, exit_status): info = self.mfix_exe_cache.get(key, {}) status = info.get('status', ERROR) if info.get('stderror', []): status = ERROR if status == LOOKING: status = OK info['status'] = status self.set_solver_icon(key[1], status) if self.solver == key[1]: self.update_dialog_options() log.debug("finished: {} --print-flags: {}, {}".format( key[1], exit_code, exit_status)) def set_solver_icon(self, solver, status): items = self.ui.listwidget_solver_list.findItems( solver, Qt.MatchExactly) if not items: return item = items[0] icon = 'error_outline.svg' if status == LOOKING: icon = 'timelapse.svg' elif status == OK: icon = 'check_outline.svg' item.setIcon(get_icon(icon)) def dmp_enabled(self): flags = self.get_exe_flags(self.solver) dmp = False if flags is not None: dmp = 'dmp' in str(flags) return dmp def smp_enabled(self): flags = self.get_exe_flags(self.solver) smp = False if flags is not None: smp = 'smp' in str(flags) return smp def python_enabled(self): flags = self.get_exe_flags(self.solver) python = False if flags is not None: python = 'python' in str(flags) return python def get_error(self): key = self.get_solver_key(self.solver) if key is None: return None info = self.mfix_exe_cache.get(key, {}) return info.get('stderror', None) def get_status(self): key = self.get_solver_key(self.solver) if key is None: return None info = self.mfix_exe_cache.get(key, {}) return info.get('status', None) def get_run_command(self): get_value = self.parent.project.get_value # collect nodes[ijk] from project to guarantee that mpirun matches nodesi = get_value('nodesi', 1) nodesj = get_value('nodesj', 1) nodesk = get_value('nodesk', 1) np = nodesi * nodesj * nodesk if self.dmp_enabled() and np > 1: dmp = [ 'mpirun', # '-quiet', # '-mca', 'orte_create_session_dirs', 'true', '-mca', 'mpi_warn_on_fork', '0', '-np', str(nodesi * nodesj * nodesk) ] else: dmp = [] #if self.dmp_enabled(): # run_cmd += ['nodesi=%s'%nodesi, # 'nodesj=%s'%nodesj] # if not self.parent.project.get_value('no_k'): # run_cmd += ['nodesk=%s'%nodesk] if self.smp_enabled(): num_threads = str(self.ui.spinbox_threads.value()) smp = ['env', 'OMP_NUM_THREADS=%s' % num_threads] else: smp = [] run_cmd = smp + dmp + [self.solver_list.get(self.solver)] # Add 'server' flag to start HTTP server run_cmd += ['-s'] # Specify project file project_filename = self.parent.get_project_file() run_cmd += ['-f', project_filename] return run_cmd def get_submit_command(self): cmd = self.get_run_command() template_txt = self.ui.combobox_template.currentText() template = self.templates[template_txt] # collect widget values replace_dict = self.collect_template_values() replace_dict.update({ 'PROJECT_NAME': self.parent.project.get_value('run_name', default=''), 'COMMAND': ' '.join(cmd), 'MFIX_HOME': get_mfix_home(), }) # replace twice to make sure that any references added the first time # get replaced script = replace_with_dict(template['script'], replace_dict) script = replace_with_dict(script, replace_dict) sub_cmd = template['options'].get('submit', False) delete_cmd = template['options'].get('delete', False) # XXX status_cmd = template['options'].get('status', False) job_id_regex = template['options'].get('job_id_regex', None) ## FIXME, return something nicer than this 6-tuple return script, sub_cmd, delete_cmd, status_cmd, job_id_regex, replace_dict def submit_command(self, script, sub_cmd, delete_cmd, status_cmd, job_id_regex, replace_dict): self.remove_mfix_stop() if not sub_cmd: template_txt = self.ui.combobox_template.currentText() self.parent.error( ('The template file at: {}\n' 'does not have a submit_cmd defined').format(template_txt)) return self.parent.job_manager.submit_command(script, sub_cmd, delete_cmd, status_cmd, job_id_regex, replace_dict) def remove_mfix_stop(self): mfix_stop_file = os.path.join(self.parent.get_project_dir(), 'MFIX.STOP') if os.path.exists(mfix_stop_file): try: os.remove(mfix_stop_file) except OSError: self.parent.warn("Cannot remove %s", mfix_stop_file) return def start_command(self, cmd, cwd, env): """Start MFIX in QProcess""" self.cmdline = cmd # List of strings, same as psutil self.remove_mfix_stop() self.mfixproc = QProcess() if not self.mfixproc: log.warning("QProcess creation failed") return self.mfixproc.setWorkingDirectory(cwd) def slot_start(): # processId was only added in qt 5.3 if StrictVersion(QT_VERSION) > StrictVersion('5.3'): pid = self.mfixproc.processId() else: pid = self.mfixproc.pid() msg = "MFiX process %d is running" % pid self.parent.signal_update_runbuttons.emit(msg) def slot_read_out(): # Why convert to bytes then decode? out_str = bytes(self.mfixproc.readAllStandardOutput()).decode( 'utf-8', errors='ignore') self.parent.stdout_signal.emit(out_str) def slot_read_err(): err_str = bytes(self.mfixproc.readAllStandardError()).decode( 'utf-8', errors='ignore') self.parent.stderr_signal.emit(err_str) def slot_finish(status): # This should really be in the job manager if self.parent.job_manager.job: self.parent.job_manager.job.cleanup_and_exit() self.parent.job_manager.job = None msg = "MFiX process has stopped" self.parent.signal_update_runbuttons.emit(msg) if self.parent.job_manager.pidfile: try: os.unlink(self.parent.job_manager.pidfile) self.parent.job_manager.pidfile = None except OSError as e: if e.errno != errno.ENOENT: raise def slot_error(error): cmd_str = ' '.join(self.cmdline) if error == QProcess.FailedToStart: msg = "Process failed to start " + cmd_str elif error == QProcess.Crashed: msg = "Process exit " + cmd_str elif error == QProcess.Timedout: msg = "Process timeout " + cmd_str elif error in (QProcess.WriteError, QProcess.ReadError): msg = "Process communication error " + cmd_str else: msg = "Unknown error " + cmd_str log.warning(msg) # make the message print in red self.parent.stderr_signal.emit(msg) self.mfixproc.started.connect(slot_start) self.mfixproc.readyReadStandardOutput.connect(slot_read_out) self.mfixproc.readyReadStandardError.connect(slot_read_err) self.mfixproc.finished.connect(slot_finish) self.mfixproc.error.connect(slot_error) start_detached = True #if sys.platform.startswith('win') or 'mpirun' not in cmd: # start_detached = False # On Windows, start_detached gives a DOS box # What was the issue with mpirun? start_detached = False # https://bugreports.qt.io/browse/QTBUG-2284 # QProcessEnvironment does not work with startDetached, # fixed in Qt5.10 which we aren't using yet saved_env = None if not start_detached: process_env = QProcessEnvironment() add_env = process_env.insert else: add_env = os.environ.__setitem__ saved_env = os.environ.copy() for key, val in env.items(): add_env(key, val) add_env('MFIX_RUN_CMD', ' '.join(cmd)) if not start_detached: self.mfixproc.setProcessEnvironment(process_env) self.mfixproc.start(cmd[0], cmd[1:]) else: self.mfixproc.startDetached(cmd[0], cmd[1:]) # restore environment if saved_env: for (k, v) in list(os.environ.items()): if k not in saved_env: del os.environ[k] elif v != saved_env[k]: os.environ[k] = saved_env[k] # give gui a reference self.parent.mfix_process = self.mfixproc self.parent.slot_rundir_timer()
class CondaProcess(QObject): """Conda API modified to work with QProcess instead of popen.""" # Signals sig_finished = Signal(str, object, str) sig_partial = Signal(str, object, str) sig_started = Signal() ENCODING = 'ascii' ROOT_PREFIX = None def __init__(self, parent): QObject.__init__(self, parent) self._parent = parent self._output = None self._partial = None self._stdout = None self._error = None self._parse = False self._function_called = '' self._name = None self._process = QProcess() self.set_root_prefix() # Signals self._process.finished.connect(self._call_conda_ready) self._process.readyReadStandardOutput.connect(self._call_conda_partial) # --- Helpers # ------------------------------------------------------------------------- def _is_running(self): return self._process.state() != QProcess.NotRunning def _is_not_running(self): return self._process.state() == QProcess.NotRunning def _call_conda_partial(self): """ """ stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(stdout, CondaProcess.ENCODING) stderr = self._process.readAllStandardError() stderr = handle_qbytearray(stderr, CondaProcess.ENCODING) if self._parse: try: self._output = json.loads(stdout) except Exception: # Result is a partial json. Can only be parsed when finished self._output = stdout else: self._output = stdout self._partial = self._output self._stdout = self._output self._error = stderr self.sig_partial.emit(self._function_called, self._partial, self._error) def _call_conda_ready(self): """Function called when QProcess in _call_conda finishes task.""" function = self._function_called if self._stdout is None: stdout = to_text_string(self._process.readAllStandardOutput(), encoding=CondaProcess.ENCODING) else: stdout = self._stdout if self._error is None: stderr = to_text_string(self._process.readAllStandardError(), encoding=CondaProcess.ENCODING) else: stderr = self._error if function == 'get_conda_version': pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)') m = pat.match(stderr.strip()) if m is None: m = pat.match(stdout.strip()) if m is None: raise Exception('output did not match: {0}'.format(stderr)) self._output = m.group(1) elif function == 'config_path': result = self._output self._output = result['rc_path'] elif function == 'config_get': result = self._output self._output = result['get'] elif (function == 'config_delete' or function == 'config_add' or function == 'config_set' or function == 'config_remove'): result = self._output self._output = result.get('warnings', []) elif function == 'pip': result = [] lines = self._output.split('\n') for line in lines: if '<pip>' in line: temp = line.split()[:-1] + ['pip'] result.append('-'.join(temp)) self._output = result if stderr.strip(): self._error = stderr self._parse = False self.sig_finished.emit(self._function_called, self._output, self._error) def _abspath(self, abspath): """ """ if abspath: if sys.platform == 'win32': python = join(CondaProcess.ROOT_PREFIX, 'python.exe') conda = join(CondaProcess.ROOT_PREFIX, 'Scripts', 'conda-script.py') else: python = join(CondaProcess.ROOT_PREFIX, 'bin/python') conda = join(CondaProcess.ROOT_PREFIX, 'bin/conda') cmd_list = [python, conda] else: # Just use whatever conda/pip is on the path cmd_list = ['conda'] return cmd_list def _call_conda(self, extra_args, abspath=True): """ """ if self._is_not_running(): cmd_list = self._abspath(abspath) cmd_list.extend(extra_args) self._error, self._output = None, None self._process.start(cmd_list[0], cmd_list[1:]) self.sig_started.emit() def _call_pip(self, name=None, prefix=None, extra_args=None): """ """ if self._is_not_running(): cmd_list = self._pip_cmd(name=name, prefix=prefix) cmd_list.extend(extra_args) self._error, self._output = None, None self._parse = False self._process.start(cmd_list[0], cmd_list[1:]) self.sig_started.emit() def _call_and_parse(self, extra_args, abspath=True): """ """ self._parse = True self._call_conda(extra_args, abspath=abspath) def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()): """ """ cmd_list = [] if kwargs.get('override_channels', False) and 'channel' not in kwargs: raise TypeError('conda search: override_channels requires channel') if 'env' in kwargs: cmd_list.extend(['--name', kwargs.pop('env')]) if 'prefix' in kwargs: cmd_list.extend(['--prefix', kwargs.pop('prefix')]) if 'channel' in kwargs: channel = kwargs.pop('channel') if isinstance(channel, str): cmd_list.extend(['--channel', channel]) else: cmd_list.append('--channel') cmd_list.extend(channel) for key in keys: if key in kwargs and kwargs[key]: cmd_list.append('--' + key.replace('_', '-')) return cmd_list # --- Public api # ------------------------------------------------------------------------ @staticmethod def linked(prefix, as_spec=False): """ Return the (set of canonical names) of linked packages in `prefix`. """ if not isdir(prefix): raise Exception('no such directory: {0}'.format(prefix)) meta_dir = join(prefix, 'conda-meta') if not isdir(meta_dir): # We might have nothing in linked (and no conda-meta directory) result = set() result = set(fn[:-5] for fn in os.listdir(meta_dir) if fn.endswith('.json')) new_result = [] if as_spec: for r in result: n, v, b = CondaProcess.split_canonical_name(r) new_result.append("{0}={1}".format(n, v)) result = "\n".join(new_result) return result @staticmethod def split_canonical_name(cname): """ Split a canonical package name into (name, version, build) strings. """ result = tuple(cname.rsplit('-', 2)) return result def set_root_prefix(self, prefix=None): """ Set the prefix to the root environment (default is /opt/anaconda). """ if prefix: CondaProcess.ROOT_PREFIX = prefix else: # Find conda instance, and then use info() to get 'root_prefix' if CondaProcess.ROOT_PREFIX is None: info = self.info(abspath=False) CondaProcess.ROOT_PREFIX = info['root_prefix'] def get_conda_version(self): """ Return the version of conda being used (invoked) as a string. """ if self._is_not_running(): self._function_called = 'get_conda_version' self._call_conda(['--version']) def get_envs(self, emit=False): """ Return all of the (named) environments (this does not include the root environment), as a list of absolute paths to their prefixes. """ if self._is_not_running(): info = self.info() result = info['envs'] if emit: self.sig_finished.emit('get_envs', result, "") return result def get_prefix_envname(self, name, emit=False): """ Given the name of an environment return its full prefix path, or None if it cannot be found. """ if self._is_not_running(): if name == 'root': prefix = CondaProcess.ROOT_PREFIX for env_prefix in self.get_envs(): if basename(env_prefix) == name: prefix = env_prefix break if emit: self.sig_finished.emit('get_prefix_envname', prefix, "") return prefix def info(self, abspath=True, emit=False): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ if self._is_not_running(): qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(['info', '--json']) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) if emit: self.sig_finished.emit("info", str(info), "") return info def package_info(self, package, abspath=True): """ Return a dictionary with package information. Structure is { 'package_name': [{ 'depends': list, 'version': str, 'name': str, 'license': str, 'fn': ..., ... }] } """ if self._is_not_running(): self._function_called = 'package_info' self._call_and_parse(['info', package, '--json'], abspath=abspath) def search(self, regex=None, spec=None, **kwargs): """ Search for packages. """ cmd_list = ['search', '--json'] if regex and spec: raise TypeError('conda search: only one of regex or spec allowed') if regex: cmd_list.append(regex) if spec: cmd_list.extend(['--spec', spec]) if 'platform' in kwargs: platform = kwargs.pop('platform') platforms = ('win-32', 'win-64', 'osx-64', 'linux-32', 'linux-64') if platform not in platforms: raise TypeError('conda search: platform must be one of ' + ', '.join(platforms)) cmd_list.extend(['--platform', platform]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('canonical', 'unknown', 'use_index_cache', 'outdated', 'override_channels'))) if self._is_not_running(): self._function_called = 'search' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def share(self, prefix): """ Create a "share package" of the environment located in `prefix`, and return a dictionary with (at least) the following keys: - 'prefix': the full path to the created package - 'warnings': a list of warnings This file is created in a temp directory, and it is the callers responsibility to remove this directory (after the file has been handled in some way). """ if self._is_not_running: self._function_called = 'share' self._call_and_parse(['share', '--json', '--prefix', prefix]) def create(self, name=None, prefix=None, pkgs=None): """ Create an environment either by 'name' or 'prefix' with a specified set of packages. """ # TODO: Fix temporal hack if not pkgs or not isinstance(pkgs, (list, tuple, str)): raise TypeError('must specify a list of one or more packages to ' 'install into new environment') cmd_list = ['create', '--yes', '--quiet', '--json', '--mkdir'] if name: ref = name search = [os.path.join(d, name) for d in self.info()['envs_dirs']] cmd_list.extend(['--name', name]) elif prefix: ref = prefix search = [prefix] cmd_list.extend(['--prefix', prefix]) else: raise TypeError("Must specify either an environment 'name' or a " "'prefix' for new environment.") if any(os.path.exists(path) for path in search): raise CondaEnvExistsError('Conda environment [%s] already exists' % ref) # TODO: Fix temporal hack if isinstance(pkgs, (list, tuple)): cmd_list.extend(pkgs) elif isinstance(pkgs, str): cmd_list.extend(['--file', pkgs]) if self._is_not_running: self._function_called = 'create' self._call_conda(cmd_list) def install(self, name=None, prefix=None, pkgs=None, dep=True): """ Install packages into an environment either by 'name' or 'prefix' with a specified set of packages """ # TODO: Fix temporal hack if not pkgs or not isinstance(pkgs, (list, tuple, str)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--yes', '--json', '--force-pscheck'] if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: # just install into the current environment, whatever that is pass # TODO: Fix temporal hack if isinstance(pkgs, (list, tuple)): cmd_list.extend(pkgs) elif isinstance(pkgs, str): cmd_list.extend(['--file', pkgs]) if not dep: cmd_list.extend(['--no-deps']) if self._is_not_running: self._function_called = 'install' self._call_conda(cmd_list) def update(self, *pkgs, **kwargs): """ Update package(s) (in an environment) by name. """ cmd_list = ['update', '--json', '--quiet', '--yes'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to update, \ or all=True.") cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'no_deps', 'override_channels', 'no_pin', 'force', 'all', 'use_index_cache', 'use_local', 'alt_hint'))) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = 'update' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove(self, *pkgs, **kwargs): """ Remove a package (from an environment) by name. Returns { success: bool, (this is always true), (other information) } """ cmd_list = ['remove', '--json', '--quiet', '--yes', '--force-pscheck'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to remove, \ or all=True.") if kwargs.get('name') and kwargs.get('prefix'): raise TypeError("conda remove: At most one of 'name', 'prefix' " "allowed") if kwargs.get('name'): cmd_list.extend(['--name', kwargs.pop('name')]) if kwargs.get('prefix'): cmd_list.extend(['--prefix', kwargs.pop('prefix')]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'features', 'override_channels', 'no_pin', 'force', 'all'))) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = 'remove' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove_environment(self, name=None, prefix=None, **kwargs): """ Remove an environment entirely. See ``remove``. """ if self._is_not_running: self._function_called = 'remove_environment' self.remove(name=name, prefix=prefix, all=True, **kwargs) def clone_environment(self, clone, name=None, prefix=None, **kwargs): """ Clone the environment ``clone`` into ``name`` or ``prefix``. """ cmd_list = ['create', '--json', '--quiet'] if (name and prefix) or not (name or prefix): raise TypeError("conda clone_environment: exactly one of 'name' " "or 'prefix' required.") if name: cmd_list.extend(['--name', name]) if prefix: cmd_list.extend(['--prefix', prefix]) cmd_list.extend(['--clone', clone]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'unknown', 'use_index_cache', 'use_local', 'no_pin', 'force', 'all', 'channel', 'override_channels', 'no_default_packages'))) if self._is_not_running(): self._function_called = 'clone_environment' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def clone(self, path, prefix): """ Clone a "share package" (located at `path`) by creating a new environment at `prefix`, and return a dictionary with (at least) the following keys: - 'warnings': a list of warnings The directory `path` is located in, should be some temp directory or some other directory OUTSIDE /opt/anaconda. After calling this function, the original file (at `path`) may be removed (by the caller of this function). The return object is a list of warnings. """ if self._process.state() == QProcess.NotRunning: self._function_called = 'clone' self._call_and_parse(['clone', '--json', '--prefix', prefix, path]) def _setup_config_from_kwargs(kwargs): cmd_list = ['--json', '--force'] if 'file' in kwargs: cmd_list.extend(['--file', kwargs['file']]) if 'system' in kwargs: cmd_list.append('--system') return cmd_list def config_path(self, **kwargs): """ Get the path to the config file. """ cmd_list = ['config', '--get'] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_path' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_get(self, *keys, **kwargs): """ Get the values of configuration keys. Returns a dictionary of values. Note, the key may not be in the dictionary if the key wasn't set in the configuration file. """ cmd_list = ['config', '--get'] cmd_list.extend(keys) cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_get' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_set(self, key, value, **kwargs): """ Set a key to a (bool) value. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--set', key, str(value)] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_set' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_add(self, key, value, **kwargs): """ Add a value to a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--add', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_add' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_remove(self, key, value, **kwargs): """ Remove a value from a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_remove' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_delete(self, key, **kwargs): """ Remove a key entirely. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove-key', key] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_delete' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def run(self, command, abspath=True): """ Launch the specified app by name or full package name. Returns a dictionary containing the key "fn", whose value is the full package (ending in ``.tar.bz2``) of the app. """ cmd_list = ['run', '--json', command] if self._is_not_running: self._function_called = 'run' self._call_and_parse(cmd_list, abspath=abspath) # --- Additional methods not in conda-api # ------------------------------------------------------------------------ def dependencies(self, name=None, prefix=None, pkgs=None, dep=True): """ Get dependenciy list for packages to be installed into an environment defined either by 'name' or 'prefix'. """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--dry-run', '--json'] cmd_list = ['install', '--dry-run', '--json', '--force-pscheck'] if not dep: cmd_list.extend(['--no-deps']) if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: pass cmd_list.extend(pkgs) if self._is_not_running: self._function_called = 'install_dry' self._call_and_parse(cmd_list) def environment_exists(self, name=None, prefix=None, abspath=True, emit=False): """ Check if an environment exists by 'name' or by 'prefix'. If query is by 'name' only the default conda environments directory is searched. """ if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(['list', '--json']) if name: cmd_list.extend(['--name', name]) else: cmd_list.extend(['--prefix', prefix]) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) if emit: self.sig_finished.emit("info", unicode(info), "") return 'error' not in info def export(self, filename, name=None, prefix=None, emit=False): """ Export environment by 'prefix' or 'name' as yaml 'filename'. """ if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if self._is_not_running: if name: temporal_envname = name if prefix: temporal_envname = 'tempenv' + int(random.random()*10000000) envs_dir = self.info()['envs_dirs'][0] os.symlink(prefix, os.sep.join([envs_dir, temporal_envname])) cmd = self._abspath(True) cmd.extend(['env', 'export', '--name', temporal_envname, '--file', os.path.abspath(filename)]) qprocess = QProcess() qprocess.start(cmd[0], cmd[1:]) qprocess.waitForFinished() if prefix: os.unlink(os.sep.join([envs_dir, temporal_envname])) def update_environment(self, filename, name=None, prefix=None, emit=False): """ Set environment at 'prefix' or 'name' to match 'filename' spec as yaml. """ if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if self._is_not_running: if name: temporal_envname = name if prefix: temporal_envname = 'tempenv' + int(random.random()*10000000) envs_dir = self.info()['envs_dirs'][0] os.symlink(prefix, os.sep.join([envs_dir, temporal_envname])) cmd = self._abspath(True) cmd.extend(['env', 'update', '--name', temporal_envname, '--file', os.path.abspath(filename)]) qprocess = QProcess() qprocess.start(cmd[0], cmd[1:]) qprocess.waitForFinished() if prefix: os.unlink(os.sep.join([envs_dir, 'tempenv'])) @property def error(self): return self._error @property def output(self): return self._output # --- Pip commands # ------------------------------------------------------------------------- def _pip_cmd(self, name=None, prefix=None): """ Get pip location based on environment `name` or `prefix`. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' " "required.") if name and self.environment_exists(name=name): prefix = self.get_prefix_envname(name) if sys.platform == 'win32': python = join(prefix, 'python.exe') # FIXME: pip = join(prefix, 'pip.exe') # FIXME: else: python = join(prefix, 'bin/python') pip = join(prefix, 'bin/pip') cmd_list = [python, pip] return cmd_list def pip_list(self, name=None, prefix=None, abspath=True, emit=False): """ Get list of pip installed packages. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' " "required.") if self._is_not_running: cmd_list = self._abspath(abspath) if name: cmd_list.extend(['list', '--name', name]) if prefix: cmd_list.extend(['list', '--prefix', prefix]) qprocess = QProcess() qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) result = [] lines = output.split('\n') for line in lines: if '<pip>' in line: temp = line.split()[:-1] + ['pip'] result.append('-'.join(temp)) if emit: self.sig_finished.emit("pip", str(result), "") return result def pip_remove(self, name=None, prefix=None, pkgs=None): """ Remove a pip pacakge in given environment by 'name' or 'prefix'. """ if isinstance(pkgs, list) or isinstance(pkgs, tuple): pkg = ' '.join(pkgs) else: pkg = pkgs extra_args = ['uninstall', '--yes', pkg] if self._is_not_running(): self._function_called = 'pip_remove' self._call_pip(name=name, prefix=prefix, extra_args=extra_args)