Skip to content

Commit 0ef2852

Browse files
committed
fix: Add graceful fallback when command is non-existent
1 parent 329f052 commit 0ef2852

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

src/nanocli/config.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,23 @@ def compile_config(
254254
# Apply overrides (tree rewrite)
255255
if overrides:
256256
override_cfg = parse_overrides(overrides)
257-
cfg = OmegaConf.merge(cfg, override_cfg)
257+
try:
258+
cfg = OmegaConf.merge(cfg, override_cfg)
259+
except Exception as e:
260+
# Extract the key from OmegaConf error message
261+
error_msg = str(e)
262+
if "Key" in error_msg and "not in" in error_msg:
263+
# Parse: Key 'typer' not in 'ModelConfig'
264+
import re
265+
266+
match = re.search(r"Key '(\w+)' not in '(\w+)'", error_msg)
267+
if match:
268+
key, cls = match.groups()
269+
raise ConfigError(
270+
f"Invalid config key '{key}' in '{cls}'. Check for typos in your overrides."
271+
) from None
272+
# Re-raise with friendlier message
273+
raise ConfigError(f"Config error: {error_msg}") from None
258274

259275
# Convert to typed object if schema provided
260276
if schema is not None:

src/nanocli/core.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from rich.panel import Panel
1919

2020
from nanocli.config import (
21+
ConfigError,
2122
compile_config,
2223
get_schema_structure,
2324
load_yaml,
@@ -202,7 +203,13 @@ def _execute(self, args: list[str]) -> None:
202203

203204
# Compile config
204205
if schema:
205-
config = compile_config(base=base, overrides=overrides, schema=schema)
206+
try:
207+
config = compile_config(base=base, overrides=overrides, schema=schema)
208+
except ConfigError as e:
209+
console.print(f"[red]Error:[/red] {e}")
210+
console.print()
211+
current._show_command_help(console, part, schema)
212+
sys.exit(1)
206213
else:
207214
config = None
208215

@@ -525,7 +532,13 @@ def run(
525532
base = load_yaml(cfg_file) if cfg_file else None
526533

527534
# Compile config
528-
config = compile_config(base=base, overrides=overrides, schema=schema)
535+
try:
536+
config = compile_config(base=base, overrides=overrides, schema=schema)
537+
except ConfigError as e:
538+
console.print(f"[red]Error:[/red] {e}")
539+
console.print()
540+
_show_run_help(console, schema)
541+
sys.exit(1)
529542

530543
# Handle print
531544
if flags["print"] or flags["print_global"]:

tests/test_core.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,62 @@ def download(cfg: SimpleConfig):
662662
sys.stdout = old_stdout
663663

664664
assert "train:" in output or "data:" in output
665+
666+
667+
class TestErrorHandling:
668+
"""Tests for graceful error handling."""
669+
670+
def test_invalid_override_key_in_run(self):
671+
"""Test that invalid config keys show helpful error."""
672+
from nanocli.config import ConfigError, compile_config
673+
674+
with pytest.raises(ConfigError, match="Invalid config key"):
675+
compile_config(schema=SimpleConfig, overrides=["invalid_key=value"])
676+
677+
def test_invalid_nested_key(self):
678+
"""Test that invalid nested keys are caught."""
679+
from nanocli.config import ConfigError, compile_config
680+
681+
with pytest.raises(ConfigError, match="Invalid config key"):
682+
compile_config(schema=ParentConfig, overrides=["child.invalid=value"])
683+
684+
def test_run_with_invalid_key_exits(self):
685+
"""Test that run() exits gracefully with invalid key."""
686+
with pytest.raises(SystemExit):
687+
run(SimpleConfig, args=["invalid_key=value"])
688+
689+
def test_nanocli_command_with_invalid_key(self):
690+
"""Test NanoCLI command with invalid key shows error and help."""
691+
app = NanoCLI()
692+
693+
@app.command()
694+
def train(cfg: SimpleConfig):
695+
pass
696+
697+
with pytest.raises(SystemExit):
698+
old_stdout = sys.stdout
699+
sys.stdout = io.StringIO()
700+
try:
701+
app(["train", "invalid_key=value"])
702+
finally:
703+
sys.stdout = old_stdout
704+
705+
706+
class TestConfigErrorMessages:
707+
"""Tests for config error message formatting."""
708+
709+
def test_parse_overrides_no_equals(self):
710+
"""Test that missing = raises ConfigError."""
711+
from nanocli.config import ConfigError, parse_overrides
712+
713+
with pytest.raises(ConfigError, match="Invalid override"):
714+
parse_overrides(["no_equals_sign"])
715+
716+
def test_config_error_message_has_key(self):
717+
"""Test that error message contains the invalid key."""
718+
from nanocli.config import ConfigError, compile_config
719+
720+
try:
721+
compile_config(schema=SimpleConfig, overrides=["typo_key=value"])
722+
except ConfigError as e:
723+
assert "typo_key" in str(e)

0 commit comments

Comments
 (0)