Skip to content

Commit

Permalink
Demo of PD loading Python protocols.
Browse files Browse the repository at this point in the history
  • Loading branch information
ddcc4 committed Dec 9, 2024
1 parent db7e48a commit 11663f7
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 4 deletions.
2 changes: 1 addition & 1 deletion components/src/modals/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const Modal = (props: ModalProps): JSX.Element => {
position={position}
showOverlay={showOverlay}
zIndexOverlay={zIndexOverlay}
width={styleProps.width ?? '31.25rem'}
width={styleProps.width ?? '40rem'}
header={modalHeader}
onOutsideClick={closeOnOutsideClick ?? false ? onClose : undefined}
// center within viewport aside from nav
Expand Down
1 change: 1 addition & 0 deletions protocol-designer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
<body>
<div id="root"></div>
<script type="module" src="./src/index.tsx"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/brython@3/brython.js"></script>
</body>
</html>
10 changes: 8 additions & 2 deletions protocol-designer/src/load-file/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
LoadFileAction,
NewProtocolFields,
} from './types'
import { pythonProtoParserDemo } from './pythondemo'

export interface FileUploadMessageAction {
type: 'FILE_UPLOAD_MESSAGE'
payload: FileUploadMessage
Expand Down Expand Up @@ -52,15 +54,19 @@ export const loadProtocolFile = (
// reset the state of the input to allow file re-uploads
event.currentTarget.value = ''

if (!file.name.endsWith('.json')) {
if (!file.name.endsWith('.json') && !file.name.endsWith('.py')) {
fileError('INVALID_FILE_TYPE')
} else {
reader.onload = readEvent => {
const result = ((readEvent.currentTarget as any) as FileReader).result
let parsedProtocol: PDProtocolFile | null | undefined

try {
parsedProtocol = JSON.parse((result as any) as string)
if (file.name.endsWith('.py')) {
pythonProtoParserDemo(result, file.name)
} else {
parsedProtocol = JSON.parse((result as any) as string)
}
// TODO LATER Ian 2018-05-18 validate file with JSON Schema here
parsedProtocol && dispatch(loadFileAction(parsedProtocol))
} catch (error) {
Expand Down
137 changes: 137 additions & 0 deletions protocol-designer/src/load-file/pythondemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
function transformAstToStruct(astNode: object) {

Check failure on line 1 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Missing return type on function
// The Brython AST nodes are annoying because you have to peek into their prototypes to see
// what the object class is. It's more convenient to have all the relevant properties in a
// flat object. And we want to make it easier to use destructuring to extract things from
// the AST.
const nodeClassName = astNode.constructor.$name;

Check failure on line 6 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Property '$name' does not exist on type 'Function'. Did you mean 'name'?
const struct = {};
struct[nodeClassName] = (() => {

Check failure on line 8 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{}'.
switch (astNode.constructor) {
case $B.ast.Module: return {

Check failure on line 10 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Cannot find name '$B'.
body: astNode.body.map(transformAstToStruct)

Check failure on line 11 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Property 'body' does not exist on type 'object'.
}
case $B.ast.FunctionDef: return {

Check failure on line 13 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Cannot find name '$B'.
name: astNode.name,

Check failure on line 14 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Property 'name' does not exist on type 'object'.
args: transformAstToStruct(astNode.args),

Check failure on line 15 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Property 'args' does not exist on type 'object'.

Check failure on line 15 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Unsafe argument of type `any` assigned to a parameter of type `object`
body: astNode.body.map(transformAstToStruct),

Check failure on line 16 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Property 'body' does not exist on type 'object'.
}
case $B.ast.arguments: return {

Check failure on line 18 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Cannot find name '$B'.
args: astNode.args.map(transformAstToStruct),

Check failure on line 19 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Property 'args' does not exist on type 'object'.
}
case $B.ast.arg: return {
arg: astNode.arg,
}
case $B.ast.Assign: return {
targets: astNode.targets.map(transformAstToStruct),
value: transformAstToStruct(astNode.value),

Check failure on line 26 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Unsafe argument of type `any` assigned to a parameter of type `object`
}
case $B.ast.Expr: return {
value: transformAstToStruct(astNode.value),

Check failure on line 29 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Unsafe argument of type `any` assigned to a parameter of type `object`
}
case $B.ast.Call: return {
func: transformAstToStruct(astNode.func),

Check failure on line 32 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Unsafe argument of type `any` assigned to a parameter of type `object`
args: astNode.args.map(transformAstToStruct),
}
case $B.ast.Attribute: return {
value: transformAstToStruct(astNode.value),

Check failure on line 36 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Unsafe argument of type `any` assigned to a parameter of type `object`
attr: astNode.attr,
}
case $B.ast.Dict: return {
keys: astNode.keys.map(transformAstToStruct),
values: astNode.values.map(transformAstToStruct),
}
case $B.ast.Name: return {
id: astNode.id
}
case $B.ast.Constant: return {
value: astNode.value
}
}
})();
return struct;
}

function prettyPrint(astNode: object) {

Check failure on line 54 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Missing return type on function
return astNode.Expr && prettyPrint(astNode.Expr.value) ||

Check failure on line 55 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Unsafe argument of type `any` assigned to a parameter of type `object`
astNode.Assign && `Set [${astNode.Assign.targets.map(prettyPrint).join(', ')}] := ${prettyPrint(astNode.Assign.value)}` ||

Check failure on line 56 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Unsafe argument of type `any` assigned to a parameter of type `object`
astNode.Call && `Call ${prettyPrint(astNode.Call.func)}(${astNode.Call.args.map(prettyPrint).join(', ')})` ||

Check failure on line 57 in protocol-designer/src/load-file/pythondemo.ts

View workflow job for this annotation

GitHub Actions / js checks

Unsafe argument of type `any` assigned to a parameter of type `object`
astNode.Attribute && `{${prettyPrint(astNode.Attribute.value)}}.${astNode.Attribute.attr}` ||
astNode.Name && `${astNode.Name.id}` ||
astNode.Constant && `${astNode.Constant.value}`;
}

export function pythonProtoParserDemo(pythonCode: string, filename: string): void {
// Use the Brython parser to parse the file.
const parser = new $B.Parser(pythonCode, filename, 'file');
const brythonAst = $B._PyPegen.run_parser(parser);
console.log(brythonAst);
const ast = transformAstToStruct(brythonAst);
console.log(ast);

// Let's verify that the Python protocol has a requirements section.
const {Module: {body: moduleBody}} = ast;
const requirementsNode = moduleBody.find((node) => {
// The `requirements={...}` statement is an assignment, so look for an `Assign` node in the AST.
// The assignment `target` is a list because Python allows multiple assignment (`a, b = 1, 2`).
// So we're looking for an assignment whose `target` a single-element list ['requirements'].
try {
const {Assign: {targets: [{Name: {id: assignToName}}, ...moreTargets]}} = node;
return !moreTargets.length && assignToName == 'requirements';
} catch {
}
})
console.log('requirements node:', requirementsNode);
if (!requirementsNode) {
throw new Error('requirements not found in Python file');
}

// Here's how we can extract settings from the parsed `requirements`:
const {Assign: {value: {Dict: {keys: requirementsKeyNodes, values: requirementsValueNodes}}}} = requirementsNode;
const requirementsKeys = requirementsKeyNodes.map((node) => {
const {Constant: {value}} = node;
return value;
})
const requirementsValues = requirementsValueNodes.map((node, idx) => {
try {
const {Constant: {value}} = node;
return value;
} catch {
throw Error(`requirements dict value is not literal: '${requirementsKeys[idx]}' is a ${Object.keys(node)[0]}`);
}
})
const requirementsDict = Object.fromEntries(requirementsKeys.map((key, idx) => [key, requirementsValues[idx]]));
console.log(requirementsDict);

// Let's find the run() function:
const defRunNode = moduleBody.find((node) => {
// The `def run(): ...` statement is FunctionDef node.
try {
const {FunctionDef: {name}} = node;
return name == 'run';
} catch {
}
});
console.log('run() node:', defRunNode);
if (!defRunNode) {
throw new Error('def run() not found in Python file');
}
const {FunctionDef: {args: {arguments: {args: runArgsNodes}}, body: runBody}} = defRunNode;
console.log(runArgsNodes, runBody);

// See what they named the argument to the run() function:
if (runArgsNodes.length != 1) {
throw new Error(`def run() needs 1 argument but got ${runArgsNodes.length}`);
}
const {arg: {arg: runArgName}} = runArgsNodes[0];

// Now we'll look inside the run() function body:
console.log(`def run(${runArgName}) has ${runBody.length} statements`);
const parsedRunFunc = runBody.map((node, idx) => {
return `${idx+1}: ${prettyPrint(node)}`;
});

throw Error(`DEMO! Here's what I got from the Python file:\n\n` +
`requirements = ${JSON.stringify(requirementsDict)}\n\n` +
`EXTRACTED RUN(${runArgName}) FUNCTION: \n` +
`${parsedRunFunc.join('\n')}`)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const invalidJsonModal = (props: ModalProps): ModalContents => {
{t('invalid_json_file_error')}
</StyledText>
<StyledText desktopStyle="bodyDefaultRegular" color={COLORS.red50}>
{errorMessage}
<span style={{whiteSpace: 'pre-line'}}>{errorMessage}</span>
</StyledText>
</Flex>
</Flex>
Expand Down

0 comments on commit 11663f7

Please sign in to comment.