Skip to content

Commit 476c0a3

Browse files
ddcc4jerader
andauthored
feat(protocol-designer): utility functions for Python generation (#17419)
# Overview These are utility functions for rendering basic objects into Python, to be used for Python code generation from Protocol Designer. AUTH-1385 ## Test Plan and Hands on Testing Added unit tests for all the JavaScript -> Python types we support. I'm also using this code in my private branch. ## Review requests Is this the right directory for a utils file? I noticed that there's both: - `step-generation/src/utils` - `step-generation/lib/utils` What's the difference? I'm planning to use these functions from both `protocol-designer` and `step-generation`. ## Risk assessment Low: nothing calls this code right now. --------- Co-authored-by: Jethary Alcid <[email protected]>
1 parent 9d28971 commit 476c0a3

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { formatPyStr, formatPyValue } from '../utils/pythonFormat'
3+
4+
describe('pythonFormat utils', () => {
5+
it('format string', () => {
6+
expect(
7+
formatPyStr(`Funky quotes " '\nNewline\tUnicode µ, Backslash\\`)
8+
).toEqual(`"Funky quotes \\" '\\nNewline\\tUnicode µ, Backslash\\\\"`)
9+
})
10+
11+
it('format number', () => {
12+
expect(formatPyValue(3.14)).toBe('3.14')
13+
expect(formatPyValue(-1e-10)).toBe('-1e-10')
14+
// this is the valid way to write these values in Python:
15+
expect(formatPyValue(-1 / 0)).toBe('float("-Infinity")')
16+
expect(formatPyValue(0 / 0)).toBe('float("NaN")')
17+
})
18+
19+
it('format boolean', () => {
20+
expect(formatPyValue(true)).toBe('True')
21+
expect(formatPyValue(false)).toBe('False')
22+
})
23+
24+
it('format list', () => {
25+
expect(
26+
formatPyValue(['hello', 'world', 2.71828, true, false, undefined])
27+
).toBe('["hello", "world", 2.71828, True, False, None]')
28+
})
29+
30+
it('format dict', () => {
31+
// null:
32+
expect(formatPyValue(null)).toBe('None')
33+
// zero entries:
34+
expect(formatPyValue({})).toBe('{}')
35+
// one entry:
36+
expect(formatPyValue({ one: 'two' })).toBe('{"one": "two"}')
37+
expect(formatPyValue({ 3: 4 })).toBe('{"3": 4}')
38+
// multiple entries:
39+
expect(formatPyValue({ yes: true, no: false })).toBe(
40+
'{\n "yes": True,\n "no": False,\n}'
41+
)
42+
// nested entries:
43+
expect(
44+
formatPyValue({ hello: 'world', nested: { inner: 5, extra: 6 } })
45+
).toBe(
46+
'{\n "hello": "world",\n "nested": {\n "inner": 5,\n "extra": 6,\n },\n}'
47+
)
48+
})
49+
})

step-generation/src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ export * from './safePipetteMovements'
2727
export * from './wasteChuteCommandsUtil'
2828
export * from './createTimelineFromRunCommands'
2929
export * from './constructInvariantContextFromRunCommands'
30+
export * from './pythonFormat'
3031
export const uuid: () => string = uuidv4
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/** Utility functions for Python code generation. */
2+
3+
const INDENT = ' '
4+
5+
/** Indent each of the lines in `text`. */
6+
export function indentPyLines(text: string): string {
7+
return text
8+
.split('\n')
9+
.map(line => (line ? INDENT + line : line))
10+
.join('\n')
11+
}
12+
13+
/** Render an arbitrary JavaScript value to Python. */
14+
export function formatPyValue(value: any): string {
15+
switch (typeof value) {
16+
case 'undefined':
17+
return 'None'
18+
case 'boolean':
19+
return value ? 'True' : 'False'
20+
case 'number':
21+
// `float("Infinity")` and `float("NaN")` is how you write those values in Python
22+
return Number.isFinite(value) ? `${value}` : `float("${value}")`
23+
case 'string':
24+
return formatPyStr(value)
25+
case 'object':
26+
if (value === null) {
27+
return 'None'
28+
} else if (Array.isArray(value)) {
29+
return formatPyList(value)
30+
} else {
31+
return formatPyDict(value as Record<string, any>)
32+
}
33+
default:
34+
throw Error('Cannot render value as Python', { cause: value })
35+
}
36+
}
37+
38+
/** Render the string value to Python. */
39+
export function formatPyStr(str: string): string {
40+
// Later, we can do something more elegant like outputting 'single-quoted' if str contains
41+
// double-quotes, but for now stringify() produces a valid and properly escaped Python string.
42+
return JSON.stringify(str)
43+
}
44+
45+
/** Render an array value as a Python list. */
46+
export function formatPyList(list: any[]): string {
47+
return `[${list.map(value => formatPyValue(value)).join(', ')}]`
48+
}
49+
50+
/** Render an object as a Python dict. */
51+
export function formatPyDict(dict: Record<string, any>): string {
52+
const dictEntries = Object.entries(dict)
53+
// Render dict on single line if it has 1 entry, else render 1 entry per line.
54+
if (dictEntries.length <= 1) {
55+
return `{${dictEntries
56+
.map(([key, value]) => `${formatPyStr(key)}: ${formatPyValue(value)}`)
57+
.join(', ')}}`
58+
} else {
59+
return `{\n${indentPyLines(
60+
dictEntries
61+
.map(([key, value]) => `${formatPyStr(key)}: ${formatPyValue(value)}`)
62+
.join(',\n')
63+
)},\n}`
64+
}
65+
}

0 commit comments

Comments
 (0)