fluksio run takes inputs after -- as name=value

So an input named like a flag of run, timeout or sync, can still be set.
Flags before -- keep naming inputs as they did.
This commit is contained in:
2026-10-15 10:56:46 +02:00
parent e597c34823
commit b9078dd19f
4 changed files with 50 additions and 5 deletions
+6 -3
View File
@@ -807,9 +807,12 @@ def main(argv: list[str] | None = None) -> int:
parser = _parser()
if argv and argv[0] == "run":
# A flow's inputs are its own, so `--lr 0.05` cannot be declared here:
# whatever this parser does not know is typed against the flow.
args, rest = parser.parse_known_args(argv)
return int(args.func(args, rest))
# whatever this parser does not know is typed against the flow. What
# follows `--` is the flow's whatever its name, so an input called
# `timeout` is set there rather than taken as `run --timeout`.
cut = argv.index("--") if "--" in argv else len(argv)
args, rest = parser.parse_known_args(argv[:cut])
return int(args.func(args, rest + argv[cut:]))
args = parser.parse_args(argv)
result: int = args.func(args)
return result
+12 -2
View File
@@ -453,15 +453,20 @@ def _ask_params(definition: dict[str, Any]) -> dict[str, Any]:
def _params(definition: dict[str, Any], rest: list[str]) -> dict[str, Any]:
"""Turn `--lr 0.05` into a typed parameter, using the flow's own inputs."""
"""Turn `--lr 0.05` into a typed parameter, using the flow's own inputs.
After `--` every token is `name=value`, which is how an input that shares
its name with a flag of `run` is set.
"""
types = _input_types(definition)
cut = rest.index("--") if "--" in rest else len(rest)
# Collected as written and typed afterwards, so a name this flow does not
# have is refused by name rather than by whatever its value failed to parse
# as: `--param lr=0.002` is a sweep's spelling, and said so it reads as an
# input called `param` holding unparseable json.
raw: dict[str, str | bool] = {}
pending: str | None = None
for token in rest:
for token in rest[:cut]:
if token.startswith("--"):
if pending is not None:
# A flag with no value is a flag: `--resume` means true.
@@ -480,6 +485,11 @@ def _params(definition: dict[str, Any], rest: list[str]) -> dict[str, Any]:
pending = None
if pending is not None:
raw[pending] = True
for token in rest[cut + 1 :]:
name, sep, value = token.partition("=")
if not sep or not name:
raise SyncError(f"after `--` an input is name=value, not '{token}'")
raw[name.replace("-", "_")] = value
unknown = sorted(set(raw) - set(types))
if unknown:
hint = ""
+25
View File
@@ -90,6 +90,31 @@ def test_run_arguments_are_typed_by_the_flow_they_are_for() -> None:
_params(definition, ["--nonesuch", "1"])
def test_inputs_after_a_double_dash_may_share_a_name_with_a_run_flag(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""`run --timeout` is the wait; `-- timeout=5` is the flow's input."""
from fluksio import cli
from fluksio.sdk import cli as sdk_cli
seen = []
monkeypatch.setattr(
sdk_cli, "cmd_run", lambda args, rest: seen.append((args.timeout, rest)) or 0
)
cli.main(["run", "train", "--timeout", "3", "--lr", "0.1", "--", "timeout=5"])
assert seen == [(3.0, ["--lr", "0.1", "--", "timeout=5"])]
definition = {
"inputs": [
{"spec": {"name": "lr", "dtype": "float"}},
{"spec": {"name": "timeout", "dtype": "int"}},
]
}
assert sdk_cli._params(definition, seen[0][1]) == {"lr": 0.1, "timeout": 5}
with pytest.raises(sdk_cli.SyncError, match="name=value"):
sdk_cli._params(definition, ["--", "timeout"])
def test_a_sweeps_param_spelling_is_refused_by_name() -> None:
"""`run --param lr=0.002` is a name this flow has not got, and says so."""
import pytest
+7
View File
@@ -190,6 +190,13 @@ Syncs local code, then submits a run. Flags such as `--lr` name the flow's
declared inputs and are converted to their declared types. Unknown inputs are
rejected. Use `--param name=a,b` with `sweep` for multiple values.
Everything after `--` is an input written as `name=value`. Use it for an input
that shares its name with a flag of `run`, such as `timeout`:
```sh
fluksio run train --wait --timeout 600 -- timeout=30 lr=0.05
```
After the first sync, `run` imports the module that declared the flow. If no
declaration is recorded, it searches the working directory and skips paths
covered by `.gitignore`. Import errors in that fallback search produce