-
Notifications
You must be signed in to change notification settings - Fork 44
Expand file tree
/
Copy pathcommand.py
More file actions
180 lines (141 loc) · 5.61 KB
/
Copy pathcommand.py
File metadata and controls
180 lines (141 loc) · 5.61 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""CLI - Group/Command classes."""
import importlib
from collections import OrderedDict
import click.exceptions
from click_didyoumean import DYMGroup
def _is_json_output_requested(exception):
"""Determine if JSON output was requested, checking context and argv."""
# Check context if available
ctx = getattr(exception, "ctx", None)
if ctx and ctx.params:
fmt = ctx.params.get("output")
if fmt in ("json", "pretty_json"):
return True
# Fallback: check sys.argv for output format flags
import sys
argv = sys.argv
if "--output-format=json" in argv or "--output-format=pretty_json" in argv:
return True
for idx, arg in enumerate(argv):
if (
arg in ("-F", "--output-format")
and idx + 1 < len(argv)
and argv[idx + 1] in ("json", "pretty_json")
):
return True
return False
def _format_click_exception_as_json(exception):
"""Format a ClickException as a JSON error dict."""
return {
"detail": exception.format_message(),
"meta": {
"code": exception.exit_code,
"description": "Usage Error",
},
"help": {
"context": "Invalid usage",
"hint": "Check your command arguments/flags.",
},
}
class AliasGroup(DYMGroup):
"""A command group with DYM, alias and lazy-subcommand support.
``lazy_commands`` maps a command name to the module that registers it.
The module is imported on first use, which keeps startup fast because
an invocation imports only the module of the invoked command.
``lazy_aliases`` supplies the alias map for those commands up front.
"""
def __init__(self, *args, **kwargs):
lazy_commands = kwargs.pop("lazy_commands", None)
lazy_aliases = kwargs.pop("lazy_aliases", None)
super().__init__(*args, **kwargs)
self.lazy_commands = dict(lazy_commands or {})
self.aliases = OrderedDict()
self.inverse = {}
for name, aliases in (lazy_aliases or {}).items():
self.aliases[name] = aliases
for alias in aliases:
self.inverse[alias] = name
def resolve_command(self, ctx, args):
try:
return super().resolve_command(ctx, args)
except click.exceptions.UsageError:
# Before DYM kicks in, check to see if the command prefix matches
# exactly one command, then use that instead.
if args:
cmd_name = args[0]
cmds = self.list_commands(ctx)
matched = [cmd for cmd in cmds if cmd.startswith(cmd_name)]
if len(matched) == 1 and len(cmd_name) > 1:
args[0] = matched[0]
return super().resolve_command(ctx, args)
raise
def list_commands(self, ctx):
commands = sorted(set(super().list_commands(ctx)) | set(self.lazy_commands))
if getattr(ctx, "showing_help", False):
for k, v in enumerate(commands):
try:
commands[k] = f"{v}|{'|'.join(self.aliases[v])}"
except KeyError:
pass
return commands
for k in self.inverse:
commands.append(k)
return commands
def get_command(self, ctx, cmd_name):
if getattr(ctx, "showing_help", False) and "|" in cmd_name:
cmd_name = cmd_name.split("|")[0]
try:
cmd_name = self.inverse[cmd_name]
except KeyError:
pass
cmd = super().get_command(ctx, cmd_name)
if cmd is None and cmd_name in self.lazy_commands:
importlib.import_module(self.lazy_commands[cmd_name])
cmd = super().get_command(ctx, cmd_name)
return cmd
def command(self, *args, **kwargs):
def decorator(f):
# pylint: disable=missing-docstring
aliases = kwargs.pop("aliases", [])
cmd = super(AliasGroup, self).command(*args, **kwargs)(f)
if aliases:
self.aliases[cmd.name] = aliases
for alias in aliases:
self.inverse[alias] = cmd.name
return cmd
return decorator
def group(self, *args, **kwargs):
def decorator(f):
# pylint: disable=missing-docstring
aliases = kwargs.pop("aliases", [])
cmd = super(AliasGroup, self).group(*args, **kwargs)(f)
if aliases:
self.aliases[cmd.name] = aliases
for alias in aliases:
self.inverse[alias] = cmd.name
return cmd
return decorator
def format_commands(self, ctx, formatter):
ctx.showing_help = True
return super().format_commands(ctx, formatter)
def main(self, *args, **kwargs):
"""Override main to intercept exceptions and format as JSON if requested."""
import sys
original_standalone_mode = kwargs.get("standalone_mode", True)
kwargs["standalone_mode"] = False
try:
return super().main(*args, **kwargs)
except click.exceptions.Abort:
if not original_standalone_mode:
raise
click.echo("Aborted!", err=True)
sys.exit(1)
except click.exceptions.ClickException as e:
if _is_json_output_requested(e):
import json
click.echo(json.dumps(_format_click_exception_as_json(e), indent=4))
sys.exit(e.exit_code)
if not original_standalone_mode:
raise
e.show()
sys.exit(e.exit_code)