From 2c34dc5bce6a5fbff4cc7efd3764fac5e6cd00e9 Mon Sep 17 00:00:00 2001 From: Anish Mehta Date: Wed, 9 Sep 2026 01:11:19 +0530 Subject: [PATCH 1/3] fix(cli): restore shell completion script generation `moss completions bash|zsh` always failed with "Shell completion is unavailable in this Typer installation" and exited 1, even on a healthy install. Two separate defects: 1. The import fallback tried `typer.main.get_completion_script` and then `typer._completion_shared.get_completion_script`. Neither resolves on current Typer, but `typer.completion.get_completion_script` does, with an identical `(prog_name, complete_var, shell)` signature. It was never attempted. 2. Even when the inner import succeeded, control fell through to an unconditional `print_error(...)` / `raise typer.Exit(1)`, so the fallback path could never succeed regardless of the import. Adds `typer.completion` to the chain and nests the error handling so the failure message is raised only when every import path is exhausted. The message is lifted to a module constant to avoid duplicating it. This makes the two existing tests in tests/test_completions.py pass (28 passed / 2 failed -> 30 passed). black, isort and mypy are clean; the file has one fewer flake8 E501 than before. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JJrKvKssS5wMSUEU5ePWuw --- .../src/moss_cli/commands/completions.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/moss-cli/src/moss_cli/commands/completions.py b/packages/moss-cli/src/moss_cli/commands/completions.py index d110ab01..42767209 100644 --- a/packages/moss-cli/src/moss_cli/commands/completions.py +++ b/packages/moss-cli/src/moss_cli/commands/completions.py @@ -9,6 +9,7 @@ from .. import output PROG_NAME = "moss" +_UNAVAILABLE = "Shell completion is unavailable in this Typer installation." class Shell(str, Enum): @@ -43,18 +44,15 @@ def completions_command( from typer.main import get_completion_script # type: ignore[attr-defined] except Exception: # pragma: no cover try: - from typer._completion_shared import get_completion_script # type: ignore - except Exception: # pragma: no cover - depends on Typer installation - output.print_error( - "Shell completion is unavailable in this Typer installation.", - json_mode, - ) - raise typer.Exit(1) - output.print_error( - "Shell completion is unavailable in this Typer installation.", - json_mode, - ) - raise typer.Exit(1) + from typer.completion import get_completion_script # type: ignore + except Exception: # pragma: no cover + try: + from typer._completion_shared import ( # type: ignore + get_completion_script, + ) + except Exception: # pragma: no cover + output.print_error(_UNAVAILABLE, json_mode) + raise typer.Exit(1) complete_var = "_{}_COMPLETE".format(PROG_NAME.replace("-", "_").upper()) script = get_completion_script( From 619bec9d0a255f57b724cd077a4f7f9bdb36768d Mon Sep 17 00:00:00 2001 From: Anish Mehta Date: Tue, 15 Sep 2026 01:30:15 +0530 Subject: [PATCH 2/3] fix(cli): only swallow ImportError in completion fallbacks Narrow the three fallback handlers from `except Exception` to `except ImportError` so genuine Typer initialisation failures (TypeError, RuntimeError, ...) propagate instead of being masked by the "completions unavailable" message. Addresses CodeRabbit review. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DakLsFtFd23piCzmyiDngh --- packages/moss-cli/src/moss_cli/commands/completions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/moss-cli/src/moss_cli/commands/completions.py b/packages/moss-cli/src/moss_cli/commands/completions.py index 42767209..042d7bb8 100644 --- a/packages/moss-cli/src/moss_cli/commands/completions.py +++ b/packages/moss-cli/src/moss_cli/commands/completions.py @@ -42,15 +42,15 @@ def completions_command( try: # Prefer a public API when available. from typer.main import get_completion_script # type: ignore[attr-defined] - except Exception: # pragma: no cover + except ImportError: # pragma: no cover try: from typer.completion import get_completion_script # type: ignore - except Exception: # pragma: no cover + except ImportError: # pragma: no cover try: from typer._completion_shared import ( # type: ignore get_completion_script, ) - except Exception: # pragma: no cover + except ImportError: # pragma: no cover output.print_error(_UNAVAILABLE, json_mode) raise typer.Exit(1) From 5e16d7bb3976438c21feb25ef04f35958c00d28e Mon Sep 17 00:00:00 2001 From: Anish Mehta Date: Tue, 15 Sep 2026 23:59:28 +0530 Subject: [PATCH 3/3] test(cli): cover the Typer completion-script fallback chain Move the nested import fallbacks into _resolve_get_completion_script so the lookup order and the unavailable case can be exercised directly. Add tests that fake each Typer module to verify typer.main is preferred, missing modules or symbols fall through in order, and the command exits with the unavailable error when no API is found. Co-Authored-By: Claude Fable 5.1 --- .../src/moss_cli/commands/completions.py | 39 ++++++---- packages/moss-cli/tests/test_completions.py | 73 +++++++++++++++++++ 2 files changed, 98 insertions(+), 14 deletions(-) diff --git a/packages/moss-cli/src/moss_cli/commands/completions.py b/packages/moss-cli/src/moss_cli/commands/completions.py index 042d7bb8..c4ce774a 100644 --- a/packages/moss-cli/src/moss_cli/commands/completions.py +++ b/packages/moss-cli/src/moss_cli/commands/completions.py @@ -2,7 +2,9 @@ from __future__ import annotations +import importlib from enum import Enum +from typing import Callable, Optional import typer @@ -10,6 +12,25 @@ PROG_NAME = "moss" _UNAVAILABLE = "Shell completion is unavailable in this Typer installation." +# Where `get_completion_script` has lived across Typer releases, most public first. +_COMPLETION_SCRIPT_MODULES = ( + "typer.main", + "typer.completion", + "typer._completion_shared", +) + + +def _resolve_get_completion_script() -> Optional[Callable[..., str]]: + """Return Typer's ``get_completion_script`` from the first module that exposes it.""" + for module_name in _COMPLETION_SCRIPT_MODULES: + try: + module = importlib.import_module(module_name) + except ImportError: + continue + get_completion_script = getattr(module, "get_completion_script", None) + if get_completion_script is not None: + return get_completion_script + return None class Shell(str, Enum): @@ -39,20 +60,10 @@ def completions_command( """ json_mode = ctx.obj.get("json_output", False) if ctx.obj else False - try: - # Prefer a public API when available. - from typer.main import get_completion_script # type: ignore[attr-defined] - except ImportError: # pragma: no cover - try: - from typer.completion import get_completion_script # type: ignore - except ImportError: # pragma: no cover - try: - from typer._completion_shared import ( # type: ignore - get_completion_script, - ) - except ImportError: # pragma: no cover - output.print_error(_UNAVAILABLE, json_mode) - raise typer.Exit(1) + get_completion_script = _resolve_get_completion_script() + if get_completion_script is None: + output.print_error(_UNAVAILABLE, json_mode) + raise typer.Exit(1) complete_var = "_{}_COMPLETE".format(PROG_NAME.replace("-", "_").upper()) script = get_completion_script( diff --git a/packages/moss-cli/tests/test_completions.py b/packages/moss-cli/tests/test_completions.py index b7b2c185..877e1c82 100644 --- a/packages/moss-cli/tests/test_completions.py +++ b/packages/moss-cli/tests/test_completions.py @@ -1,6 +1,10 @@ +import sys +import types + from typer.testing import CliRunner from moss_cli import completion +from moss_cli.commands import completions from moss_cli.main import app runner = CliRunner() @@ -32,6 +36,75 @@ def test_completions_rejects_unsupported_shell(): assert result.exit_code != 0 +def _fake_module(name, has_script): + module = types.ModuleType(name) + if has_script: + module.get_completion_script = lambda **kwargs: f"script from {name}" + return module + + +def _install_typer_modules(monkeypatch, availability): + # None in sys.modules makes importlib raise ImportError, i.e. "module missing". + for name, has_script in availability.items(): + module = None if has_script is None else _fake_module(name, has_script) + monkeypatch.setitem(sys.modules, name, module) + + +def test_resolve_get_completion_script_prefers_typer_main(monkeypatch): + _install_typer_modules( + monkeypatch, + { + "typer.main": True, + "typer.completion": True, + "typer._completion_shared": True, + }, + ) + + func = completions._resolve_get_completion_script() + + assert func is not None + assert func() == "script from typer.main" + + +def test_resolve_get_completion_script_falls_back_in_order(monkeypatch): + # typer.main exists but lacks the symbol, typer.completion is missing entirely. + _install_typer_modules( + monkeypatch, + { + "typer.main": False, + "typer.completion": None, + "typer._completion_shared": True, + }, + ) + + func = completions._resolve_get_completion_script() + + assert func is not None + assert func() == "script from typer._completion_shared" + + +def test_resolve_get_completion_script_returns_none_when_unavailable(monkeypatch): + _install_typer_modules( + monkeypatch, + { + "typer.main": False, + "typer.completion": False, + "typer._completion_shared": None, + }, + ) + + assert completions._resolve_get_completion_script() is None + + +def test_completions_reports_error_when_typer_has_no_completion_api(monkeypatch): + monkeypatch.setattr(completions, "_resolve_get_completion_script", lambda: None) + + result = runner.invoke(app, ["completions", "bash"]) + + assert result.exit_code == 1 + assert "Shell completion is unavailable" in result.output + + def test_complete_index_name_lists_indexes(monkeypatch): class FakeIndex: def __init__(self, name):