Skip to content

Commit f12850d

Browse files
allow expressions in config overrides
1 parent 6eb813b commit f12850d

8 files changed

Lines changed: 104 additions & 9 deletions

File tree

.agents/skills/pioreactor-experiment-profiles/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pioreactors:
6363
# if: <bool_or_expression>
6464
# options: {<option_name>: <value>} # expressions allowed via ${{ }}
6565
# args: [<string>, ...]
66-
# config_overrides: {<config_name>: <value>}
66+
# config_overrides: {<config_name>: <value>} # expressions allowed via ${{ }}
6767
#
6868
# - type: update
6969
# t: <time_string_or_float>

core/pioreactor/actions/leader/experiment_profile.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ def start_job(
725725
action_metrics: ActionMetrics,
726726
options: dict[str, Any],
727727
args: list[str],
728-
config_overrides: dict[str, str],
728+
config_overrides: dict[str, Any],
729729
) -> Callable[..., None]:
730730
def _callable() -> None:
731731
nonlocal env
@@ -741,13 +741,15 @@ def _callable() -> None:
741741
return
742742

743743
if dry_run:
744+
evaluated_config_overrides = evaluate_options(config_overrides, env)
744745
logger.info(
745-
f"{action_count}. Dry-run: Starting {job_name} on {unit} with options {evaluate_options(options, env)} and args {args}."
746+
f"{action_count}. Dry-run: Starting {job_name} on {unit} with options {evaluate_options(options, env)} and args {args}, and overrides {evaluated_config_overrides}."
746747
)
747748
else:
748749
evaluated_options = evaluate_options(options, env)
750+
evaluated_config_overrides = evaluate_options(config_overrides, env)
749751
logger.debug(
750-
f"{action_count}. Starting {job_name} on {unit} with options {evaluated_options}, args {args}, and overrides {config_overrides}."
752+
f"{action_count}. Starting {job_name} on {unit} with options {evaluated_options}, args {args}, and overrides {evaluated_config_overrides}."
751753
)
752754
address = resolve_to_address(unit)
753755
for attempt in range(1, START_JOB_SUBMIT_MAX_ATTEMPTS + 1):
@@ -760,8 +762,8 @@ def _callable() -> None:
760762
"env": _get_worker_env_for_start(unit, experiment, parent_job.job_key),
761763
"args": args,
762764
"config_overrides": [
763-
[f"{job_name}.config", key, value]
764-
for (key, value) in config_overrides.items()
765+
[f"{job_name}.config", key, str(value)]
766+
for (key, value) in evaluated_config_overrides.items()
765767
],
766768
},
767769
)

core/pioreactor/experiment_profiles/profiles_spec.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pioreactors:
4949
if: <bool_or_expression>
5050
options: {<option_name>: <value>} # expressions allowed via ${{ }}
5151
args: [<string>, ...]
52-
config_overrides: {<config_name>: <value>}
52+
config_overrides: {<config_name>: <value>} # expressions allowed via ${{ }}
5353

5454
- type: update
5555
t: <time_string_or_float>

core/pioreactor/experiment_profiles/validate.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,18 @@ def _validate_options_expressions(
203203
)
204204

205205

206+
def _validate_config_override_expressions(
207+
diagnostics: list[Diagnostic], *, path: str, config_overrides: dict[str, Any]
208+
) -> None:
209+
for key, value in config_overrides.items():
210+
if is_bracketed_expression(value):
211+
_validate_expression_field(
212+
diagnostics,
213+
path=f"{path}.config_overrides.{key}",
214+
expression=value,
215+
)
216+
217+
206218
def _validate_log_message_expressions(diagnostics: list[Diagnostic], *, path: str, message: str) -> None:
207219
for match in re.findall(FLEXIBLE_EXPRESSION_PATTERN, message):
208220
_validate_expression_field(
@@ -369,6 +381,11 @@ def _validate_action_expressions(diagnostics: list[Diagnostic], *, path: str, ac
369381
if isinstance(action, (struct.Start, struct.Update)):
370382
_validate_options_expressions(diagnostics, path=path, options=action.options)
371383

384+
if isinstance(action, struct.Start):
385+
_validate_config_override_expressions(
386+
diagnostics, path=path, config_overrides=action.config_overrides
387+
)
388+
372389
if isinstance(action, struct.Log):
373390
_validate_log_message_expressions(diagnostics, path=path, message=action.options.message)
374391

core/tests/test_execute_experiment_profile.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,40 @@ def test_execute_experiment_profile_hack_for_led_intensity(
173173
}
174174

175175

176+
@patch("pioreactor.actions.leader.experiment_profile._load_experiment_profile")
177+
def test_execute_experiment_profile_start_evaluates_config_overrides(
178+
mock__load_experiment_profile,
179+
) -> None:
180+
experiment = "_testing_experiment"
181+
profile = Profile(
182+
experiment_profile_name="test_profile",
183+
inputs={"dc": 100},
184+
pioreactors={
185+
"unit1": PioreactorSpecificBlock(
186+
jobs={
187+
"air_bubbler": Job(
188+
actions=[
189+
Start(
190+
t="0s",
191+
config_overrides={"duty_cycle": "${{dc}}"},
192+
)
193+
]
194+
)
195+
}
196+
)
197+
},
198+
metadata=Metadata(author="test_author"),
199+
)
200+
201+
mock__load_experiment_profile.return_value = profile
202+
203+
with capture_requests() as bucket:
204+
execute_experiment_profile("profile.yaml", experiment)
205+
206+
assert bucket[0].url == "http://unit1.local:4999/unit_api/jobs/run/job_name/air_bubbler"
207+
assert bucket[0].json["config_overrides"] == [["air_bubbler.config", "duty_cycle", "100"]]
208+
209+
176210
@patch("pioreactor.actions.leader.experiment_profile._load_experiment_profile")
177211
def test_execute_experiment_profile_start_failure_is_logged(
178212
mock__load_experiment_profile, caplog: pytest.LogCaptureFixture
@@ -1568,7 +1602,7 @@ def test_execute_experiment_profile_with_config_overrides(
15681602
action = Start(
15691603
hours_elapsed=0,
15701604
options={"target": "${{unit1:jobbing:target + 1}}", "dont_eval": "1.0 + 1.0"},
1571-
config_overrides={"option1": "value1", "option2": "value2"},
1605+
config_overrides={"option1": "value1", "option2": "value2", "option3": 100},
15721606
)
15731607

15741608
profile = Profile(
@@ -1604,5 +1638,6 @@ def test_execute_experiment_profile_with_config_overrides(
16041638
"config_overrides": [
16051639
["jobbing.config", "option1", "value1"],
16061640
["jobbing.config", "option2", "value2"],
1641+
["jobbing.config", "option3", "100"],
16071642
],
16081643
}

core/tests/test_validate_experiment_profile.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ def test_validate_profile_checks_all_expression_fields_with_full_parser() -> Non
117117
t: 0s
118118
options:
119119
target_rpm: "${{ 1 + }}"
120+
- type: start
121+
t: 0s
122+
config_overrides:
123+
target_rpm: "${{ 1 + }}"
120124
- type: log
121125
t: 0s
122126
options:
@@ -136,7 +140,8 @@ def test_validate_profile_checks_all_expression_fields_with_full_parser() -> Non
136140
"common.jobs.stirring.actions[2].condition",
137141
"common.jobs.stirring.actions[3].wait_until",
138142
"common.jobs.stirring.actions[4].options.target_rpm",
139-
"common.jobs.stirring.actions[5].options.message",
143+
"common.jobs.stirring.actions[5].config_overrides.target_rpm",
144+
"common.jobs.stirring.actions[6].options.message",
140145
}
141146

142147

frontend/src/__tests__/ExperimentProfileEditor.test.jsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,28 @@ pioreactors:
206206

207207
expect(screen.getAllByText("??").length).toBeGreaterThan(0);
208208
});
209+
210+
test("shows invalid config overrides syntax when config overrides are written as a list", () => {
211+
renderEditor({
212+
initialCode: `experiment_profile_name: preview
213+
214+
pioreactors:
215+
xr1:
216+
jobs:
217+
stirring:
218+
actions:
219+
- type: update
220+
t: 0s
221+
options:
222+
target_rpm: 500
223+
config_overrides:
224+
- target_rpm: 500
225+
`,
226+
initialFilename: "draft_profile",
227+
filenameEditable: true,
228+
onSave: async () => {},
229+
});
230+
231+
expect(screen.getByText("invalid config overrides syntax!")).toBeInTheDocument();
232+
});
209233
});

frontend/src/components/DisplayProfile.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,17 @@ const ActionDetails = ({ action, jobName, index, actionPath, comments }) => {
385385
return null;
386386
};
387387

388+
const renderInvalidConfigOverridesMessage = () => {
389+
if (Array.isArray(action.config_overrides)) {
390+
return (
391+
<Typography variant="body2" component="div" sx={level3}>
392+
<UnderlineSpan title="`config_overrides` field doesn't use `-` in front. Remove it.">invalid config overrides syntax!</UnderlineSpan>
393+
</Typography>
394+
);
395+
}
396+
return null;
397+
};
398+
388399
switch (action?.type) {
389400
case 'start':
390401
case 'update':
@@ -396,6 +407,7 @@ const ActionDetails = ({ action, jobName, index, actionPath, comments }) => {
396407
{renderOptions(action?.type)}
397408
{renderInvalidOptionsMessage()}
398409
{renderConfigOverrides()}
410+
{renderInvalidConfigOverridesMessage()}
399411
</>
400412
);
401413
case 'log':

0 commit comments

Comments
 (0)