Skip to content

Commit b436205

Browse files
committed
fix(cli,upgrade): handle runtime lazy-command errors and uv tool detection
1 parent accfdfd commit b436205

3 files changed

Lines changed: 47 additions & 7 deletions

File tree

src/specfact_cli/cli.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -559,9 +559,10 @@ def _invoke(args: tuple[str, ...]) -> None:
559559
resolved_name = resolve_command(cmd_name)
560560
try:
561561
real_typer = CommandRegistry.get_typer(resolved_name)
562-
except ValueError as exc:
562+
click_cmd = get_command(real_typer)
563+
except (RuntimeError, ValueError) as exc:
564+
click.echo(str(exc), err=True)
563565
raise click.ClickException(str(exc)) from exc
564-
click_cmd = get_command(real_typer)
565566
# Build full prog name from root (e.g. "specfact sync") so usage shows "specfact sync bridge", not "sync sync bridge"
566567
parts: list[str] = []
567568
p = ctx.parent
@@ -626,9 +627,9 @@ def _get_real_click_group(self) -> click.Group | None:
626627
resolved_name = resolve_command(self._lazy_cmd_name)
627628
try:
628629
real_typer = CommandRegistry.get_typer(resolved_name)
629-
except ValueError:
630+
click_cmd = get_command(real_typer)
631+
except (RuntimeError, ValueError):
630632
return None
631-
click_cmd = get_command(real_typer)
632633
if isinstance(click_cmd, click.Group):
633634
return click_cmd
634635
return None
@@ -641,9 +642,9 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non
641642
resolved_name = resolve_command(self._lazy_cmd_name)
642643
try:
643644
real_typer = CommandRegistry.get_typer(resolved_name)
644-
except ValueError:
645+
click_cmd = get_command(real_typer)
646+
except (RuntimeError, ValueError):
645647
return
646-
click_cmd = get_command(real_typer)
647648
prog_name = (
648649
f"{ctx.parent.command.name} {self._lazy_cmd_name}"
649650
if ctx.parent and ctx.parent.command

src/specfact_cli/modules/upgrade/src/commands.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,17 @@ def _detect_uv_installation(executable_path: str) -> InstallationMethod | None:
9898
command="uv pip install --upgrade specfact-cli",
9999
location=str(Path(executable_path).parent.parent),
100100
)
101-
if Path(sys.executable).name in {"uv", "uv.exe"}:
101+
try:
102+
result = subprocess.run(
103+
["uv", "tool", "list"],
104+
capture_output=True,
105+
text=True,
106+
timeout=5,
107+
check=False,
108+
)
109+
except (subprocess.TimeoutExpired, FileNotFoundError):
110+
return None
111+
if "specfact-cli" in result.stdout:
102112
return InstallationMethod(method="uv", command="uv tool upgrade specfact-cli", location=None)
103113
return None
104114

tests/unit/commands/test_update.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,35 @@ def side_effect(*args, **kwargs):
9696
# Should detect pipx first (before checking pip)
9797
assert method.method == "pipx", f"Expected pipx, got {method.method}"
9898

99+
100+
101+
@patch("specfact_cli.modules.upgrade.src.commands.subprocess.run")
102+
@patch("specfact_cli.modules.upgrade.src.commands.sys.executable", "/usr/bin/python3")
103+
@patch("specfact_cli.modules.upgrade.src.commands.sys.argv", ["/usr/bin/python3", "-m", "specfact_cli"])
104+
def test_detect_uv_tool_installation(self, mock_subprocess: MagicMock) -> None:
105+
"""Test detecting uv tool installation via `uv tool list`."""
106+
107+
def side_effect(*args, **kwargs):
108+
result = MagicMock()
109+
cmd = args[0] if args else []
110+
cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
111+
if "pipx list" in cmd_str:
112+
result.returncode = 1
113+
result.stdout = ""
114+
elif "uv tool list" in cmd_str:
115+
result.returncode = 0
116+
result.stdout = "specfact-cli v0.46.10"
117+
else:
118+
result.returncode = 1
119+
result.stdout = ""
120+
return result
121+
122+
mock_subprocess.side_effect = side_effect
123+
124+
method = detect_installation_method()
125+
assert method.method == "uv"
126+
assert method.command == "uv tool upgrade specfact-cli"
127+
99128
@patch("specfact_cli.modules.upgrade.src.commands.subprocess.run")
100129
@patch("specfact_cli.modules.upgrade.src.commands.sys.executable", "/usr/bin/python3")
101130
@patch("specfact_cli.modules.upgrade.src.commands.sys.argv", ["/usr/bin/python3", "-m", "specfact_cli"])

0 commit comments

Comments
 (0)