Skip to content

Commit f0d5eda

Browse files
committed
feat: add support for CodeBlocks with argv
Signed-off-by: Nick Mitchell <[email protected]>
1 parent 41f6429 commit f0d5eda

File tree

7 files changed

+213
-16
lines changed

7 files changed

+213
-16
lines changed

pdl-live-react/src/pdl_ast.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,6 +1919,11 @@ export type Kind15 = "code"
19191919
*
19201920
*/
19211921
export type Lang = "python" | "command" | "jinja" | "pdl"
1922+
/**
1923+
* Pip requirements.txt
1924+
*
1925+
*/
1926+
export type Requirements = string | string[] | null
19221927
/**
19231928
* Code to execute.
19241929
*
@@ -1947,6 +1952,32 @@ export type Code =
19471952
| ImportBlock
19481953
| ErrorBlock
19491954
| EmptyBlock
1955+
| (
1956+
| boolean
1957+
| number
1958+
| string
1959+
| FunctionBlock
1960+
| CallBlock
1961+
| LitellmModelBlock
1962+
| GraniteioModelBlock
1963+
| CodeBlock
1964+
| GetBlock
1965+
| DataBlock
1966+
| IfBlock
1967+
| MatchBlock
1968+
| RepeatBlock
1969+
| TextBlock
1970+
| LastOfBlock
1971+
| ArrayBlock
1972+
| ObjectBlock
1973+
| MessageBlock
1974+
| ReadBlock
1975+
| IncludeBlock
1976+
| ImportBlock
1977+
| ErrorBlock
1978+
| EmptyBlock
1979+
| null
1980+
)[]
19501981
| null
19511982
/**
19521983
* Name of the variable used to store the result of the execution of the block.
@@ -2837,6 +2868,7 @@ export interface CodeBlock {
28372868
pdl__is_leaf?: PdlIsLeaf15
28382869
kind?: Kind15
28392870
lang: Lang
2871+
requirements?: Requirements
28402872
code: Code
28412873
}
28422874
/**

src/pdl/pdl-schema.json

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,25 @@
13061306
"title": "Lang",
13071307
"type": "string"
13081308
},
1309+
"requirements": {
1310+
"anyOf": [
1311+
{
1312+
"type": "string"
1313+
},
1314+
{
1315+
"items": {
1316+
"type": "string"
1317+
},
1318+
"type": "array"
1319+
},
1320+
{
1321+
"type": "null"
1322+
}
1323+
],
1324+
"default": null,
1325+
"description": "Pip requirements.txt\n ",
1326+
"title": "Requirements"
1327+
},
13091328
"code": {
13101329
"anyOf": [
13111330
{
@@ -1380,6 +1399,88 @@
13801399
{
13811400
"$ref": "#/$defs/EmptyBlock"
13821401
},
1402+
{
1403+
"items": {
1404+
"anyOf": [
1405+
{
1406+
"type": "boolean"
1407+
},
1408+
{
1409+
"type": "integer"
1410+
},
1411+
{
1412+
"type": "number"
1413+
},
1414+
{
1415+
"type": "string"
1416+
},
1417+
{
1418+
"$ref": "#/$defs/FunctionBlock"
1419+
},
1420+
{
1421+
"$ref": "#/$defs/CallBlock"
1422+
},
1423+
{
1424+
"$ref": "#/$defs/LitellmModelBlock"
1425+
},
1426+
{
1427+
"$ref": "#/$defs/GraniteioModelBlock"
1428+
},
1429+
{
1430+
"$ref": "#/$defs/CodeBlock"
1431+
},
1432+
{
1433+
"$ref": "#/$defs/GetBlock"
1434+
},
1435+
{
1436+
"$ref": "#/$defs/DataBlock"
1437+
},
1438+
{
1439+
"$ref": "#/$defs/IfBlock"
1440+
},
1441+
{
1442+
"$ref": "#/$defs/MatchBlock"
1443+
},
1444+
{
1445+
"$ref": "#/$defs/RepeatBlock"
1446+
},
1447+
{
1448+
"$ref": "#/$defs/TextBlock"
1449+
},
1450+
{
1451+
"$ref": "#/$defs/LastOfBlock"
1452+
},
1453+
{
1454+
"$ref": "#/$defs/ArrayBlock"
1455+
},
1456+
{
1457+
"$ref": "#/$defs/ObjectBlock"
1458+
},
1459+
{
1460+
"$ref": "#/$defs/MessageBlock"
1461+
},
1462+
{
1463+
"$ref": "#/$defs/ReadBlock"
1464+
},
1465+
{
1466+
"$ref": "#/$defs/IncludeBlock"
1467+
},
1468+
{
1469+
"$ref": "#/$defs/ImportBlock"
1470+
},
1471+
{
1472+
"$ref": "#/$defs/ErrorBlock"
1473+
},
1474+
{
1475+
"$ref": "#/$defs/EmptyBlock"
1476+
},
1477+
{
1478+
"type": "null"
1479+
}
1480+
]
1481+
},
1482+
"type": "array"
1483+
},
13831484
{
13841485
"type": "null"
13851486
}

src/pdl/pdl_ast.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@
1515
Union,
1616
)
1717

18-
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, RootModel
18+
from pydantic import (
19+
BaseModel,
20+
BeforeValidator,
21+
ConfigDict,
22+
Field,
23+
RootModel,
24+
model_validator,
25+
)
1926
from pydantic.json_schema import SkipJsonSchema
2027

2128
from .pdl_lazy import PdlDict, PdlLazy
@@ -462,10 +469,23 @@ class CodeBlock(LeafBlock):
462469
]
463470
"""Programming language of the code.
464471
"""
465-
code: "BlockType"
472+
requirements: Optional[str | list[str]] = None
473+
"""Pip requirements.txt
474+
"""
475+
code: "BlockOrBlocksType"
466476
"""Code to execute.
467477
"""
468478

479+
@model_validator(mode="after")
480+
def lang_is_python(self):
481+
if self.requirements is not None and self.lang != "python":
482+
raise ValueError(
483+
"CodeBlock requirements field provided for non-python block"
484+
)
485+
if isinstance(self.code, list) and self.lang != "command":
486+
raise ValueError("CodeBlock code field is array for non-command block")
487+
return self
488+
469489

470490
class GetBlock(LeafBlock):
471491
"""

src/pdl/pdl_ast_utils.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ def iter_block_children(f: Callable[[BlockType], None], block: BlockType) -> Non
4848
if block.pdl__trace is not None:
4949
f(block.pdl__trace)
5050
case CodeBlock():
51-
f(block.code)
51+
if isinstance(block.code, list):
52+
for b in block.code:
53+
f(b)
54+
else:
55+
f(block.code)
5256
case GetBlock():
5357
pass
5458
case DataBlock():
@@ -150,7 +154,10 @@ def map_block_children(f: MappedFunctions, block: BlockType) -> BlockType:
150154
if block.parameters is not None:
151155
block.parameters = f.f_expr(block.parameters)
152156
case CodeBlock():
153-
block.code = f.f_block(block.code)
157+
if isinstance(block.code, list):
158+
block.code = [f.f_block(b) for b in block.code]
159+
else:
160+
block.code = f.f_block(block.code)
154161
case GetBlock():
155162
pass
156163
case DataBlock():

src/pdl/pdl_dumper.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ def block_to_dict( # noqa: C901
135135
d["modelResponse"] = block.modelResponse
136136
case CodeBlock():
137137
d["lang"] = block.lang
138-
d["code"] = block_to_dict(block.code, json_compatible)
138+
if isinstance(block.code, list):
139+
d["code"] = [block_to_dict(b, json_compatible) for b in block.code]
140+
else:
141+
d["code"] = block_to_dict(block.code, json_compatible)
142+
d["requirements"] = block.requirements
139143
case GetBlock():
140144
d["get"] = block.get
141145
case DataBlock():

src/pdl/pdl_interpreter.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,14 +1431,24 @@ def process_call_code(
14311431
state: InterpreterState, scope: ScopeType, block: CodeBlock, loc: PdlLocationType
14321432
) -> tuple[PdlLazy[Any], LazyMessages, ScopeType, CodeBlock]:
14331433
background: LazyMessages
1434-
code_, _, _, block = process_block_of(
1435-
block,
1436-
"code",
1437-
state.with_yield_result(False).with_yield_background(False),
1438-
scope,
1439-
loc,
1440-
)
1441-
code_s = code_.result()
1434+
code_a = None
1435+
if isinstance(block.code, list):
1436+
code_s = ""
1437+
code_a, _, _, _ = process_block(
1438+
state.with_yield_result(False).with_yield_background(False),
1439+
scope,
1440+
ArrayBlock(array=block.code),
1441+
loc,
1442+
)
1443+
else:
1444+
code_, _, _, block = process_block_of(
1445+
block,
1446+
"code",
1447+
state.with_yield_result(False).with_yield_background(False),
1448+
scope,
1449+
loc,
1450+
)
1451+
code_s = code_.result()
14421452
match block.lang:
14431453
case "python":
14441454
try:
@@ -1456,7 +1466,7 @@ def process_call_code(
14561466
) from exc
14571467
case "command":
14581468
try:
1459-
result = call_command(code_s)
1469+
result = call_command(code_s, code_a)
14601470
background = PdlList(
14611471
[
14621472
PdlDict( # type: ignore
@@ -1530,8 +1540,11 @@ def call_python(code: str, scope: ScopeType) -> PdlLazy[Any]:
15301540
return PdlConst(result)
15311541

15321542

1533-
def call_command(code: str) -> PdlLazy[str]:
1534-
args = shlex.split(code)
1543+
def call_command(code: str, code_a: PdlLazy[list[str]] | None) -> PdlLazy[str]:
1544+
if code_a is not None and isinstance(code_a.result(), list):
1545+
args = code_a.result()
1546+
else:
1547+
args = shlex.split(code)
15351548
p = subprocess.run(
15361549
args, capture_output=True, text=True, check=False, shell=False
15371550
) # nosec B603

tests/test_code.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ def test_contribute_false():
6161
]
6262
}
6363

64+
command_data_args = {
65+
"lastOf": [
66+
{
67+
"def": "world",
68+
"lang": "command",
69+
"code": ["echo", "-n", "World"],
70+
"contribute": [],
71+
},
72+
"Hello ${ world }!",
73+
]
74+
}
75+
6476

6577
def test_command():
6678
result = exec_dict(command_data, output="all")
@@ -70,6 +82,14 @@ def test_command():
7082
assert scope["world"] == "World"
7183

7284

85+
def test_command_args():
86+
result = exec_dict(command_data_args, output="all")
87+
document = result["result"]
88+
scope = result["scope"]
89+
assert document == "Hello World!"
90+
assert scope["world"] == "World"
91+
92+
7393
def test_jinja1():
7494
prog_str = """
7595
defs:

0 commit comments

Comments
 (0)