@selenite/graph-editor
Version:
A graph editor for visual programming, based on rete and svelte.
381 lines (370 loc) • 14.9 kB
JavaScript
import { NodeComponent } from './NodeComponent';
export class PythonComponentData {
type;
data;
constructor(type, data) {
this.type = type;
this.data = data;
}
}
export class PythonNodeComponent extends NodeComponent {
static isDynamicInput(inputs) {
return Object.entries(inputs).some(([key, value]) => {
return value.some((data) => data.type === 'dynamic');
});
}
async data(inputs) {
const isDynamicInput = PythonNodeComponent.isDynamicInput(inputs);
if (!isDynamicInput) {
// convert inputs to regular inputs extracting data from PythonComponentData
const staticInputs = {};
for (const [key, value] of Object.entries(inputs)) {
staticInputs[key] = value.map((data) => data.data);
}
let staticData = this.node.data(staticInputs);
if (staticData instanceof Promise) {
staticData = await staticData;
}
const staticPyData = {};
// convert static data to PythonComponentData
for (const key in staticData) {
if (this.dynamicOutputs.has(key)) {
if (!(key in this.dataCodeGetters))
throw new Error(`Missing data code getter for ${key}`);
staticPyData[key] = new PythonComponentData('dynamic', await this.formatPythonVars(this.dataCodeGetters[key]()));
}
else
staticPyData[key] = new PythonComponentData('static', staticData[key]);
}
return staticPyData;
}
// Dynamic inputs case
const res = {};
for (const [key, dataGetter] of Object.entries(this.dataCodeGetters)) {
if (!dataGetter)
throw new Error(`Missing data code getter for ${key}`);
res[key] = new PythonComponentData('dynamic', await this.formatPythonVars(dataGetter(), inputs));
}
return res;
}
dataCodeGetters = {};
importsStatements = new Set();
code = [];
createdVariables = new Set();
actualCreatedVars = {};
dynamicOutputs = new Set();
classes = {};
initCode = [];
parseArguments = new Map();
codeTemplateGetters = new Map([['exec', this.getCodeTemplate]]);
newlinesBefore = 0;
constructor({ owner }) {
super({ id: 'python_NC', owner: owner });
}
setDataCodeGetter(key, codeGetter) {
this.dataCodeGetters[key] = codeGetter;
}
addImportStatement(...statements) {
for (const statement of statements) {
this.importsStatements.add(statement);
}
}
addParseArgument(params) {
if (this.parseArguments.has(params.name))
throw new Error(`Argument ${params.name} already exists`);
this.parseArguments.set(params.name, params);
}
addClass(code) {
const pattern = /.*?class\s+(\w+)\s*.*?:/s;
const name = pattern.exec(code)?.[1];
if (!name || name in this.classes)
throw new Error(`Class ${name} already exists`);
this.classes[name] = code.trim().replaceAll('\t', ' ');
}
// TODO; change init into getter
addInitCode(code) {
this.initCode.push(code.trim());
}
addCode(...code) {
this.code.push(...code);
}
addVariable(...names) {
this.addDynamicOutput(...names.filter((key) => key in this.node.outputs));
for (const name of names) {
this.setDataCodeGetter(name, () => `$(${name})`);
this.createdVariables.add(name);
}
}
addVariables(...names) {
this.addDynamicOutput(...names.filter((key) => key in this.node.outputs));
for (const name of names) {
this.setDataCodeGetter(name, () => `$(${name})`);
this.createdVariables.add(name);
}
}
addDynamicOutput(...name) {
for (const n of name) {
this.dynamicOutputs.add(n);
}
}
setCodeTemplateGetter(getter, key = 'exec') {
this.codeTemplateGetters.set(key, getter);
}
setEmptyNewlinesBefore(numNewlines) {
if (numNewlines < 0)
throw new Error('Number of newlines must be positive');
this.newlinesBefore = numNewlines;
}
getCodeTemplate() {
return `
{{this}}
{{exec}}
`;
}
assignActualVars(usedVars) {
for (const varName of this.createdVariables) {
let numAttempts = 0;
let attemptedVarName = varName;
while (usedVars.has(attemptedVarName)) {
numAttempts++;
attemptedVarName = varName + (numAttempts + 1);
}
this.actualCreatedVars[varName] = attemptedVarName;
usedVars.add(attemptedVarName);
}
return usedVars;
}
static toPythonData(data) {
if (data === undefined)
return 'None';
if (data === null)
return 'None';
if (typeof data === 'string')
return `"${data}"`;
if (typeof data === 'number')
return data.toString();
if (typeof data === 'boolean')
return data ? 'True' : 'False';
if (typeof data === 'object') {
if (Array.isArray(data)) {
return `[${data.map((item) => PythonNodeComponent.toPythonData(item)).join(', ')}]`;
}
else {
return `{${Object.entries(data)
.map(([key, value]) => `${key}: ${PythonNodeComponent.toPythonData(value)}`)
.join(', ')}}`;
}
}
throw new Error(`Cannot convert data to python: ${data}`);
}
async fetch() {
return await this.node.getFactory().pythonDataflowEngine.fetch(this.node.id);
}
async fetchInputs() {
try {
return await this.node.getFactory().pythonDataflowEngine.fetchInputs(this.node.id);
}
catch (e) {
const firstMatch = /"(.*?)"/.exec(e.message);
if (firstMatch) {
const nodeId = firstMatch[1];
console.error('Problematic node', this.node.getFactory().getEditor().getNode(nodeId));
}
throw e;
}
}
// TODO : python dataflow engine
async formatPythonVars(template, inputs) {
const varPattern = /([^$]*)\$(\(([^)]+)\)|\[([^]+)\])([^$]*)|([^]+)/g;
let matchVar;
let resCode = '';
if (inputs === undefined)
inputs = await this.fetchInputs();
while ((matchVar = varPattern.exec(template)) !== null) {
const codeBefore = matchVar[1];
const varName = matchVar[3];
const dataKey = matchVar[4];
const codeAfter = matchVar[5];
const codeWhenNoVar = matchVar[6];
if (codeBefore)
resCode += codeBefore;
if (dataKey) {
const data = this.node.getData(dataKey, inputs);
if (data !== undefined) {
resCode += PythonNodeComponent.toPythonData(data);
}
}
if (varName) {
// data is created by this node as a variable
if (varName in this.actualCreatedVars) {
resCode += this.actualCreatedVars[varName];
}
else {
if (varName in this.node.ingoingDataConnections) {
const input = inputs[varName][0];
if (input.type === 'dynamic') {
resCode += inputs[varName][0].data;
}
else {
resCode += PythonNodeComponent.toPythonData(input.data);
}
}
// data comes from control
else {
const data = this.node.getData(varName, inputs);
resCode += PythonNodeComponent.toPythonData(data);
}
}
}
if (codeAfter)
resCode += codeAfter;
if (codeWhenNoVar)
resCode += codeWhenNoVar;
}
return resCode;
}
static async collectPythonData(node, nodeInput, indentation, allVars) {
// Stop case
if (node === null || nodeInput === null) {
return {
importsStatements: new Set(),
code: '',
allVars: allVars,
classes: {},
initCode: [],
parserArguments: new Map()
};
}
if (nodeInput === 'exec')
allVars = node.pythonComponent.assignActualVars(allVars);
// console.log(node.pythonComponent.actualCreatedVars);
// Get code template
const getter = node.pythonComponent.codeTemplateGetters.get(nodeInput);
if (!getter)
throw new Error(`No code template getter for ${nodeInput}`);
const inputs = await node.fetchInputs();
// Cleanup code template
let codeTemplate = getter({ inputs }).replace(/^\n*([^]*?)\s*$/, '$1');
codeTemplate = await node.pythonComponent.formatPythonVars(codeTemplate);
// Add intentation in front of everyline of codeTemplate
codeTemplate = codeTemplate.replaceAll(/^(?!.*{{.*}}.*)/gm, indentation);
const templateVars = {};
let resImportsStatements = node.pythonComponent.importsStatements;
let resClasses = node.pythonComponent.classes;
let resInitCode = nodeInput !== 'exec'
? []
: await Promise.all(node.pythonComponent.initCode.map((code) => node.pythonComponent.formatPythonVars(code)));
let resParserArguments = node.pythonComponent.parseArguments;
// Pattern to match indendation and variables in code template
const pattern = /( *){{(.+?)}}(\??)/g;
// Iterate on code template variables
let match;
while ((match = pattern.exec(codeTemplate)) !== null) {
// Compute child indentation and ensure it is made of spaces
const childIndentation = match[1].replaceAll('\t', ' ') + indentation;
const key = match[2];
const pass = match[3];
if (key === 'this') {
templateVars[key] = (await Promise.all(node.pythonComponent.code.map(async (code) => childIndentation + (await node.pythonComponent.formatPythonVars(code))))).join('\n');
}
else {
const outgoer = node.getOutgoers(key.replace('_', '-'))?.at(0);
const conn = node.outgoingExecConnections[key.replace('_', '-')][0];
const targetInput = conn?.targetInput;
const childRes = await PythonNodeComponent.collectPythonData(outgoer, targetInput, childIndentation, allVars);
const { importsStatements, classes, initCode } = childRes;
let code = childRes.code;
if (pass && /^\s*$/.test(code))
code = childIndentation + 'pass';
allVars = childRes.allVars;
// Merge imports statements
resImportsStatements = new Set((function* () {
yield* resImportsStatements;
yield* importsStatements;
})());
// Merge classes
resClasses = {
...resClasses,
...classes
};
// Merge init code
resInitCode = [...resInitCode, ...initCode];
// Merge parser arguments
resParserArguments = new Map((function* () {
yield* resParserArguments;
yield* childRes.parserArguments;
})());
templateVars[key] = code;
}
}
codeTemplate = '\n'.repeat(node.pythonComponent.newlinesBefore) + codeTemplate;
// Remove redundant indendation since indendation is
// already included in child code
// Remove trailing ? (pass symbol) in code template
codeTemplate = codeTemplate.replaceAll(/^[\t ]*({{.*?}})\??(.*)$/gm, '$1$2');
//
// const resCodeTemplate = getMessageFormatter(codeTemplate).format(templateVars);
const resCodeTemplate = codeTemplate.replace(/{{(.*?)}}/g, (match, key) => {
const value = templateVars[key.trim()];
return value !== undefined ? value : match;
});
return {
importsStatements: resImportsStatements,
code: resCodeTemplate,
allVars: allVars,
classes: resClasses,
initCode: resInitCode,
parserArguments: resParserArguments
};
}
async toPython() {
// const PythonWorker = await import('./Python_NC.worker?worker');
// const worker = new PythonWorker.default();
// worker.postMessage("Hello world from window!")
// TODO: implement web worker
const { importsStatements, code, classes, initCode, parserArguments } = await PythonNodeComponent.collectPythonData(this.node, 'exec', ' ', new Set(['comm', 'rank', 'xml', 'args', 'xmlfile']));
const imports = [...importsStatements].join('\n');
const fClasses = Object.values(classes).join('\n\n');
const fInitCode = initCode.join('\n ');
const fParserArguments = [...parserArguments.values()]
.map((arg) => {
return `parser.add_argument('--${arg.name}', type=${arg.type}, required=${PythonNodeComponent.toPythonData(arg.required)}, help="${arg.help}")`;
})
.join('\n ');
const fParserArgumenntsExtracted = [...parserArguments.values()]
.map((arg) => {
return `${arg.name} = args.${arg.name}`;
})
.join('\n ');
return `
import argparse
from mpi4py import MPI
#GEOSX
from utilities.input import XML
${imports}
${fClasses ? '\n\n' : ''}${fClasses}${fClasses ? '\n\n' : ''}
def parse_args():
"""Get arguments
Returns:
argument '--xml': Input xml file for GEOSX
"""
parser = argparse.ArgumentParser(description="Modeling acquisition example")
parser.add_argument('--xml', type=str, required=True,
help="Input xml file for GEOSX")
${fParserArguments}
args ,_ = parser.parse_known_args()
return args
def main():
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
args = parse_args()
xmlfile = args.xml
${fParserArgumenntsExtracted}
xml = XML(xmlfile)
${fInitCode}
${code}
if __name__ == "__main__":
main()
`.trim();
}
}