From 7386b746c4f1d31a0beeb3c08048554040b1a4b4 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Tue, 26 Sep 2023 21:40:59 -0400 Subject: [PATCH 1/6] fix(bindgen): run functionName preRun on init The constructor or the event could come first. --- .../compare-double-images-controller.ts | 8 ++++--- .../demo-app/vector-magnitude-controller.ts | 8 ++++--- .../demo-app/compress-stringify-controller.ts | 8 ++++--- .../typescript/test/browser/demo-app/index.ts | 4 ++-- .../parse-string-decompress-controller.ts | 8 ++++--- ...-presentation-state-to-image-controller.ts | 8 ++++--- .../typescript/test/browser/demo-app/index.ts | 12 +++++----- .../demo-app/read-dicom-tags-controller.ts | 8 ++++--- .../structured-report-to-text-controller.ts | 8 ++++--- .../interface-functions-demo-typescript.js | 8 ++++--- src/bindgen/typescript/typescript-bindings.js | 22 ++++++++++--------- 11 files changed, 60 insertions(+), 42 deletions(-) diff --git a/packages/compare-images/typescript/test/browser/demo-app/compare-double-images-controller.ts b/packages/compare-images/typescript/test/browser/demo-app/compare-double-images-controller.ts index dc3841ac5..de2ddd96a 100644 --- a/packages/compare-images/typescript/test/browser/demo-app/compare-double-images-controller.ts +++ b/packages/compare-images/typescript/test/browser/demo-app/compare-double-images-controller.ts @@ -147,19 +147,21 @@ class CompareDoubleImagesController { const url = new URL(document.location) url.search = params window.history.replaceState({ functionName: 'compareDoubleImages' }, '', url) + await preRun() } - await preRun() } } const tabGroup = document.querySelector('sl-tab-group') tabGroup.addEventListener('sl-tab-show', onSelectTab) - document.addEventListener('DOMContentLoaded', () => { + function onInit() { const params = new URLSearchParams(window.location.search) if (params.has('functionName') && params.get('functionName') === 'compareDoubleImages') { + tabGroup.show('compareDoubleImages-panel') preRun() } - }) + } + onInit() const runButton = document.querySelector('#compareDoubleImagesInputs sl-button[name="run"]') runButton.addEventListener('click', async (event) => { diff --git a/packages/compare-images/typescript/test/browser/demo-app/vector-magnitude-controller.ts b/packages/compare-images/typescript/test/browser/demo-app/vector-magnitude-controller.ts index 6c9a3c94b..527503f76 100644 --- a/packages/compare-images/typescript/test/browser/demo-app/vector-magnitude-controller.ts +++ b/packages/compare-images/typescript/test/browser/demo-app/vector-magnitude-controller.ts @@ -88,19 +88,21 @@ class VectorMagnitudeController { const url = new URL(document.location) url.search = params window.history.replaceState({ functionName: 'vectorMagnitude' }, '', url) + await preRun() } - await preRun() } } const tabGroup = document.querySelector('sl-tab-group') tabGroup.addEventListener('sl-tab-show', onSelectTab) - document.addEventListener('DOMContentLoaded', () => { + function onInit() { const params = new URLSearchParams(window.location.search) if (params.has('functionName') && params.get('functionName') === 'vectorMagnitude') { + tabGroup.show('vectorMagnitude-panel') preRun() } - }) + } + onInit() const runButton = document.querySelector('#vectorMagnitudeInputs sl-button[name="run"]') runButton.addEventListener('click', async (event) => { diff --git a/packages/compress-stringify/typescript/test/browser/demo-app/compress-stringify-controller.ts b/packages/compress-stringify/typescript/test/browser/demo-app/compress-stringify-controller.ts index fbf209c3f..f8afafe90 100644 --- a/packages/compress-stringify/typescript/test/browser/demo-app/compress-stringify-controller.ts +++ b/packages/compress-stringify/typescript/test/browser/demo-app/compress-stringify-controller.ts @@ -94,19 +94,21 @@ class CompressStringifyController { const url = new URL(document.location) url.search = params window.history.replaceState({ functionName: 'compressStringify' }, '', url) + await preRun() } - await preRun() } } const tabGroup = document.querySelector('sl-tab-group') tabGroup.addEventListener('sl-tab-show', onSelectTab) - document.addEventListener('DOMContentLoaded', () => { + function onInit() { const params = new URLSearchParams(window.location.search) if (params.has('functionName') && params.get('functionName') === 'compressStringify') { + tabGroup.show('compressStringify-panel') preRun() } - }) + } + onInit() const runButton = document.querySelector('#compressStringifyInputs sl-button[name="run"]') runButton.addEventListener('click', async (event) => { diff --git a/packages/compress-stringify/typescript/test/browser/demo-app/index.ts b/packages/compress-stringify/typescript/test/browser/demo-app/index.ts index a539d23a6..4c93b87ca 100644 --- a/packages/compress-stringify/typescript/test/browser/demo-app/index.ts +++ b/packages/compress-stringify/typescript/test/browser/demo-app/index.ts @@ -8,8 +8,6 @@ compressStringify.setPipelinesBaseUrl(pipelinesBaseUrl) const pipelineWorkerUrl: string | URL | null = new URL('/web-workers/pipeline.worker.js', document.location.origin).href compressStringify.setPipelineWorkerUrl(pipelineWorkerUrl) -import './compress-stringify-controller.js' -import './parse-string-decompress-controller.js' const params = new URLSearchParams(window.location.search) if (!params.has('functionName')) { @@ -18,3 +16,5 @@ if (!params.has('functionName')) { url.search = params window.history.replaceState({ functionName: 'compressStringify' }, '', url) } +import './compress-stringify-controller.js' +import './parse-string-decompress-controller.js' diff --git a/packages/compress-stringify/typescript/test/browser/demo-app/parse-string-decompress-controller.ts b/packages/compress-stringify/typescript/test/browser/demo-app/parse-string-decompress-controller.ts index 88026ce20..1e68f0107 100644 --- a/packages/compress-stringify/typescript/test/browser/demo-app/parse-string-decompress-controller.ts +++ b/packages/compress-stringify/typescript/test/browser/demo-app/parse-string-decompress-controller.ts @@ -84,19 +84,21 @@ class ParseStringDecompressController { const url = new URL(document.location) url.search = params window.history.replaceState({ functionName: 'parseStringDecompress' }, '', url) + await preRun() } - await preRun() } } const tabGroup = document.querySelector('sl-tab-group') tabGroup.addEventListener('sl-tab-show', onSelectTab) - document.addEventListener('DOMContentLoaded', () => { + function onInit() { const params = new URLSearchParams(window.location.search) if (params.has('functionName') && params.get('functionName') === 'parseStringDecompress') { + tabGroup.show('parseStringDecompress-panel') preRun() } - }) + } + onInit() const runButton = document.querySelector('#parseStringDecompressInputs sl-button[name="run"]') runButton.addEventListener('click', async (event) => { diff --git a/packages/dicom/typescript/test/browser/demo-app/apply-presentation-state-to-image-controller.ts b/packages/dicom/typescript/test/browser/demo-app/apply-presentation-state-to-image-controller.ts index 3c3f8c7d1..c1f22d2eb 100644 --- a/packages/dicom/typescript/test/browser/demo-app/apply-presentation-state-to-image-controller.ts +++ b/packages/dicom/typescript/test/browser/demo-app/apply-presentation-state-to-image-controller.ts @@ -133,19 +133,21 @@ class ApplyPresentationStateToImageController { const url = new URL(document.location) url.search = params window.history.replaceState({ functionName: 'applyPresentationStateToImage' }, '', url) + await preRun() } - await preRun() } } const tabGroup = document.querySelector('sl-tab-group') tabGroup.addEventListener('sl-tab-show', onSelectTab) - document.addEventListener('DOMContentLoaded', () => { + function onInit() { const params = new URLSearchParams(window.location.search) if (params.has('functionName') && params.get('functionName') === 'applyPresentationStateToImage') { + tabGroup.show('applyPresentationStateToImage-panel') preRun() } - }) + } + onInit() const runButton = document.querySelector('#applyPresentationStateToImageInputs sl-button[name="run"]') runButton.addEventListener('click', async (event) => { diff --git a/packages/dicom/typescript/test/browser/demo-app/index.ts b/packages/dicom/typescript/test/browser/demo-app/index.ts index 0a1459759..88d5e4b01 100644 --- a/packages/dicom/typescript/test/browser/demo-app/index.ts +++ b/packages/dicom/typescript/test/browser/demo-app/index.ts @@ -8,12 +8,6 @@ dicom.setPipelinesBaseUrl(pipelinesBaseUrl) const pipelineWorkerUrl: string | URL | null = new URL('/web-workers/pipeline.worker.js', document.location.origin).href dicom.setPipelineWorkerUrl(pipelineWorkerUrl) -import './apply-presentation-state-to-image-controller.js' -import './read-dicom-encapsulated-pdf-controller.js' -import './structured-report-to-html-controller.js' -import './structured-report-to-text-controller.js' -import './read-dicom-tags-controller.js' -import './read-image-dicom-file-series-controller.js' const params = new URLSearchParams(window.location.search) if (!params.has('functionName')) { @@ -22,3 +16,9 @@ if (!params.has('functionName')) { url.search = params window.history.replaceState({ functionName: 'applyPresentationStateToImage' }, '', url) } +import './apply-presentation-state-to-image-controller.js' +import './read-dicom-encapsulated-pdf-controller.js' +import './structured-report-to-html-controller.js' +import './structured-report-to-text-controller.js' +import './read-dicom-tags-controller.js' +import './read-image-dicom-file-series-controller.js' diff --git a/packages/dicom/typescript/test/browser/demo-app/read-dicom-tags-controller.ts b/packages/dicom/typescript/test/browser/demo-app/read-dicom-tags-controller.ts index d78dc360a..0958d96af 100644 --- a/packages/dicom/typescript/test/browser/demo-app/read-dicom-tags-controller.ts +++ b/packages/dicom/typescript/test/browser/demo-app/read-dicom-tags-controller.ts @@ -92,19 +92,21 @@ class ReadDicomTagsController { const url = new URL(document.location) url.search = params window.history.replaceState({ functionName: 'readDicomTags' }, '', url) + await preRun() } - await preRun() } } const tabGroup = document.querySelector('sl-tab-group') tabGroup.addEventListener('sl-tab-show', onSelectTab) - document.addEventListener('DOMContentLoaded', () => { + function onInit() { const params = new URLSearchParams(window.location.search) if (params.has('functionName') && params.get('functionName') === 'readDicomTags') { + tabGroup.show('readDicomTags-panel') preRun() } - }) + } + onInit() const runButton = document.querySelector('#readDicomTagsInputs sl-button[name="run"]') runButton.addEventListener('click', async (event) => { diff --git a/packages/dicom/typescript/test/browser/demo-app/structured-report-to-text-controller.ts b/packages/dicom/typescript/test/browser/demo-app/structured-report-to-text-controller.ts index 28239d798..c783c2e9d 100644 --- a/packages/dicom/typescript/test/browser/demo-app/structured-report-to-text-controller.ts +++ b/packages/dicom/typescript/test/browser/demo-app/structured-report-to-text-controller.ts @@ -164,19 +164,21 @@ class StructuredReportToTextController { const url = new URL(document.location) url.search = params window.history.replaceState({ functionName: 'structuredReportToText' }, '', url) + await preRun() } - await preRun() } } const tabGroup = document.querySelector('sl-tab-group') tabGroup.addEventListener('sl-tab-show', onSelectTab) - document.addEventListener('DOMContentLoaded', () => { + function onInit() { const params = new URLSearchParams(window.location.search) if (params.has('functionName') && params.get('functionName') === 'structuredReportToText') { + tabGroup.show('structuredReportToText-panel') preRun() } - }) + } + onInit() const runButton = document.querySelector('#structuredReportToTextInputs sl-button[name="run"]') runButton.addEventListener('click', async (event) => { diff --git a/src/bindgen/typescript/demo/interface-functions-demo-typescript.js b/src/bindgen/typescript/demo/interface-functions-demo-typescript.js index 013591c0c..d3d6d2d1e 100644 --- a/src/bindgen/typescript/demo/interface-functions-demo-typescript.js +++ b/src/bindgen/typescript/demo/interface-functions-demo-typescript.js @@ -154,19 +154,21 @@ class ${functionNamePascalCase}Model { const url = new URL(document.location) url.search = params window.history.replaceState({ functionName: '${functionName}' }, '', url) + await preRun() } - await preRun() } } const tabGroup = document.querySelector('sl-tab-group') tabGroup.addEventListener('sl-tab-show', onSelectTab) - document.addEventListener('DOMContentLoaded', () => { + function onInit() { const params = new URLSearchParams(window.location.search) if (params.has('functionName') && params.get('functionName') === '${functionName}') { + tabGroup.show('${functionName}-panel') preRun() } - }) + } + onInit() const runButton = document.querySelector('#${functionName}Inputs sl-button[name="run"]') runButton.addEventListener('click', async (event) => { diff --git a/src/bindgen/typescript/typescript-bindings.js b/src/bindgen/typescript/typescript-bindings.js index c21f51bff..f9435d353 100644 --- a/src/bindgen/typescript/typescript-bindings.js +++ b/src/bindgen/typescript/typescript-bindings.js @@ -39,6 +39,7 @@ function typescriptBindings (outputDir, buildDir, wasmBinaries, options, forNode let demoFunctionsHtml = '' let pipelinesFunctionsTabs = '' let demoFunctionsTypeScript = '' + const allUsedInterfaceTypes = new Set() const packageName = options.packageName @@ -102,7 +103,17 @@ function typescriptBindings (outputDir, buildDir, wasmBinaries, options, forNode const moduleCamelCase = camelCase(parsedPath.name) const modulePascalCase = `${moduleCamelCase[0].toUpperCase()}${moduleCamelCase.substring(1)}` const functionName = camelCase(interfaceJson.name) - firstFunctionName = firstFunctionName || functionName + if (!firstFunctionName) { + firstFunctionName = functionName + demoFunctionsTypeScript += `\nconst params = new URLSearchParams(window.location.search) +if (!params.has('functionName')) { + params.set('functionName', '${firstFunctionName}') + const url = new URL(document.location) + url.search = params + window.history.replaceState({ functionName: '${firstFunctionName}' }, '', url) +} +` + } const useCamelCase = true const functionDemoHtml = interfaceFunctionsDemoHtml(interfaceJson, functionName, useCamelCase) @@ -144,15 +155,6 @@ function typescriptBindings (outputDir, buildDir, wasmBinaries, options, forNode readmePipelines += readmeResult }) - demoFunctionsTypeScript += `\nconst params = new URLSearchParams(window.location.search) -if (!params.has('functionName')) { - params.set('functionName', '${firstFunctionName}') - const url = new URL(document.location) - url.search = params - window.history.replaceState({ functionName: '${firstFunctionName}' }, '', url) -} -` - if (allUsedInterfaceTypes.size > 0) { indexContent += '\n' allUsedInterfaceTypes.forEach(iType => indexContent += `export type { ${iType} } from 'itk-wasm'\n`) From e5ecfce20b29ab4e9b4b68861d54414c35eb93fe Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Wed, 27 Sep 2023 15:21:50 -0400 Subject: [PATCH 2/6] refactor(bindgen): output file paths are passed as on input argument Instead of an input option. --- .../compare-double-images-controller.ts | 5 ++- .../demo-app/vector-magnitude-controller.ts | 5 ++- ...-presentation-state-to-image-controller.ts | 3 +- src/bindgen/python/function-module-args.js | 11 +++++ .../python/function-module-docstring.js | 15 +++++++ .../interface-functions-demo-typescript.js | 25 +++++++---- src/bindgen/typescript/function-module.js | 42 ++++++++++--------- src/bindgen/typescript/options-module.js | 11 ----- src/bindgen/typescript/typescript-bindings.js | 3 +- 9 files changed, 75 insertions(+), 45 deletions(-) diff --git a/packages/compare-images/typescript/test/browser/demo-app/compare-double-images-controller.ts b/packages/compare-images/typescript/test/browser/demo-app/compare-double-images-controller.ts index de2ddd96a..4031a90dc 100644 --- a/packages/compare-images/typescript/test/browser/demo-app/compare-double-images-controller.ts +++ b/packages/compare-images/typescript/test/browser/demo-app/compare-double-images-controller.ts @@ -1,7 +1,8 @@ // Generated file. To retain edits, remove this comment. -import { readImageFile, copyImage } from 'itk-wasm' -import { writeImageArrayBuffer, copyImage } from 'itk-wasm' +import { readImageFile } from 'itk-wasm' +import { writeImageArrayBuffer } from 'itk-wasm' +import { copyImage } from 'itk-wasm' import * as compareImages from '../../../dist/bundles/compare-images.js' import compareDoubleImagesLoadSampleInputs, { usePreRun } from "./compare-double-images-load-sample-inputs.js" diff --git a/packages/compare-images/typescript/test/browser/demo-app/vector-magnitude-controller.ts b/packages/compare-images/typescript/test/browser/demo-app/vector-magnitude-controller.ts index 527503f76..d2405d7c9 100644 --- a/packages/compare-images/typescript/test/browser/demo-app/vector-magnitude-controller.ts +++ b/packages/compare-images/typescript/test/browser/demo-app/vector-magnitude-controller.ts @@ -1,7 +1,8 @@ // Generated file. To retain edits, remove this comment. -import { readImageFile, copyImage } from 'itk-wasm' -import { writeImageArrayBuffer, copyImage } from 'itk-wasm' +import { readImageFile } from 'itk-wasm' +import { writeImageArrayBuffer } from 'itk-wasm' +import { copyImage } from 'itk-wasm' import * as compareImages from '../../../dist/bundles/compare-images.js' import vectorMagnitudeLoadSampleInputs, { usePreRun } from "./vector-magnitude-load-sample-inputs.js" diff --git a/packages/dicom/typescript/test/browser/demo-app/apply-presentation-state-to-image-controller.ts b/packages/dicom/typescript/test/browser/demo-app/apply-presentation-state-to-image-controller.ts index c1f22d2eb..e263397c6 100644 --- a/packages/dicom/typescript/test/browser/demo-app/apply-presentation-state-to-image-controller.ts +++ b/packages/dicom/typescript/test/browser/demo-app/apply-presentation-state-to-image-controller.ts @@ -1,6 +1,7 @@ // Generated file. To retain edits, remove this comment. -import { writeImageArrayBuffer, copyImage } from 'itk-wasm' +import { writeImageArrayBuffer } from 'itk-wasm' +import { copyImage } from 'itk-wasm' import * as dicom from '../../../dist/bundles/dicom.js' import applyPresentationStateToImageLoadSampleInputs, { usePreRun } from "./apply-presentation-state-to-image-load-sample-inputs.js" diff --git a/src/bindgen/python/function-module-args.js b/src/bindgen/python/function-module-args.js index 2b76f2a75..3822417ef 100644 --- a/src/bindgen/python/function-module-args.js +++ b/src/bindgen/python/function-module-args.js @@ -11,6 +11,16 @@ function functionModuleArgs(interfaceJson) { const pythonType = interfaceJsonTypeToPythonType.get(canonical) functionArgs += ` ${snakeCase(value.name)}: ${pythonType},\n` }) + const outputFiles = interfaceJson.outputs.filter(o => { return o.type.includes('FILE') }) + outputFiles.forEach((output) => { + const isArray = output.itemsExpectedMax > 1 + const optionName = `${output.name}` + if (isArray) { + functionArgs += ` ${snakeCase(optionName)}: List[str],\n` + } else { + functionArgs += ` ${snakeCase(optionName)}: str,\n` + } + }) interfaceJson['parameters'].forEach((value) => { if (value.name === "memory-io" || value.name === "version") { return @@ -45,6 +55,7 @@ function functionModuleArgs(interfaceJson) { } } }) + return functionArgs } diff --git a/src/bindgen/python/function-module-docstring.js b/src/bindgen/python/function-module-docstring.js index 3a685c2b2..a1fc650a2 100644 --- a/src/bindgen/python/function-module-docstring.js +++ b/src/bindgen/python/function-module-docstring.js @@ -13,6 +13,18 @@ function functionModuleDocstring(interfaceJson) { docstring += `\n :param ${snakeCase(value.name)}: ${description}\n` docstring += ` :type ${snakeCase(value.name)}: ${pythonType}\n` }) + const outputFiles = interfaceJson.outputs.filter(o => { return o.type.includes('FILE') }) + outputFiles.forEach((output) => { + const isArray = output.itemsExpectedMax > 1 + const optionName = `${output.name}` + const description = output.description.replaceAll('\n', ' ') + docstring += `\n :param ${snakeCase(optionName)}: ${description}\n` + if (isArray) { + docstring += ` :type ${snakeCase(optionName)}: List[str]\n` + } else { + docstring += ` :type ${snakeCase(optionName)}: str\n` + } + }) interfaceJson['parameters'].forEach((value) => { if (value.name === "memory-io" || value.name === "version") { return @@ -25,6 +37,9 @@ function functionModuleDocstring(interfaceJson) { }) const jsonOutputs = interfaceJson['outputs'] jsonOutputs.forEach((value) => { + if (value.type.includes('FILE')) { + return + } const description = value.description.replaceAll('\n', ' ') const canonical = canonicalType(value.type) const pythonType = interfaceJsonTypeToPythonType.get(canonical) diff --git a/src/bindgen/typescript/demo/interface-functions-demo-typescript.js b/src/bindgen/typescript/demo/interface-functions-demo-typescript.js index d3d6d2d1e..bfb2f797e 100644 --- a/src/bindgen/typescript/demo/interface-functions-demo-typescript.js +++ b/src/bindgen/typescript/demo/interface-functions-demo-typescript.js @@ -27,7 +27,7 @@ function interfaceFunctionsDemoTypeScript(packageName, interfaceJson, outputPath result += `import { readMeshFile } from 'itk-wasm'\n` } if (needReadImage) { - result += `import { readImageFile, copyImage } from 'itk-wasm'\n` + result += `import { readImageFile } from 'itk-wasm'\n` } const needWriteMesh = interfaceJson.outputs.filter((value) => interfaceJsonTypeToInterfaceType.get(value.type) === 'Mesh').length > 0 if (needWriteMesh) { @@ -35,7 +35,10 @@ function interfaceFunctionsDemoTypeScript(packageName, interfaceJson, outputPath } const needWriteImage = interfaceJson.outputs.filter((value) => interfaceJsonTypeToInterfaceType.get(value.type) === 'Image').length > 0 if (needWriteImage) { - result += `import { writeImageArrayBuffer, copyImage } from 'itk-wasm'\n` + result += `import { writeImageArrayBuffer } from 'itk-wasm'\n` + } + if (needReadImage || needWriteImage) { + result += `import { copyImage } from 'itk-wasm'\n` } result += `import * as ${camelCase(bundleName)} from '../../../dist/bundles/${bundleName}.js'\n` @@ -117,6 +120,12 @@ class ${functionNamePascalCase}Model { result += inputParametersDemoTypeScript(functionName, indent, input, true, 'inputs') }) + interfaceJson.outputs.forEach((output) => { + if (output.type.includes('FILE')) { + result += inputParametersDemoTypeScript(functionName, indent, output, true, 'inputs') + } + }) + if (interfaceJson.parameters.length > 1) { result += `${indent}// ----------------------------------------------\n${indent}// Options\n` interfaceJson.parameters.forEach((parameter) => { @@ -128,12 +137,6 @@ class ${functionNamePascalCase}Model { }) } - interfaceJson.outputs.forEach((output) => { - if (output.type.includes('FILE')) { - result += inputParametersDemoTypeScript(functionName, indent, output, true, 'options') - } - }) - result += `${indent}// ----------------------------------------------\n${indent}// Outputs` interfaceJson.outputs.forEach((output) => { result += outputDemoTypeScript(functionName, '', indent, output) @@ -222,6 +225,12 @@ class ${functionNamePascalCase}Model { result += ` this.model.inputs.get('${camelCase(input.name)}'),\n` } }) + interfaceJson.outputs.forEach((output) => { + const defaultData = output.type.includes('BINARY') ? 'new Uint8Array()' : "''" + if (output.type.startsWith('OUTPUT_BINARY_FILE') || output.type.startsWith('OUTPUT_TEXT_FILE')) { + result += ` this.model.inputs.get('${camelCase(output.name)}'),\n` + } + }) result += ' Object.fromEntries(this.model.options.entries())\n' result += ' )\n' result += ' this.webWorker = webWorker\n' diff --git a/src/bindgen/typescript/function-module.js b/src/bindgen/typescript/function-module.js index 0d49afcd7..09700fc13 100644 --- a/src/bindgen/typescript/function-module.js +++ b/src/bindgen/typescript/function-module.js @@ -99,6 +99,14 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, functionContent += ` * @param {${typescriptType}} ${camelCase(input.name)} - ${input.description}\n` readmeParametersTable.push([`\`${camelCase(input.name)}\``, `*${typescriptType}*`, input.description]) }) + const outputFiles = interfaceJson.outputs.filter(o => { return o.type.includes('FILE') }) + outputFiles.forEach((output) => { + const isArray = output.itemsExpectedMax > 1 ? '[]' : '' + const typescriptType = `string${isArray}` + functionContent += ` * @param {${typescriptType}} ${camelCase(output.name)} - ${output.description}\n` + readmeParametersTable.push([`\`${camelCase(output.name)}\``, `*${typescriptType}*`, output.description]) + }) + if (haveOptions) { functionContent += ` * @param {${modulePascalCase}Options} options - options object\n` } @@ -113,7 +121,7 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, } interfaceJson.inputs.forEach((input, index) => { let typescriptType = interfaceJsonTypeToTypeScriptType.get(input.type) - const end = index === interfaceJson.inputs.length - 1 && !haveOptions ? '\n' : ',\n' + const end = index === interfaceJson.inputs.length - 1 && !haveOptions && !outputFiles.length ? '\n' : ',\n' const isArray = input.itemsExpectedMax > 1 ? '[]' : '' const fileType = forNode ? 'string' : 'File' if (typescriptType === 'TextFile' || typescriptType === 'BinaryFile') { @@ -127,6 +135,12 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, } functionCall += ` ${camelCase(input.name)}: ${typescriptType}${end}` }) + outputFiles.forEach((output, index) => { + const end = index === interfaceJson.outputs.length - 1 && !haveOptions ? '\n' : ',\n' + const isArray = output.itemsExpectedMax > 1 ? '[]' : '' + const typescriptType = `string${isArray}` + functionCall += ` ${camelCase(output.name)}: ${typescriptType}${end}` + }) if (haveOptions) { let requiredOptions = '' interfaceJson.parameters.forEach((parameter) => { @@ -179,9 +193,7 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, const camel = camelCase(output.name) if (isArray) { const defaultData = interfaceType === 'BinaryFile' ? 'new Uint8Array()' : "''" - functionContent += ` const ${camel}PipelineOutputs = typeof options.${camel}Path !== 'undefined' ? options.${camel}Path.map((p) => { return { type: InterfaceTypes.${interfaceType}, data: { path: p, data: ${defaultData} }}}) : []\n` - } else { - functionContent += ` const ${camel}Path = options.${camel}Path ?? '${camel}'\n` + functionContent += ` const ${camel}PipelineOutputs = ${camel}.map((p) => { return { type: InterfaceTypes.${interfaceType}, data: { path: p, data: ${defaultData} }}})\n` } } } @@ -200,7 +212,7 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, haveArray = true functionContent += ` ...${camel}PipelineOutputs,\n` } else { - functionContent += ` { type: InterfaceTypes.${interfaceType}, data: { path: ${camel}Path, data: ${defaultData} }},\n` + functionContent += ` { type: InterfaceTypes.${interfaceType}, data: { path: ${camel}, data: ${defaultData} }},\n` } } else if (!interfaceType.includes('File')) { functionContent += ` { type: InterfaceTypes.${interfaceType} },\n` @@ -218,7 +230,7 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, const isArray = output.itemsExpectedMax > 1 if (isArray) { functionContent += ` const ${camel}Start = outputIndex\n` - functionContent += ` outputIndex += options.${camel}Path ? options.${camel}Path.length : 0\n` + functionContent += ` outputIndex += ${camel}.length\n` functionContent += ` const ${camel}End = outputIndex\n` } else { functionContent += ` const ${camel}Index = outputIndex\n` @@ -296,25 +308,17 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, let name = ` const ${camel}Name = '${outputCount.toString()}'\n` const isArray = output.itemsExpectedMax > 1 ? '[]' : '' if (interfaceType.includes('File')) { - if (forNode) { - if (isArray) { - name = '' - } else { - name = ` const ${camel}Name = options.${camel}Path ?? '${camel}'\n` - } + if (isArray) { + name = '' } else { - if (isArray) { - name = '' - } else { - name = ` const ${camel}Name = ${camel}Path\n` - } + name = ` const ${camel}Name = ${camel}?? '${camel}'\n` } } functionContent += name if (isArray) { - functionContent += ` options.${camel}Path?.forEach((p) => args.push(p))\n` + functionContent += ` ${camel}.forEach((p) => args.push(p))\n` if (forNode && interfaceType.includes('File')) { - functionContent += ` options.${camel}Path?.forEach((p) => mountDirs.add(path.dirname(p)))\n` + functionContent += ` ${camel}.forEach((p) => mountDirs.add(path.dirname(p)))\n` } } else { functionContent += ` args.push(${camel}Name)\n` diff --git a/src/bindgen/typescript/options-module.js b/src/bindgen/typescript/options-module.js index 6486aed39..046dd573c 100644 --- a/src/bindgen/typescript/options-module.js +++ b/src/bindgen/typescript/options-module.js @@ -49,17 +49,6 @@ function optionsModule (srcOutputDir, interfaceJson, modulePascalCase, nodeTextC readmeOptionsTable.push([`\`${camelCase(parameter.name)}\``, `*${parameterType}*`, parameter.description]) }) - const outputFiles = interfaceJson.outputs.filter(o => { return o.type.includes('FILE') }) - outputFiles.forEach((output) => { - const outputDescription = `${output.description} path` - optionsInterfaceContent += ` /** ${outputDescription} */\n` - const isArray = output.itemsExpectedMax > 1 ? '[]' : '' - const parameterType = `string${isArray}` - const optionName = `${output.name}-path` - optionsInterfaceContent += ` ${camelCase(optionName)}?: ${parameterType}\n\n` - readmeOptionsTable.push([`\`${camelCase(optionName)}\``, `*${parameterType}*`, outputDescription]) - }) - // Insert the import statement in the beginning for the file. if (optionsImportTypes.size !== 0) { optionsContent += `import { ${Array.from(optionsImportTypes).join(',')} } from 'itk-wasm'\n\n` diff --git a/src/bindgen/typescript/typescript-bindings.js b/src/bindgen/typescript/typescript-bindings.js index f9435d353..d6e14c0de 100644 --- a/src/bindgen/typescript/typescript-bindings.js +++ b/src/bindgen/typescript/typescript-bindings.js @@ -135,8 +135,7 @@ if (!params.has('functionName')) { indexContent += `export type { ${modulePascalCase}${nodeTextCamel}Result }\n\n` const filteredParameters = interfaceJson.parameters.filter(p => { return p.name !== 'memory-io' && p.name !== 'version'}) - const haveOutputFile = interfaceJson.outputs.some(o => { return o.type.includes('FILE') }) - const haveOptions = !!filteredParameters.length || haveOutputFile + const haveOptions = !!filteredParameters.length const { readmeOptions } = optionsModule(srcOutputDir, interfaceJson, modulePascalCase, nodeTextCamel, moduleKebabCase, haveOptions) if (haveOptions) { From 2cf3c7eb68e8a6bea710467a2f1783ba9833beaa Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Wed, 27 Sep 2023 21:34:47 -0400 Subject: [PATCH 3/6] feat(bindgen): Python output file support --- .../python/function-module-return-type.js | 9 +- .../python/wasi/wasi-function-module.js | 140 ++++++++++++++---- src/bindgen/typescript/function-module.js | 4 +- 3 files changed, 124 insertions(+), 29 deletions(-) diff --git a/src/bindgen/python/function-module-return-type.js b/src/bindgen/python/function-module-return-type.js index 44ee43b8d..407dae055 100644 --- a/src/bindgen/python/function-module-return-type.js +++ b/src/bindgen/python/function-module-return-type.js @@ -9,7 +9,14 @@ function functionModuleReturnType(interfaceJson) { jsonOutputs.forEach((value) => { const canonical = canonicalType(value.type) const pythonType = interfaceJsonTypeToPythonType.get(canonical) - returnType += `${pythonType}, ` + if (value.type.includes('FILE')) { + return + } + if (value.itemsExpectedMax > 1) { + returnType += `List[${pythonType}], ` + } else { + returnType += `${pythonType}, ` + } }) returnType = returnType.substring(0, returnType.length - 2) returnType += "]" diff --git a/src/bindgen/python/wasi/wasi-function-module.js b/src/bindgen/python/wasi/wasi-function-module.js index d7caed5ed..934a6a0d1 100644 --- a/src/bindgen/python/wasi/wasi-function-module.js +++ b/src/bindgen/python/wasi/wasi-function-module.js @@ -13,9 +13,7 @@ import writeIfOverrideNotPresent from '../../write-if-override-not-present.js' function wasiFunctionModule(interfaceJson, pypackage, modulePath) { const functionName = snakeCase(interfaceJson.name) - let moduleContent = `# Generated file. Do not edit. - -from pathlib import Path, PurePosixPath + let moduleContent = `from pathlib import Path, PurePosixPath import os from typing import Dict, Tuple, Optional, List, Any @@ -34,20 +32,65 @@ from itkwasm import ( const returnType = functionModuleReturnType(interfaceJson) const docstring = functionModuleDocstring(interfaceJson) + let pipelineOutputFilePrep = '' + interfaceJson.outputs.forEach((output) => { + if (interfaceJsonTypeToInterfaceType.has(output.type)) { + const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) + const isArray = output.itemsExpectedMax > 1 + if (interfaceType.includes('File')) { + const snake = snakeCase(output.name) + if (isArray) { + pipelineOutputFilePrep += ` ${snake}_pipeline_outputs = [PipelineOutput(InterfaceTypes.${interfaceType}, ${interfaceType}(PurePosixPath(p))) for p in ${snake}]\n` + } + } + } + }) + let pipelineOutputs = '' + let haveArray = false interfaceJson.outputs.forEach((output) => { if (interfaceJsonTypeToInterfaceType.has(output.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) + const isArray = output.itemsExpectedMax > 1 switch (interfaceType) { case "TextFile": case "BinaryFile": - pipelineOutputs += ` PipelineOutput(InterfaceTypes.${interfaceType}, ${interfaceType}(PurePosixPath(${snakeCase(output.name)}))),\n` + if (isArray) { + haveArray = true + pipelineOutputs += ` *${snakeCase(output.name)}_pipeline_outputs,\n` + } else { + pipelineOutputs += ` PipelineOutput(InterfaceTypes.${interfaceType}, ${interfaceType}(PurePosixPath(${snakeCase(output.name)}))),\n` + } break default: pipelineOutputs += ` PipelineOutput(InterfaceTypes.${interfaceType}),\n` } } }) + let pipelineOutputIndices = '' + if (haveArray) { + pipelineOutputIndices += ` output_index = 0\n` + interfaceJson.outputs.forEach((output) => { + if (interfaceJsonTypeToInterfaceType.has(output.type)) { + const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) + if (interfaceType.includes('File')) { + const snake = snakeCase(output.name) + const isArray = output.itemsExpectedMax > 1 + if (isArray) { + pipelineOutputIndices += ` ${snake}_start = output_index\n` + pipelineOutputIndices += ` output_index += len(${snake})\n` + pipelineOutputIndices += ` ${snake}_end = output_index\n` + } else { + pipelineOutputIndices += ` ${snake}_index = output_index\n` + pipelineOutputIndices += ` output_index += 1\n` + } + } else if (!interfaceType.includes('File')) { + pipelineOutputIndices += ` ${snake}_index = output_index\n` + pipelineOutputIndices += ` output_index += 1\n` + } + } + }) + } let pipelineInputs = '' interfaceJson['inputs'].forEach((input) => { @@ -90,18 +133,33 @@ from itkwasm import ( let outputCount = 0 args += " # Outputs\n" interfaceJson.outputs.forEach((output) => { + const snake = snakeCase(output.name) if (interfaceJsonTypeToInterfaceType.has(output.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) - const name = interfaceType.includes('File') ? `str(PurePosixPath(${snakeCase(output.name)}))` : `'${outputCount.toString()}'` - args += ` args.append(${name})\n` + const isArray = output.itemsExpectedMax > 1 + let name = ` ${snake}_name = '${outputCount.toString()}'\n` + if (interfaceType.includes('File')) { + if (isArray) { + name = '' + } else { + name = ` ${snake}_name = str(PurePosixPath(${snake}))\n` + } + } + args += name + if (isArray) { + args += ` args.extend([str(PurePosixPath(p)) for p in ${snake}])\n` + } else { + args += ` args.append(${snake}_name)\n` + } + args += '\n' outputCount++ } else { - const snake = snakeCase(output.name) args += ` args.append(str(${snake}))\n` } }) args += " # Options\n" + args += ` input_count = len(pipeline_inputs)\n` interfaceJson.parameters.forEach((parameter) => { if (parameter.name === 'memory-io' || parameter.name === 'version') { // Internal @@ -126,14 +184,14 @@ from itkwasm import ( args += ` args.append(input_file)\n` } else if (interfaceType.includes('Stream')) { // for streams - args += ` input_count_string = str(len(pipeline_inputs))\n` args += ` pipeline_inputs.append(PipelineInput(InterfaceTypes.${interfaceType}, ${interfaceType}(value)))\n` - args += ` args.append(input_count_spring)\n` + args += ` args.append(str(input_count))\n` + args += ` input_count += 1\n` } else { // Image, Mesh, PolyData, JsonCompatible - args += ` input_count_string = str(len(pipeline_inputs))\n` args += ` pipeline_inputs.append(PipelineInput(InterfaceTypes.${interfaceType}, value))\n` - args += ` args.append(input_count_string)\n` + args += ` args.append(str(input_count))\n` + args += ` input_count += 1\n` } } else { args += ` args.append(str(value))\n` @@ -150,16 +208,16 @@ from itkwasm import ( args += ` args.append(input_file)\n` } else if (interfaceType.includes('Stream')) { // for streams - args += ` input_count_string = str(len(pipeline_inputs))\n` args += ` pipeline_inputs.append(PipelineInput(InterfaceTypes.${interfaceType}, ${interfaceType}(${snake})))\n` args += ` args.append('--${parameter.name}')\n` - args += ` args.append(input_count_string)\n` + args += ` args.append(str(input_count))\n` + args += ` input_count += 1\n` } else { // Image, Mesh, PolyData, JsonCompatible - args += ` input_count_string = str(len(pipeline_inputs))\n` args += ` pipeline_inputs.append(PipelineInput(InterfaceTypes.${interfaceType}, ${snake}))\n` args += ` args.append('--${parameter.name}')\n` - args += ` args.append(input_count_string)\n` + args += ` args.append(str(input_count))\n` + args += ` input_count += 1\n` } } else { args += ` if ${snake}:\n` @@ -192,24 +250,54 @@ from itkwasm import ( case "float": return `float(${value})` case "Any": - return `${value}.data.data` + return `${value}.data` default: return `${value}.data` } } + outputCount = 0 const jsonOutputs = interfaceJson['outputs'] - if (jsonOutputs.length > 1) { + const numOutputs = interfaceJson.outputs.filter(o => !o.type.includes('FILE')).length + if (numOutputs > 1) { postOutput += ' result = (\n' - jsonOutputs.forEach((value, index) => { - const outputValue = `outputs[${index}]` - postOutput += ` ${toPythonType(value.type, outputValue)},\n` + } else if (numOutputs === 1){ + postOutput = ' result = ' + } + + if (numOutputs > 0) { + // const outputValue = "outputs[0]" + // postOutput = ` result = ${toPythonType(jsonOutputs[0].type, outputValue)}\n` + const indent = numOutputs > 1 ? ' ' : '' + const comma = numOutputs > 1 ? ',' : '' + jsonOutputs.forEach((value) => { + if (value.type.includes('FILE')) { + outputCount++ + return + } + const snake = snakeCase(value.name) + const outputIndex = haveArray ? `${snake}_index` : outputCount.toString() + if (haveArray) { + const isArray = value.itemsExpectedMax > 1 + if (isArray) { + const outputValue = `outputs[${snake}_start:${snake}_end]` + postOutput += `${indent}*[${toPythonType(value.type, 'v')} for v in ${outputValue}]${comma}\n` + } else { + const outputValue = `outputs[${snake}_index]` + postOutput += `${indent}${toPythonType(value.type, outputValue)}${comma}\n` + } + } else { + const outputValue = `outputs[${outputIndex}]` + postOutput += `${indent}${toPythonType(value.type, outputValue)}${comma}\n` + } + outputCount++ }) + } + if (numOutputs > 1) { postOutput += ' )\n' - } else { - const outputValue = "outputs[0]" - postOutput = ` result = ${toPythonType(jsonOutputs[0].type, outputValue)}\n` } - postOutput += ' return result\n' + if (numOutputs > 0) { + postOutput += ' return result\n' + } moduleContent += `def ${functionName}( ${functionArgs}) -> ${returnType}: @@ -217,10 +305,10 @@ ${functionArgs}) -> ${returnType}: global _pipeline if _pipeline is None: _pipeline = Pipeline(file_resources('${pypackage}').joinpath(Path('wasm_modules') / Path('${interfaceJson.name}.wasi.wasm'))) - +${pipelineOutputFilePrep} pipeline_outputs: List[PipelineOutput] = [ ${pipelineOutputs} ] - +${pipelineOutputIndices} pipeline_inputs: List[PipelineInput] = [ ${pipelineInputs} ] diff --git a/src/bindgen/typescript/function-module.js b/src/bindgen/typescript/function-module.js index 09700fc13..2666eec0a 100644 --- a/src/bindgen/typescript/function-module.js +++ b/src/bindgen/typescript/function-module.js @@ -306,12 +306,12 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, if (interfaceJsonTypeToInterfaceType.has(output.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) let name = ` const ${camel}Name = '${outputCount.toString()}'\n` - const isArray = output.itemsExpectedMax > 1 ? '[]' : '' + const isArray = output.itemsExpectedMax > 1 if (interfaceType.includes('File')) { if (isArray) { name = '' } else { - name = ` const ${camel}Name = ${camel}?? '${camel}'\n` + name = ` const ${camel}Name = ${camel}\n` } } functionContent += name From 4b07cda335b164b24a04dcc27028cbaa3398cc45 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 28 Sep 2023 08:42:24 -0400 Subject: [PATCH 4/6] feat(bindgen): Check for positional input of an array of files Still todo: dynamic input indexing with multiple arguments. Also, cli11 currently does not distinguish between the end of a input array set of arguments and a positional output. --- src/bindgen/input-array-check.js | 12 +++++ src/bindgen/typescript/function-module.js | 44 +++++++++++++++---- src/bindgen/typescript/typescript-bindings.js | 2 + 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/bindgen/input-array-check.js diff --git a/src/bindgen/input-array-check.js b/src/bindgen/input-array-check.js new file mode 100644 index 000000000..5b55ef995 --- /dev/null +++ b/src/bindgen/input-array-check.js @@ -0,0 +1,12 @@ +function inputArrayCheck(interfaceJson) { + interfaceJson.inputs.forEach((input) => { + const isArray = input.itemsExpectedMax > 1 + if (isArray) { + console.error('Positional multi-value inputs are currently not supported -- please use an option instead') + console.error(`Violating option: ${input.name}`) + process.exit(1) + } + }) +} + +export default inputArrayCheck diff --git a/src/bindgen/typescript/function-module.js b/src/bindgen/typescript/function-module.js index 2666eec0a..0d5b53eda 100644 --- a/src/bindgen/typescript/function-module.js +++ b/src/bindgen/typescript/function-module.js @@ -10,7 +10,7 @@ import writeIfOverrideNotPresent from '../write-if-override-not-present.js' function readFileIfNotInterfaceType(forNode, interfaceType, varName, indent, isArray) { if (forNode) { if (isArray) { - return `${indent}value.forEach((p) => mountDirs.add(path.dirname(p) as string))\n` + return `${indent}${varName}.forEach((p) => mountDirs.add(path.dirname(p) as string))\n` } else { return `${indent}mountDirs.add(path.dirname(${varName} as string))\n` } @@ -18,13 +18,13 @@ function readFileIfNotInterfaceType(forNode, interfaceType, varName, indent, isA } else { if (interfaceType === 'TextFile') { if (isArray) { - return `${indent}${varName}.map((v) => {\n${indent}${indent}let vFile = v\n${indent}${indent}if (v instanceof File) {\n${indent}${indent} const vBuffer = await v.arrayBuffer()\n${indent}${indent} vFile = { path: v.name, data: new TextDecoder().decode(vBuffer) }\n${indent}${indent}}\n${indent}})\n` + return `${indent}const ${varName}File = await Promise.all(${varName}.map(async (v) => {\n${indent}${indent}let vFile = v\n${indent}${indent}if (v instanceof File) {\n${indent}${indent} const vBuffer = await v.arrayBuffer()\n${indent}${indent} vFile = { path: v.name, data: new TextDecoder().decode(vBuffer) }\n${indent}${indent}}\n${indent}${indent}return vFile\n${indent}}))\n` } else { return `${indent}let ${varName}File = ${varName}\n${indent}if (${varName} instanceof File) {\n${indent} const ${varName}Buffer = await ${varName}.arrayBuffer()\n${indent} ${varName}File = { path: ${varName}.name, data: new TextDecoder().decode(${varName}Buffer) }\n${indent}}\n` } } else { if (isArray) { - return `${indent}${varName}.map((v) => {\n${indent}let vFile = v\n${indent}${indent}if (v instanceof File) {\n${indent}${indent} const vBuffer = await v.arrayBuffer()\n${indent}${indent} vFile = { path: v.name, data: new Uint8Array(vBuffer) }\n${indent}${indent}}\n${indent}})\n` + return `${indent}const ${varName}File = await Promise.all(${varName}.map(async (v) => {\n${indent}let vFile = v\n${indent}${indent}if (v instanceof File) {\n${indent}${indent} const vBuffer = await v.arrayBuffer()\n${indent}${indent} vFile = { path: v.name, data: new Uint8Array(vBuffer) }\n${indent}${indent}}${indent}${indent}\nreturn vFile\n${indent}}))\n` } else { return `${indent}let ${varName}File = ${varName}\n${indent}if (${varName} instanceof File) {\n${indent} const ${varName}Buffer = await ${varName}.arrayBuffer()\n${indent} ${varName}File = { path: ${varName}.name, data: new Uint8Array(${varName}Buffer) }\n${indent}}\n` @@ -251,6 +251,9 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, const camel = camelCase(input.name) const isArray = input.itemsExpectedMax > 1 functionContent += readFileIfNotInterfaceType(forNode, interfaceType, camel, ' ', isArray) + if (!forNode && isArray) { + functionContent += ` const ${camel}PipelineInputs = ${camel}File.map(i => { return { type: InterfaceTypes.${interfaceType}, data: i as ${interfaceType} }})\n` + } } } }) @@ -259,9 +262,14 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, if (interfaceJsonTypeToInterfaceType.has(input.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(input.type) const camel = camelCase(input.name) + const isArray = input.itemsExpectedMax > 1 if (interfaceType.includes('File')) { if (!forNode) { - functionContent += ` { type: InterfaceTypes.${interfaceType}, data: ${camel}File as ${interfaceType} },\n` + if (isArray) { + functionContent += ` ...${camel}PipelineInputs,\n` + } else { + functionContent += ` { type: InterfaceTypes.${interfaceType}, data: ${camel}File as ${interfaceType} },\n` + } } } else { let data = camel @@ -283,16 +291,36 @@ function functionModule (srcOutputDir, forNode, interfaceJson, modulePascalCase, const camel = camelCase(input.name) if (interfaceJsonTypeToInterfaceType.has(input.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(input.type) + const isArray = input.itemsExpectedMax > 1 let name = ` const ${camel}Name = '${inputCount.toString()}'\n` if (interfaceType.includes('File')) { - if (forNode) { - name = ` const ${camel}Name = ${camel}\n` + if (isArray) { + name = '' } else { - name = ` const ${camel}Name = (${camel}File as ${interfaceType}).path\n` + if (forNode) { + name = ` const ${camel}Name = ${camel}\n` + } else { + name = ` const ${camel}Name = (${camel}File as ${interfaceType}).path\n` + } } } functionContent += name - functionContent += ` args.push(${camel}Name as string)\n\n` + if (isArray) { + if (forNode) { + functionContent += ` ${camel}.forEach((p) => args.push(p))\n` + } else { + functionContent += ` ${camel}File.forEach((p) => args.push((p as ${interfaceType}).path))\n` + } + if (forNode && interfaceType.includes('File')) { + functionContent += ` ${camel}.forEach((p) => mountDirs.add(path.dirname(p)))\n` + } + } else { + functionContent += ` args.push(${camel}Name)\n` + if (forNode && interfaceType.includes('File')) { + functionContent += ` mountDirs.add(path.dirname(${camel}Name))\n` + } + } + functionContent += '\n' inputCount++ } else { functionContent += ` args.push(${camel}.toString())\n\n` diff --git a/src/bindgen/typescript/typescript-bindings.js b/src/bindgen/typescript/typescript-bindings.js index d6e14c0de..005ddcb0a 100644 --- a/src/bindgen/typescript/typescript-bindings.js +++ b/src/bindgen/typescript/typescript-bindings.js @@ -15,6 +15,7 @@ import resultsModule from './results-module.js' import optionsModule from './options-module.js' import functionModule from './function-module.js' import outputOptionsCheck from '../output-options-check.js' +import inputArrayCheck from '../input-array-check.js' // Array of types that will require an import from itk-wasm function bindgenResource(filePath) { @@ -98,6 +99,7 @@ function typescriptBindings (outputDir, buildDir, wasmBinaries, options, forNode const { interfaceJson, parsedPath } = wasmBinaryInterfaceJson(outputDir, buildDir, wasmBinaryName) outputOptionsCheck(interfaceJson) + inputArrayCheck(interfaceJson) const moduleKebabCase = parsedPath.name const moduleCamelCase = camelCase(parsedPath.name) From ce051631569741a0264598fbd789d653ebba609f Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 28 Sep 2023 20:36:57 -0400 Subject: [PATCH 5/6] feat(bindgen): python emescripten function output file support --- .../core/python/itkwasm/itkwasm/__init__.py | 2 +- packages/core/python/itkwasm/itkwasm/pyodide.py | 4 ++-- .../emscripten/emscripten-function-module.js | 17 ++++++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/core/python/itkwasm/itkwasm/__init__.py b/packages/core/python/itkwasm/itkwasm/__init__.py index bc4b61213..bd795f7ce 100644 --- a/packages/core/python/itkwasm/itkwasm/__init__.py +++ b/packages/core/python/itkwasm/itkwasm/__init__.py @@ -1,6 +1,6 @@ """itkwasm: Python interface to itk-wasm WebAssembly modules.""" -__version__ = "1.0b141" +__version__ = "1.0b142" from .interface_types import InterfaceTypes from .image import Image, ImageType diff --git a/packages/core/python/itkwasm/itkwasm/pyodide.py b/packages/core/python/itkwasm/itkwasm/pyodide.py index 70beeb0c2..8538cdd6d 100644 --- a/packages/core/python/itkwasm/itkwasm/pyodide.py +++ b/packages/core/python/itkwasm/itkwasm/pyodide.py @@ -150,7 +150,7 @@ def to_py(js_proxy): # int, etc return js_proxy -def to_js(py): +def to_js(py, **kwargs): import pyodide import js if isinstance(py, list): @@ -222,4 +222,4 @@ def to_js(py): text_file_dict['data'] = data return pyodide.ffi.to_js(text_file_dict, dict_converter=js.Object.fromEntries) - return pyodide.ffi.to_js(py) + return pyodide.ffi.to_js(py, **kwargs) diff --git a/src/bindgen/python/emscripten/emscripten-function-module.js b/src/bindgen/python/emscripten/emscripten-function-module.js index bd2a1a59b..8809de801 100644 --- a/src/bindgen/python/emscripten/emscripten-function-module.js +++ b/src/bindgen/python/emscripten/emscripten-function-module.js @@ -12,9 +12,7 @@ import writeIfOverrideNotPresent from '../../write-if-override-not-present.js' function emscriptenFunctionModule(interfaceJson, pypackage, modulePath) { const functionName = snakeCase(interfaceJson.name) - let moduleContent = `# Generated file. Do not edit. - -from pathlib import Path + let moduleContent = `from pathlib import Path import os from typing import Dict, Tuple, Optional, List, Any @@ -49,6 +47,19 @@ from itkwasm import ( args += `to_js(${snakeCase(input.name)}), ` } }) + interfaceJson.outputs.forEach((output) => { + if (interfaceJsonTypeToInterfaceType.has(output.type)) { + const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) + switch (interfaceType) { + case "TextFile": + case "BinaryFile": + args += `to_js(${snakeCase(output.name)}), ` + break + default: + // + } + } + }) let addKwargs = '' interfaceJson.parameters.forEach((parameter) => { From e171721d9fbc34e1a66532f203c77106045f899f Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Thu, 28 Sep 2023 21:20:44 -0400 Subject: [PATCH 6/6] feat(bindgen): python dispatch function output file support --- src/bindgen/python/dispatch-function-module.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/bindgen/python/dispatch-function-module.js b/src/bindgen/python/dispatch-function-module.js index 34a6ed32a..ab4fdaeb8 100644 --- a/src/bindgen/python/dispatch-function-module.js +++ b/src/bindgen/python/dispatch-function-module.js @@ -6,6 +6,7 @@ import functionModuleImports from './function-module-imports.js' import functionModuleArgs from './function-module-args.js' import functionModuleDocstring from './function-module-docstring.js' import functionModuleReturnType from './function-module-return-type.js' +import interfaceJsonTypeToInterfaceType from '../interface-json-type-to-interface-type.js' function dispatchFunctionModule(interfaceJson, pypackage, modulePath) { const functionName = snakeCase(interfaceJson.name) @@ -24,9 +25,22 @@ from itkwasm import ( const docstring = functionModuleDocstring(interfaceJson) let functionArgsToPass = "" - interfaceJson['inputs'].forEach((value) => { + interfaceJson.inputs.forEach((value) => { functionArgsToPass += `${snakeCase(value.name)}, ` }) + interfaceJson.outputs.forEach((output) => { + if (interfaceJsonTypeToInterfaceType.has(output.type)) { + const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) + switch (interfaceType) { + case "TextFile": + case "BinaryFile": + functionArgsToPass += `${snakeCase(output.name)}, ` + break + default: + // + } + } + }) interfaceJson['parameters'].forEach((value) => { if (value.name === "memory-io" || value.name === "version") { return