diff --git a/backend/fluksio/cli.py b/backend/fluksio/cli.py index b187f48..6656ae5 100644 --- a/backend/fluksio/cli.py +++ b/backend/fluksio/cli.py @@ -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 diff --git a/backend/fluksio/sdk/cli.py b/backend/fluksio/sdk/cli.py index 4c651a1..23fc960 100644 --- a/backend/fluksio/sdk/cli.py +++ b/backend/fluksio/sdk/cli.py @@ -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 = "" diff --git a/backend/tests/test_cli.py b/backend/tests/test_cli.py index 355d218..c6826c8 100644 --- a/backend/tests/test_cli.py +++ b/backend/tests/test_cli.py @@ -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 diff --git a/docs/code/cli.md b/docs/code/cli.md index cbf3b1b..5758313 100644 --- a/docs/code/cli.md +++ b/docs/code/cli.md @@ -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