-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path__init__.py
More file actions
151 lines (122 loc) · 5.65 KB
/
Copy path__init__.py
File metadata and controls
151 lines (122 loc) · 5.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Unified CLI entry point for model-gear (binary: ``model``).
The model-ops verbs (``switch``, ``serve``/``stop``, ``status``, ``assess``,
``benchmark``, ``init``) are the heart of the tool; the agent-first verbs
(``whoami``, ``learn``, ``explain``, ``overview``, ``doctor``, ``cli``) keep the
sibling rubric satisfied. Each verb module exposes ``register(sub)`` following
the same pattern.
Error propagation contract
--------------------------
Every handler raises :class:`model_gear.cli._errors.ModelGearError` on failure;
``main()`` catches it via :func:`_dispatch` and routes through
:mod:`model_gear.cli._output`. Unknown exceptions are wrapped into a
``ModelGearError`` so no Python traceback leaks to stderr.
Argparse errors (unknown verb, missing arg) also route through the structured
format — ``_ModelGearArgumentParser`` overrides ``.error()`` and the subparsers
are built with ``parser_class=_ModelGearArgumentParser``. Whether errors render
as text or JSON depends on whether ``--json`` appears in the raw argv
(:func:`main` sets ``_json_hint`` before ``parse_args``).
"""
from __future__ import annotations
import argparse
import sys
from model_gear import __version__
from model_gear.cli._errors import EXIT_USER_ERROR, ModelGearError
from model_gear.cli._output import emit_error
class _ModelGearArgumentParser(argparse.ArgumentParser):
"""ArgumentParser that routes errors through :func:`emit_error`.
Argparse's default error handler writes ``prog: error: <msg>`` to stderr
and exits 2, skipping the ModelGearError plumbing (and the ``hint:`` line
agents look for). This subclass emits the structured format and exits with
:attr:`EXIT_USER_ERROR`.
JSON mode: parse-time errors happen before ``args.json`` exists, so we rely
on a class-level ``_json_hint`` that :func:`main` pre-populates by scanning
raw argv for ``--json``. Shared across all subparser instances.
"""
_json_hint: bool = False
def error(self, message: str) -> None: # type: ignore[override]
err = ModelGearError(
code=EXIT_USER_ERROR,
message=message,
remediation=f"run '{self.prog} --help' to see valid arguments",
)
emit_error(err, json_mode=type(self)._json_hint)
raise SystemExit(err.code)
def _argv_has_json(argv: list[str] | None) -> bool:
tokens = argv if argv is not None else sys.argv[1:]
return any(t == "--json" or t.startswith("--json=") for t in tokens)
def _build_parser() -> argparse.ArgumentParser:
from model_gear.cli._commands import assess as _assess_cmd
from model_gear.cli._commands import benchmark as _benchmark_cmd
from model_gear.cli._commands import cli as _cli_group
from model_gear.cli._commands import doctor as _doctor_cmd
from model_gear.cli._commands import explain as _explain_cmd
from model_gear.cli._commands import fleet as _fleet_cmd
from model_gear.cli._commands import init as _init_cmd
from model_gear.cli._commands import learn as _learn_cmd
from model_gear.cli._commands import overview as _overview_cmd
from model_gear.cli._commands import serve as _serve_cmd
from model_gear.cli._commands import status as _status_cmd
from model_gear.cli._commands import stop as _stop_cmd
from model_gear.cli._commands import switch as _switch_cmd
from model_gear.cli._commands import whoami as _whoami_cmd
parser = _ModelGearArgumentParser(
prog="model",
description="model-gear — run, assess, and switch the local vLLM model",
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {__version__}",
)
# parser_class propagates to every subparser so their .error() routes
# through _ModelGearArgumentParser too.
sub = parser.add_subparsers(dest="command", parser_class=_ModelGearArgumentParser)
# Model-ops verbs (the heart of the tool).
_switch_cmd.register(sub)
_serve_cmd.register(sub)
_stop_cmd.register(sub)
_status_cmd.register(sub)
_assess_cmd.register(sub)
_benchmark_cmd.register(sub)
_init_cmd.register(sub)
_fleet_cmd.register(sub)
# Agent-first / introspection verbs (sibling rubric).
_whoami_cmd.register(sub)
_learn_cmd.register(sub)
_explain_cmd.register(sub)
_overview_cmd.register(sub)
_doctor_cmd.register(sub)
_cli_group.register(sub)
return parser
def _dispatch(args: argparse.Namespace) -> int:
"""Invoke the registered handler and translate exceptions to exit codes.
A handler may return ``None`` (success, exit 0) or an ``int`` exit code.
Failures MUST raise :class:`ModelGearError`; any other exception is wrapped
into one so no Python traceback leaks.
"""
json_mode = bool(getattr(args, "json", False))
try:
rc = args.func(args)
except ModelGearError as err:
emit_error(err, json_mode=json_mode)
return err.code
except Exception as err: # noqa: BLE001 - last-resort; wrap and route cleanly
wrapped = ModelGearError(
code=EXIT_USER_ERROR,
message=f"unexpected: {err.__class__.__name__}: {err}",
remediation="file a bug at https://github.com/agentculture/model-gear/issues",
)
emit_error(wrapped, json_mode=json_mode)
return wrapped.code
return rc if rc is not None else 0
def main(argv: list[str] | None = None) -> int:
# Pre-parse peek so argparse-level errors honour --json.
_ModelGearArgumentParser._json_hint = _argv_has_json(argv)
parser = _build_parser()
args = parser.parse_args(argv)
if args.command is None:
parser.print_help()
return 0
return _dispatch(args)
if __name__ == "__main__":
sys.exit(main())