def export(self): from tifffile import imsave from pyqtgraph import FileDialog start_doc = getattr(self.catalog, self.stream).metadata['start'] if 'FileName' in start_doc: current_dir = str(Path(start_doc['FileName']).parent) current_name = Path(start_doc['FileName']).stem elif 'sample_name' in start_doc: current_dir = str( Path.home()) # sample name is usually just te file stem current_name = Path(start_doc['sample_name']).stem else: current_dir = Path.home() current_name = 'xicamNCEM_output' # Get a file path to save to in current directory fd = FileDialog() fd.setNameFilter("TIF (*.tif)") fd.setDirectory(current_dir) fd.selectFile(current_name) fd.setFileMode(FileDialog.AnyFile) fd.setAcceptMode(FileDialog.AcceptSave) if fd.exec_(): file_names = fd.selectedFiles()[0] outPath = Path(file_names) else: return if outPath.suffix != '.tif': outPath = outPath.with_suffix('.tif') scale0, units0 = self._get_physical_size() # Avoid overflow in field of view later if units0[0] == 'm': units0 = ['um', 'um'] scale0 = [ii * 1e6 for ii in scale0] # Change scale from pixel to 1/pixel FOV = [1 / ii for ii in scale0] # Add units to metadata for Imagej type output metadata = {'unit': units0[0]} # Get the data and change to float image = self.xarray[self.currentIndex, :, :].astype('f') imsave(outPath, image, imagej=True, resolution=FOV, metadata=metadata)
class Flowchart(Node): sigFileLoaded = QtCore.Signal(object) sigFileSaved = QtCore.Signal(object) sigNodeCreated = QtCore.Signal(object) sigNodeChanged = QtCore.Signal(object) # called when output is expected to have changed def __init__(self, name=None, filePath=None, library=None, broker_addr="", graphmgr_addr="", checkpoint_addr="", prometheus_dir=None, hutch=None): super().__init__(name) self.socks = [] self.library = library or LIBRARY self.graphmgr_addr = graphmgr_addr self.source_library = None self.ctx = zmq.asyncio.Context() self.broker = self.ctx.socket( zmq.PUB) # used to create new node processes self.broker.connect(broker_addr) self.socks.append(self.broker) self.graphinfo = self.ctx.socket(zmq.SUB) self.graphinfo.setsockopt_string(zmq.SUBSCRIBE, '') self.graphinfo.connect(graphmgr_addr.info) self.socks.append(self.graphinfo) self.checkpoint = self.ctx.socket( zmq.SUB) # used to receive ctrlnode updates from processes self.checkpoint.setsockopt_string(zmq.SUBSCRIBE, '') self.checkpoint.connect(checkpoint_addr) self.socks.append(self.checkpoint) self.filePath = filePath self._graph = nx.MultiDiGraph() self.nextZVal = 10 self._widget = None self._scene = None self.deleted_nodes = [] self.prometheus_dir = prometheus_dir self.hutch = hutch def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def close(self): for sock in self.socks: sock.close(linger=0) if self._widget is not None: self._widget.graphCommHandler.close() self.ctx.term() def start_prometheus(self): port = Ports.Prometheus while True: try: pc.start_http_server(port) break except OSError: port += 1 if self.prometheus_dir: if not os.path.exists(self.prometheus_dir): os.makedirs(self.prometheus_dir) pth = f"drpami_{socket.gethostname()}_client.json" pth = os.path.join(self.prometheus_dir, pth) conf = [{"targets": [f"{socket.gethostname()}:{port}"]}] try: with open(pth, 'w') as f: json.dump(conf, f) except PermissionError: logging.error("Permission denied: %s", pth) pass def setLibrary(self, lib): self.library = lib self.widget().chartWidget.buildMenu() def nodes(self, **kwargs): return self._graph.nodes(**kwargs) def createNode(self, nodeType=None, name=None, pos=None): """Create a new Node and add it to this flowchart. """ if name is None: n = 0 while True: name = "%s.%d" % (nodeType, n) if name not in self._graph.nodes(): break n += 1 # create an instance of the node node = self.library.getNodeType(nodeType)(name) self.addNode(node, pos) return node def addNode(self, node, pos=None): """Add an existing Node to this flowchart. See also: createNode() """ if pos is None: pos = [0, 0] if type(pos) in [QtCore.QPoint, QtCore.QPointF]: pos = [pos.x(), pos.y()] item = node.graphicsItem() item.setZValue(self.nextZVal * 2) self.nextZVal += 1 self.viewBox.addItem(item) pos = (find_nearest(pos[0]), find_nearest(pos[1])) item.moveBy(*pos) self._graph.add_node(node.name(), node=node) node.sigClosed.connect(self.nodeClosed) node.sigTerminalConnected.connect(self.nodeConnected) node.sigTerminalDisconnected.connect(self.nodeDisconnected) node.sigNodeEnabled.connect(self.nodeEnabled) self.sigNodeCreated.emit(node) if node.isChanged(True, True): self.sigNodeChanged.emit(node) @asyncSlot(object, object) async def nodeClosed(self, node, input_vars): self._graph.remove_node(node.name()) await self.broker.send_string(node.name(), zmq.SNDMORE) await self.broker.send_pyobj(fcMsgs.CloseNode()) ctrl = self.widget() name = node.name() if hasattr(node, 'to_operation'): self.deleted_nodes.append(name) self.sigNodeChanged.emit(node) elif isinstance(node, SourceNode): await ctrl.features.discard(name, name) await ctrl.graphCommHandler.unview(name) elif node.viewable(): views = [] for term, in_var in input_vars.items(): discarded = await ctrl.features.discard(name, in_var) if discarded: views.append(in_var) if views: await ctrl.graphCommHandler.unview(views) def nodeConnected(self, localTerm, remoteTerm): if remoteTerm.isOutput() or localTerm.isCondition(): t = remoteTerm remoteTerm = localTerm localTerm = t localNode = localTerm.node().name() remoteNode = remoteTerm.node().name() key = localNode + '.' + localTerm.name( ) + '->' + remoteNode + '.' + remoteTerm.name() if not self._graph.has_edge(localNode, remoteNode, key=key): self._graph.add_edge(localNode, remoteNode, key=key, from_term=localTerm.name(), to_term=remoteTerm.name()) self.sigNodeChanged.emit(localTerm.node()) def nodeDisconnected(self, localTerm, remoteTerm): if remoteTerm.isOutput() or localTerm.isCondition(): t = remoteTerm remoteTerm = localTerm localTerm = t localNode = localTerm.node().name() remoteNode = remoteTerm.node().name() key = localNode + '.' + localTerm.name( ) + '->' + remoteNode + '.' + remoteTerm.name() if self._graph.has_edge(localNode, remoteNode, key=key): self._graph.remove_edge(localNode, remoteNode, key=key) self.sigNodeChanged.emit(localTerm.node()) @asyncSlot(object) async def nodeEnabled(self, root): enabled = root._enabled outputs = [n for n, d in self._graph.out_degree() if d == 0] sources_targets = list(it.product([root.name()], outputs)) ctrl = self.widget() views = [] for s, t in sources_targets: paths = list(nx.algorithms.all_simple_paths(self._graph, s, t)) for path in paths: for node in path: node = self._graph.nodes[node]['node'] name = node.name() node.nodeEnabled(enabled) if not enabled: if hasattr(node, 'to_operation'): self.deleted_nodes.append(name) elif node.viewable(): for term, in_var in node.input_vars().items(): discarded = await ctrl.features.discard( name, in_var) if discarded: views.append(in_var) else: node.changed = True if node.conditions(): preds = self._graph.predecessors(node.name()) preds = filter(lambda n: n.startswith("Filter"), preds) for filt in preds: node = self._graph.nodes[filt]['node'] node.nodeEnabled(enabled) if views: await ctrl.graphCommHandler.unview(views) await ctrl.applyClicked() def connectTerminals(self, term1, term2, type_file=None): """Connect two terminals together within this flowchart.""" term1.connectTo(term2, type_file=type_file) def chartGraphicsItem(self): """ Return the graphicsItem that displays the internal nodes and connections of this flowchart. Note that the similar method `graphicsItem()` is inherited from Node and returns the *external* graphical representation of this flowchart.""" return self.viewBox def widget(self): """ Return the control widget for this flowchart. This widget provides GUI access to the parameters for each node and a graphical representation of the flowchart. """ if self._widget is None: self._widget = FlowchartCtrlWidget(self, self.graphmgr_addr) self.scene = self._widget.scene() self.viewBox = self._widget.viewBox() return self._widget def saveState(self): """ Return a serializable data structure representing the current state of this flowchart. """ state = {} state['nodes'] = [] state['connects'] = [] state['viewbox'] = self.viewBox.saveState() for name, node in self.nodes(data='node'): cls = type(node) clsName = cls.__name__ ns = {'class': clsName, 'name': name, 'state': node.saveState()} state['nodes'].append(ns) for from_node, to_node, data in self._graph.edges(data=True): from_term = data['from_term'] to_term = data['to_term'] state['connects'].append((from_node, from_term, to_node, to_term)) state['source_configuration'] = self.widget( ).sourceConfigure.saveState() state['library'] = self.widget().libraryEditor.saveState() return state def restoreState(self, state): """ Restore the state of this flowchart from a previous call to `saveState()`. """ self.blockSignals(True) try: if 'source_configuration' in state: src_cfg = state['source_configuration'] self.widget().sourceConfigure.restoreState(src_cfg) if src_cfg['files']: self.widget().sourceConfigure.applyClicked() if 'library' in state: lib_cfg = state['library'] self.widget().libraryEditor.restoreState(lib_cfg) self.widget().libraryEditor.applyClicked() if 'viewbox' in state: self.viewBox.restoreState(state['viewbox']) nodes = state['nodes'] nodes.sort(key=lambda a: a['state']['pos'][0]) for n in nodes: if n['class'] == 'SourceNode': try: ttype = eval(n['state']['terminals']['Out']['ttype']) n['state']['terminals']['Out']['ttype'] = ttype node = SourceNode(name=n['name'], terminals=n['state']['terminals']) self.addNode(node=node) except Exception: printExc( "Error creating node %s: (continuing anyway)" % n['name']) else: try: node = self.createNode(n['class'], name=n['name']) except Exception: printExc( "Error creating node %s: (continuing anyway)" % n['name']) node.restoreState(n['state']) if hasattr(node, "display"): node.display(topics=None, terms=None, addr=None, win=None) if hasattr(node.widget, 'restoreState') and 'widget' in n['state']: node.widget.restoreState(n['state']['widget']) connections = {} with tempfile.NamedTemporaryFile(mode='w') as type_file: type_file.write("from mypy_extensions import TypedDict\n") type_file.write("from typing import *\n") type_file.write("import numbers\n") type_file.write("import amitypes\n") type_file.write("T = TypeVar('T')\n\n") nodes = self.nodes(data='node') for n1, t1, n2, t2 in state['connects']: try: node1 = nodes[n1] term1 = node1[t1] node2 = nodes[n2] term2 = node2[t2] self.connectTerminals(term1, term2, type_file) if term1.isInput() or term1.isCondition: in_name = node1.name() + '_' + term1.name() in_name = in_name.replace('.', '_') out_name = node2.name() + '_' + term2.name() out_name = out_name.replace('.', '_') else: in_name = node2.name() + '_' + term2.name() in_name = in_name.replace('.', '_') out_name = node1.name() + '_' + term1.name() out_name = out_name.replace('.', '_') connections[(in_name, out_name)] = (term1, term2) except Exception: print(node1.terminals) print(node2.terminals) printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2)) type_file.flush() status = subprocess.run( ["mypy", "--follow-imports", "silent", type_file.name], capture_output=True, text=True) if status.returncode != 0: lines = status.stdout.split('\n')[:-1] for line in lines: m = re.search(r"\"+(\w+)\"+", line) if m: m = m.group().replace('"', '') for i in connections: if i[0] == m: term1, term2 = connections[i] term1.disconnectFrom(term2) break elif i[1] == m: term1, term2 = connections[i] term1.disconnectFrom(term2) break finally: self.blockSignals(False) for name, node in self.nodes(data='node'): self.sigNodeChanged.emit(node) @asyncSlot(str) async def loadFile(self, fileName=None): """ Load a flowchart (*.fc) file. """ with open(fileName, 'r') as f: state = json.load(f) ctrl = self.widget() await ctrl.clear() self.restoreState(state) self.viewBox.autoRange() self.sigFileLoaded.emit(fileName) await ctrl.applyClicked(build_views=False) nodes = [] for name, node in self.nodes(data='node'): if node.viewed: nodes.append(node) await ctrl.chartWidget.build_views(nodes, ctrl=True, export=True) def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): """ Save this flowchart to a .fc file """ if fileName is None: if startDir is None: startDir = self.filePath if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.saveFile) return if not fileName.endswith('.fc'): fileName += ".fc" state = self.saveState() state = json.dumps(state, indent=2, separators=(',', ': '), sort_keys=True, cls=amitypes.TypeEncoder) with open(fileName, 'w') as f: f.write(state) f.write('\n') ctrl = self.widget() ctrl.graph_info.labels(self.hutch, ctrl.graph_name).info({'graph': state}) ctrl.chartWidget.updateStatus(f"Saved graph to: {fileName}") self.sigFileSaved.emit(fileName) async def clear(self): """ Remove all nodes from this flowchart except the original input/output nodes. """ for name, gnode in self._graph.nodes().items(): node = gnode['node'] await self.broker.send_string(name, zmq.SNDMORE) await self.broker.send_pyobj(fcMsgs.CloseNode()) node.close(emit=False) self._graph = nx.MultiDiGraph() async def updateState(self): while True: node_name = await self.checkpoint.recv_string() # there is something wrong with this # in ami.client.flowchart.NodeProcess.send_checkpoint we send a # fcMsgs.NodeCheckPoint but we are only ever receiving the state new_node_state = await self.checkpoint.recv_pyobj() node = self._graph.nodes[node_name]['node'] current_node_state = node.saveState() restore_ctrl = False restore_widget = False if 'ctrl' in new_node_state: if current_node_state['ctrl'] != new_node_state['ctrl']: current_node_state['ctrl'] = new_node_state['ctrl'] restore_ctrl = True if 'widget' in new_node_state: if current_node_state['widget'] != new_node_state['widget']: restore_widget = True current_node_state['widget'] = new_node_state['widget'] if 'geometry' in new_node_state: node.geometry = QtCore.QByteArray.fromHex( bytes(new_node_state['geometry'], 'ascii')) if restore_ctrl or restore_widget: node.restoreState(current_node_state) node.changed = node.isChanged(restore_ctrl, restore_widget) if node.changed: self.sigNodeChanged.emit(node) node.viewed = new_node_state['viewed'] async def updateSources(self, init=False): num_workers = None while True: topic = await self.graphinfo.recv_string() source = await self.graphinfo.recv_string() msg = await self.graphinfo.recv_pyobj() if topic == 'sources': source_library = SourceLibrary() for source, node_type in msg.items(): pth = [] for part in source.split(':')[:-1]: if pth: part = ":".join((pth[-1], part)) pth.append(part) source_library.addNodeType(source, amitypes.loads(node_type), [pth]) self.source_library = source_library if init: break ctrl = self.widget() tree = ctrl.ui.source_tree ctrl.ui.clear_model(tree) ctrl.ui.create_model(ctrl.ui.source_tree, self.source_library.getLabelTree()) ctrl.chartWidget.updateStatus("Updated sources.") elif topic == 'event_rate': if num_workers is None: ctrl = self.widget() compiler_args = await ctrl.graphCommHandler.compilerArgs num_workers = compiler_args['num_workers'] events_per_second = [None] * num_workers total_events = [None] * num_workers time_per_event = msg[ctrl.graph_name] worker = int(re.search(r'(\d)+', source).group()) events_per_second[worker] = len(time_per_event) / ( time_per_event[-1][1] - time_per_event[0][0]) total_events[worker] = msg['num_events'] if all(events_per_second): events_per_second = int(np.sum(events_per_second)) total_num_events = int(np.sum(total_events)) ctrl = self.widget() ctrl.ui.rateLbl.setText( f"Num Events: {total_num_events} Events/Sec: {events_per_second}" ) events_per_second = [None] * num_workers total_events = [None] * num_workers elif topic == 'error': ctrl = self.widget() if hasattr(msg, 'node_name'): node_name = ctrl.metadata[msg.node_name]['parent'] node = self.nodes(data='node')[node_name] node.setException(msg) ctrl.chartWidget.updateStatus( f"{source} {node.name()}: {msg}", color='red') else: ctrl.chartWidget.updateStatus(f"{source}: {msg}", color='red') async def run(self): await asyncio.gather(self.updateState(), self.updateSources())
class LogWidget(QtGui.QWidget): """A widget to show log entries and filter them. """ sigDisplayEntry = QtCore.Signal(object) ## for thread-safetyness sigAddEntry = QtCore.Signal(object) ## for thread-safetyness sigScrollToAnchor = QtCore.Signal(object) # for internal use. Stylesheet = """ body {color: #000; font-family: sans;} .entry {} .error .message {color: #900} .warning .message {color: #740} .user .message {color: #009} .status .message {color: #090} .logExtra {margin-left: 40px;} .traceback {color: #555; height: 0px;} .timestamp {color: #000;} """ pageTemplate = """ <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <style type="text/css"> %s </style> <script type="text/javascript"> function showDiv(id) { div = document.getElementById(id); div.style.visibility = "visible"; div.style.height = "auto"; } </script> </head> <body> </body> </html> """ % Stylesheet def __init__(self, parent): """Creates the log widget. @param object parent: Qt parent object for log widet """ QtGui.QWidget.__init__(self, parent) self.ui = LogWidgetTemplate.Ui_Form() self.ui.setupUi(self) #self.ui.input.hide() self.ui.filterTree.topLevelItem(1).setExpanded(True) self.entries = [] ## stores all log entries in memory self.cache = { } ## for storing html strings of entries that have already been processed self.displayedEntries = [] self.typeFilters = [] self.importanceFilter = 0 self.dirFilter = False self.entryArrayBuffer = np.zeros( 1000, dtype=[ ### a record array for quick filtering of entries ('index', 'int32'), ('importance', 'int32'), ('msgType', '|S10'), ('directory', '|S100'), ('entryId', 'int32') ]) self.entryArray = self.entryArrayBuffer[:0] self.filtersChanged() self.sigDisplayEntry.connect(self.displayEntry, QtCore.Qt.QueuedConnection) self.sigAddEntry.connect(self.addEntry, QtCore.Qt.QueuedConnection) self.ui.exportHtmlBtn.clicked.connect(self.exportHtml) self.ui.filterTree.itemChanged.connect(self.setCheckStates) self.ui.importanceSlider.valueChanged.connect(self.filtersChanged) self.ui.output.anchorClicked.connect(self.linkClicked) self.sigScrollToAnchor.connect(self.scrollToAnchor, QtCore.Qt.QueuedConnection) def loadFile(self, f): """Load a log file for display. @param str f: path to file that should be laoded. f must be able to be read by pyqtgraph configfile.py """ log = configfile.readConfigFile(f) self.entries = [] self.entryArrayBuffer = np.zeros(len(log), dtype=[('index', 'int32'), ('importance', 'int32'), ('msgType', '|S10'), ('directory', '|S100'), ('entryId', 'int32')]) self.entryArray = self.entryArrayBuffer[:] i = 0 for k, v in log.items(): v['id'] = k[ 9:] ## record unique ID to facilitate HTML generation (javascript needs this ID) self.entries.append(v) self.entryArray[i] = np.array( [(i, v.get('importance', 5), v.get('msgType', 'status'), v.get('currentDir', ''), v.get('entryId', v['id']))], dtype=[('index', 'int32'), ('importance', 'int32'), ('msgType', '|S10'), ('directory', '|S100'), ('entryId', 'int32')]) i += 1 self.filterEntries( ) ## puts all entries through current filters and displays the ones that pass def addEntry(self, entry): """Add a log entry to the list. """ ## All incoming messages begin here ## for thread-safetyness: isGuiThread = QtCore.QThread.currentThread( ) == QtCore.QCoreApplication.instance().thread() if not isGuiThread: self.sigAddEntry.emit(entry) return self.entries.append(entry) i = len(self.entryArray) entryDir = entry.get('currentDir', None) if entryDir is None: entryDir = '' arr = np.array([ (i, entry['importance'], entry['msgType'], entryDir, entry['id']) ], dtype=[('index', 'int32'), ('importance', 'int32'), ('msgType', '|S10'), ('directory', '|S100'), ('entryId', 'int32')]) ## make more room if needed if len(self.entryArrayBuffer) == len(self.entryArray): newArray = np.empty( len(self.entryArrayBuffer) + 1000, self.entryArrayBuffer.dtype) newArray[:len(self.entryArray)] = self.entryArray self.entryArrayBuffer = newArray self.entryArray = self.entryArrayBuffer[:len(self.entryArray) + 1] self.entryArray[i] = arr self.checkDisplay( entry) ## displays the entry if it passes the current filters def setCheckStates(self, item, column): if item == self.ui.filterTree.topLevelItem(1): if item.checkState(0): for i in range(item.childCount()): item.child(i).setCheckState(0, QtCore.Qt.Checked) elif item.parent() == self.ui.filterTree.topLevelItem(1): if not item.checkState(0): self.ui.filterTree.topLevelItem(1).setCheckState( 0, QtCore.Qt.Unchecked) self.filtersChanged() def filtersChanged(self): ### Update self.typeFilters, self.importanceFilter, and self.dirFilter to reflect changes. tree = self.ui.filterTree self.typeFilters = [] for i in range(tree.topLevelItem(1).childCount()): child = tree.topLevelItem(1).child(i) if tree.topLevelItem(1).checkState(0) or child.checkState(0): text = child.text(0) self.typeFilters.append(str(text)) self.importanceFilter = self.ui.importanceSlider.value() # self.updateDirFilter() self.filterEntries() # def updateDirFilter(self, dh=None): # if self.ui.filterTree.topLevelItem(0).checkState(0): # if dh==None: # self.dirFilter = self.manager.getDirOfSelectedFile().name() # else: # self.dirFilter = dh.name() # else: # self.dirFilter = False def filterEntries(self): """Runs each entry in self.entries through the filters and displays if it makes it through.""" ### make self.entries a record array, then filtering will be much faster (to OR true/false arrays, + them) typeMask = self.entryArray['msgType'] == b'' for t in self.typeFilters: typeMask += self.entryArray['msgType'] == t.encode('ascii') mask = (self.entryArray['importance'] > self.importanceFilter) * typeMask #if self.dirFilter != False: # d = np.ascontiguousarray(self.entryArray['directory']) # j = len(self.dirFilter) # i = len(d) # d = d.view(np.byte).reshape(i, 100)[:, :j] # d = d.reshape(i*j).view('|S%d' % j) # mask *= (d == self.dirFilter) self.ui.output.clear() self.ui.output.document().setDefaultStyleSheet(self.Stylesheet) indices = list(self.entryArray[mask]['index']) self.displayEntry([self.entries[i] for i in indices]) def checkDisplay(self, entry): ### checks whether entry passes the current filters and displays it if it does. if entry['msgType'] not in self.typeFilters: return elif entry['importance'] < self.importanceFilter: return elif self.dirFilter is not False: if entry['currentDir'][:len(self.dirFilter)] != self.dirFilter: return else: self.displayEntry([entry]) def displayEntry(self, entries): ## entries should be a list of log entries ## for thread-safetyness: isGuiThread = QtCore.QThread.currentThread( ) == QtCore.QCoreApplication.instance().thread() if not isGuiThread: self.sigDisplayEntry.emit(entries) return for entry in entries: if id(entry) not in self.cache: self.cache[id(entry)] = self.generateEntryHtml(entry) html = self.cache[id(entry)] sb = self.ui.output.verticalScrollBar() isMax = sb.value() == sb.maximum() self.ui.output.append(html) self.displayedEntries.append(entry) if isMax: ## can't scroll to end until the web frame has processed the html change #frame.setScrollBarValue(QtCore.Qt.Vertical, frame.scrollBarMaximum(QtCore.Qt.Vertical)) ## Calling processEvents anywhere inside an error handler is forbidden ## because this can lead to Qt complaining about paint() recursion. self.sigScrollToAnchor.emit(str( entry['id'])) ## queued connection def scrollToAnchor(self, anchor): self.ui.output.scrollToAnchor(anchor) def generateEntryHtml(self, entry): msg = self.cleanText(entry['message']) reasons = "" docs = "" exc = "" if 'reasons' in entry: reasons = self.formatReasonStrForHTML(entry['reasons']) if 'docs' in entry: docs = self.formatDocsStrForHTML(entry['docs']) if entry.get('exception', None) is not None: exc = self.formatExceptionForHTML(entry, entryId=entry['id']) extra = reasons + docs + exc if extra != "": #extra = "<div class='logExtra'>" + extra + "</div>" extra = "<table class='logExtra'><tr><td>" + extra + "</td></tr></table>" #return """ #<div class='entry'> #<div class='%s'> #<span class='timestamp'>%s</span> #<span class='message'>%s</span> #%s #</div> #</div> #""" % (entry['msgType'], entry['timestamp'], msg, extra) return """ <a name="%s"/><table class='entry'><tr><td> <table class='%s'><tr><td> <span class='timestamp'>%s</span> <span class='message'>%s</span> %s </td></tr></table> </td></tr></table> """ % (str( entry['id']), entry['msgType'], entry['timestamp'], msg, extra) @staticmethod def cleanText(text): text = re.sub(r'&', '&', text) text = re.sub(r'>', '>', text) text = re.sub(r'<', '<', text) text = re.sub(r'\n', '<br/>\n', text) return text def formatExceptionForHTML(self, entry, exception=None, count=1, entryId=None): ### Here, exception is a dict that holds the message, reasons, docs, traceback and oldExceptions (which are also dicts, with the same entries) ## the count and tracebacks keywords are for calling recursively if exception is None: exception = entry['exception'] #if tracebacks is None: #tracebacks = [] indent = 10 text = self.cleanText(exception['message']) text = re.sub(r'^HelpfulException: ', '', text) messages = [text] if 'reasons' in exception: reasons = self.formatReasonsStrForHTML(exception['reasons']) text += reasons #self.displayText(reasons, entry, color, clean=False) if 'docs' in exception: docs = self.formatDocsStrForHTML(exception['docs']) #self.displayText(docs, entry, color, clean=False) text += docs traceback = [ self.formatTracebackForHTML(exception['traceback'], count) ] text = [text] if 'oldExc' in exception: exc, tb, msgs = self.formatExceptionForHTML(entry, exception['oldExc'], count=count + 1) text.extend(exc) messages.extend(msgs) traceback.extend(tb) #else: #if len(tracebacks)==count+1: #n=0 #else: #n=1 #for i, tb in enumerate(tracebacks): #self.displayTraceback(tb, entry, number=i+n) if count == 1: exc = "<div class=\"exception\"><ol>" + "\n".join( ["<li>%s</li>" % ex for ex in text]) + "</ol></div>" tbStr = "\n".join([ "<li><b>%s</b><br/><span class='traceback'>%s</span></li>" % (messages[i], tb) for i, tb in enumerate(traceback) ]) #traceback = "<div class=\"traceback\" id=\"%s\"><ol>"%str(entryId) + tbStr + "</ol></div>" entry['tracebackHtml'] = tbStr #return exc + '<a href="#" onclick="showDiv(\'%s\')">Show traceback</a>'%str(entryId) + traceback return exc + '<a href="exc:%s">Show traceback %s</a>' % ( str(entryId), str(entryId)) else: return text, traceback, messages def formatTracebackForHTML(self, tb, number): try: tb = [ line for line in tb if not line.startswith("Traceback (most recent call last)") ] except: print("\n" + str(tb) + "\n") raise return re.sub(" ", " ", ("").join(map(self.cleanText, tb)))[:-1] #tb = [self.cleanText(strip(x)) for x in tb] #lines = [] #prefix = '' #for l in ''.join(tb).split('\n'): #if l == '': #continue #if l[:9] == "Traceback": #prefix = ' ' + str(number) + '. ' #continue #spaceCount = 0 #while l[spaceCount] == ' ': #spaceCount += 1 #if prefix is not '': #spaceCount -= 1 #lines.append(" "*(spaceCount*4) + prefix + l) #prefix = '' #return '<div class="traceback">' + '<br />'.join(lines) + '</div>' #self.displayText('<br />'.join(lines), entry, color, clean=False) def formatReasonsStrForHTML(self, reasons): #indent = 6 reasonStr = "<table class='reasons'><tr><td>Possible reasons include:\n<ul>\n" for r in reasons: r = self.cleanText(r) reasonStr += "<li>" + r + "</li>\n" #reasonStr += " "*22 + chr(97+i) + ". " + r + "<br>" reasonStr += "</ul></td></tr></table>\n" return reasonStr def formatDocsStrForHTML(self, docs): #indent = 6 docStr = "<div class='docRefs'>Relevant documentation:\n<ul>\n" for d in docs: d = self.cleanText(d) docStr += "<li><a href=\"doc:%s\">%s</a></li>\n" % (d, d) docStr += "</ul></div>\n" return docStr def exportHtml(self, fileName=False): if fileName is False: self.fileDialog = FileDialog(self, "Save HTML as...", "htmltemp.log") #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.exportHtml) return if fileName[-5:] != '.html': fileName += '.html' #doc = self.ui.output.document().toHtml('utf-8') #for e in self.displayedEntries: #if e.has_key('tracebackHtml'): #doc = re.sub(r'<a href="exc:%s">(<[^>]+>)*Show traceback %s(<[^>]+>)*</a>'%(str(e['id']), str(e['id'])), e['tracebackHtml'], doc) doc = self.pageTemplate for e in self.displayedEntries: doc += self.cache[id(e)] for e in self.displayedEntries: if 'tracebackHtml' in e: doc = re.sub( r'<a href="exc:%s">(<[^>]+>)*Show traceback %s(<[^>]+>)*</a>' % (str(e['id']), str(e['id'])), e['tracebackHtml'], doc) f = open(fileName, 'w') f.write(doc) f.close() def linkClicked(self, url): url = url.toString() if url[:4] == 'doc:': #self.manager.showDocumentation(url[4:]) print("Not implemented") elif url[:4] == 'exc:': cursor = self.ui.output.document().find('Show traceback %s' % url[4:]) try: tb = self.entries[int(url[4:]) - 1]['tracebackHtml'] except IndexError: try: tb = self.entries[self.entryArray[ self.entryArray['entryId'] == ( int(url[4:]))]['index']]['tracebackHtml'] except: print("requested index %d, but only %d entries exist." % (int(url[4:]) - 1, len(self.entries))) raise cursor.insertHtml(tb) def clear(self): self.ui.output.clear() self.displayedEntryies = []
class ExportWidget(QtWidgets.QWidget): def __init__(self, node, text): super().__init__() self.node = node self.text = text self.setWindowTitle("Export") self.layout = QtWidgets.QFormLayout(self) self.setLayout(self.layout) self.name = QtWidgets.QLineEdit(parent=self) self.docstring = QtWidgets.QTextEdit(parent=self) self.ok = QtWidgets.QPushButton("Ok", parent=self) self.ok.clicked.connect(self.ok_clicked) self.layout.addRow("Name:", self.name) self.layout.addRow("Docstring:", self.docstring) self.layout.addWidget(self.ok) def ok_clicked(self): self.fileDialog = FileDialog(None, "Save File..", '.', "Python (*.py)") self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.saveFile) def saveFile(self, fileName): node_name = self.name.text() docstring = self.docstring.toPlainText() terminals = {} for name, term in self.node.terminals.items(): state = term.saveState() state['ttype'] = fullname(term._type) terminals[name] = state template = self.export(node_name, docstring, terminals, self.text) if not fileName.endswith('.py'): fileName += '.py' with open(fileName, 'w') as f: f.write(template) def export(self, name, docstring, terminals, text): template = f""" from typing import Any from amitypes import Array1d, Array2d, Array3d from ami.flowchart.Node import Node import ami.graph_nodes as gn {text} class {name}(Node): \""" {docstring} \""" nodeName = "{name}" def __init__(self, name): super().__init__(name, terminals={terminals}) def to_operation(self, **kwargs): proc = EventProcessor() return gn.Map(name=self.name()+"_operation", **kwargs, func=proc.on_event, begin_run=proc.begin_run, end_run=proc.end_run, begin_step=proc.begin_step, end_step=proc.end_step) """ return template