Skip to content

Commit 257b8c1

Browse files
authored
fix: allow to set all configs via tesseract build --config-override (#239)
#### Relevant issue or PR Fixes #232 #### Description of changes - Parse values passed as YAML, so arguments that expect lists (like `build_config.package_data`) can be set. - Some more error checking and helpful error messages for good measure. - Add more testing to ensure things work as intended. #### Testing done CI, old and new tests
1 parent 281ca9d commit 257b8c1

File tree

3 files changed

+105
-20
lines changed

3 files changed

+105
-20
lines changed

tesseract_core/sdk/cli.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import click
1919
import typer
20+
import yaml
2021
from jinja2 import Environment, PackageLoader, StrictUndefined
2122
from pydantic import ValidationError as PydanticValidationError
2223
from rich.console import Console as RichConsole
@@ -193,27 +194,38 @@ def main_callback(
193194

194195
def _parse_config_override(
195196
options: list[str] | None,
196-
) -> tuple[tuple[list[str], str], ...]:
197+
) -> dict[tuple[str, ...], Any]:
197198
"""Parse `["path1.path2.path3=value"]` into `[(["path1", "path2", "path3"], "value")]`."""
198199
if options is None:
199-
return ()
200+
return {}
200201

201-
def _parse_option(option: str):
202-
bad_param = typer.BadParameter(
203-
f"Invalid config override {option} (must be `keypath=value`)",
204-
param_hint="config_override",
205-
)
206-
if option.count("=") != 1:
207-
raise bad_param
202+
def _parse_option(option: str) -> tuple[tuple[str, ...], Any]:
203+
if "=" not in option:
204+
raise typer.BadParameter(
205+
f'Invalid config override "{option}" (must be `keypath=value`)',
206+
param_hint="config_override",
207+
)
208208

209-
key, value = option.split("=")
210-
if not key or not value:
211-
raise bad_param
209+
key, value = option.split("=", maxsplit=1)
210+
if not re.match(r"\w[\w|\.]*", key):
211+
raise typer.BadParameter(
212+
f'Invalid keypath "{key}" in config override "{option}"',
213+
param_hint="config_override",
214+
)
215+
216+
path = tuple(key.split("."))
217+
218+
try:
219+
value = yaml.safe_load(value)
220+
except yaml.YAMLError as e:
221+
raise typer.BadParameter(
222+
f'Invalid value for config override "{option}", could not parse value as YAML: {e}',
223+
param_hint="config_override",
224+
) from e
212225

213-
path = key.split(".")
214226
return path, value
215227

216-
return tuple(_parse_option(option) for option in options)
228+
return dict(_parse_option(option) for option in options)
217229

218230

219231
@app.command("build")

tesseract_core/sdk/engine.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ def build_tesseract(
371371
image_tag: str | None,
372372
build_dir: Path | None = None,
373373
inject_ssh: bool = False,
374-
config_override: tuple[tuple[list[str], str], ...] = (),
374+
config_override: dict[tuple[str, ...], Any] | None = None,
375375
generate_only: bool = False,
376376
) -> Image | Path:
377377
"""Build a new Tesseract from a context directory.
@@ -397,11 +397,12 @@ def build_tesseract(
397397
config = get_config(src_dir)
398398

399399
# Apply config overrides
400-
for path, value in config_override:
401-
c = config
402-
for k in path[:-1]:
403-
c = getattr(c, k)
404-
setattr(c, path[-1], value)
400+
if config_override is not None:
401+
for path, value in config_override.items():
402+
c = config
403+
for k in path[:-1]:
404+
c = getattr(c, k)
405+
setattr(c, path[-1], value)
405406

406407
image_name = config.name
407408
if image_tag:

tests/sdk_tests/test_cli.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,75 @@ def test_bad_docker_executable_env_var():
5252
)
5353
assert result.returncode == 1
5454
assert "Executable `not-a-docker` not found" in result.stderr.decode()
55+
56+
57+
@pytest.mark.parametrize(
58+
"arg_to_override",
59+
[
60+
"name",
61+
"build_config.custom_build_steps",
62+
"build_config.base_image",
63+
"build_config.package_data",
64+
],
65+
)
66+
def test_config_override(
67+
arg_to_override, cli_runner, mocker, dummy_tesseract_location, mocked_docker
68+
):
69+
mocked_build = mocker.patch("tesseract_core.sdk.engine.build_tesseract")
70+
71+
def _run_with_override(key, value):
72+
return cli_runner.invoke(
73+
cli,
74+
[
75+
"build",
76+
str(dummy_tesseract_location),
77+
"--config-override",
78+
f"{key}={value}",
79+
"--generate-only",
80+
],
81+
catch_exceptions=False,
82+
)
83+
84+
if arg_to_override == "name":
85+
argpairs = (
86+
(
87+
"my-tesseract",
88+
{("name",): "my-tesseract"},
89+
),
90+
)
91+
elif arg_to_override == "build_config.custom_build_steps":
92+
argpairs = (
93+
(
94+
"[RUN foo='bar']",
95+
{("build_config", "custom_build_steps"): ["RUN foo='bar'"]},
96+
),
97+
(
98+
'[RUN echo "hello world"]',
99+
{("build_config", "custom_build_steps"): ['RUN echo "hello world"']},
100+
),
101+
)
102+
elif arg_to_override == "build_config.base_image":
103+
argpairs = (
104+
(
105+
"ubuntu:latest",
106+
{("build_config", "base_image"): "ubuntu:latest"},
107+
),
108+
)
109+
elif arg_to_override == "build_config.package_data":
110+
argpairs = (
111+
(
112+
'["data/file.txt:/app/data/file.txt"]',
113+
{
114+
("build_config", "package_data"): [
115+
"data/file.txt:/app/data/file.txt"
116+
]
117+
},
118+
),
119+
)
120+
else:
121+
raise ValueError(f"Unknown arg_to_override: {arg_to_override}")
122+
123+
for value, expected in argpairs:
124+
result = _run_with_override(arg_to_override, value)
125+
assert result.exit_code == 0, result.stderr
126+
assert mocked_build.call_args[1]["config_override"] == expected

0 commit comments

Comments
 (0)