-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
command.py
276 lines (236 loc) · 11 KB
/
command.py
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""
Base classes that implement the CLI framework
"""
import importlib
import logging
from collections import OrderedDict
import click
from samcli.cli.formatters import RootCommandHelpTextFormatter
from samcli.cli.root.command_list import SAM_CLI_COMMANDS
from samcli.cli.row_modifiers import HighlightNewRowNameModifier, RowDefinition, ShowcaseRowModifier
logger = logging.getLogger(__name__)
# Commands that are bundled with the CLI by default in app life-cycle order.
_SAM_CLI_COMMAND_PACKAGES = [
"samcli.commands.init",
"samcli.commands.validate.validate",
"samcli.commands.build",
"samcli.commands.local.local",
"samcli.commands.package",
"samcli.commands.deploy",
"samcli.commands.delete",
"samcli.commands.logs",
"samcli.commands.publish",
"samcli.commands.traces",
"samcli.commands.sync",
"samcli.commands.pipeline.pipeline",
"samcli.commands.list.list",
"samcli.commands.docs",
"samcli.commands.remote.remote",
# We intentionally do not expose the `bootstrap` command for now. We might open it up later
# "samcli.commands.bootstrap",
]
class BaseCommand(click.MultiCommand):
"""
Dynamically loads commands. It takes a list of names of Python packages representing the commands, loads
these packages, and initializes them as Click commands. If a command "hello" is available in a Python package
"foo.bar.hello", then this package name is passed to this class to load the command. This allows commands
to be written as standalone packages that are dynamically initialized by the CLI.
Each command, along with any subcommands, is implemented using Click annotations. When the command is loaded
dynamically, this class expects the Click object to be exposed through an attribute called ``cli``. If the
attribute is not present, or is not a Click object, then an exception will be raised.
For example: if "foo.bar.hello" is the package where "hello" command is implemented, then
"/foo/bar/hello/__init__.py" file is expected to contain a Click object called ``cli``.
The command package is dynamically loaded using Python's standard ``importlib`` library. Therefore package names
can be specified using the standard Python's dot notation such as "foo.bar.hello".
By convention, the name of last module in the package's name is the command's name. ie. A package of "foo.bar.baz"
will produce a command name "baz".
"""
class CustomFormatterContext(click.Context):
formatter_class = RootCommandHelpTextFormatter
context_class = CustomFormatterContext
def __init__(self, *args, cmd_packages=None, **kwargs):
"""
Initializes the class, optionally with a list of available commands
:param cmd_packages: List of Python packages names of CLI commands
:param args: Other Arguments passed to super class
:param kwargs: Other Arguments passed to super class
"""
# alias -h to --help for all commands
kwargs["context_settings"] = dict(help_option_names=["-h", "--help"])
super().__init__(*args, **kwargs)
if not cmd_packages:
cmd_packages = _SAM_CLI_COMMAND_PACKAGES
self._commands = {}
self._commands = BaseCommand._set_commands(cmd_packages)
@staticmethod
def _set_commands(package_names):
"""
Extract the command name from package name. Last part of the module path is the command
ie. if path is foo.bar.baz, then "baz" is the command name.
:param package_names: List of package names
:return: Dictionary with command name as key and the package name as value.
"""
commands = OrderedDict()
for pkg_name in package_names:
cmd_name = pkg_name.split(".")[-1]
commands[cmd_name] = pkg_name
return commands
def format_options(self, ctx: click.Context, formatter: RootCommandHelpTextFormatter): # type: ignore
# NOTE(sriram-mv): `ignore` is put in place here for mypy even though it is the correct behavior,
# as the `formatter_class` can be set in subclass of Command. If ignore is not set,
# mypy raises argument needs to be HelpFormatter as super class defines it.
# NOTE(sriram-mv): Re-order options so that they come after the commands.
self.format_commands(ctx, formatter)
opts = [RowDefinition(name="", text="\n")]
for param in self.get_params(ctx):
row = param.get_help_record(ctx)
if row is not None:
term, help_text = row
opts.append(RowDefinition(name=term, text=help_text))
if opts:
with formatter.indented_section(name="Options", extra_indents=1):
formatter.write_rd(opts)
with formatter.indented_section(name="Examples", extra_indents=1):
formatter.write_rd(
[
RowDefinition(
name="",
text="\n",
),
RowDefinition(
name="Get Started:",
text=click.style(f"${ctx.command_path} init"),
extra_row_modifiers=[ShowcaseRowModifier()],
),
],
)
def format_commands(self, ctx: click.Context, formatter: RootCommandHelpTextFormatter): # type: ignore
# NOTE(sriram-mv): `ignore` is put in place here for mypy even though it is the correct behavior,
# as the `formatter_class` can be set in subclass of Command. If ignore is not set,
# mypy raises argument needs to be HelpFormatter as super class defines it.
with formatter.section("Commands"):
with formatter.section("Learn"):
formatter.write_rd(
[
RowDefinition(
name="docs",
text=SAM_CLI_COMMANDS.get("docs", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
)
]
)
with formatter.section("Create an App"):
formatter.write_rd(
[
RowDefinition(name="init", text=SAM_CLI_COMMANDS.get("init", "")),
],
)
with formatter.section("Develop your App"):
formatter.write_rd(
[
RowDefinition(
name="build",
text=SAM_CLI_COMMANDS.get("build", ""),
),
RowDefinition(
name="local",
text=SAM_CLI_COMMANDS.get("local", ""),
),
RowDefinition(
name="validate",
text=SAM_CLI_COMMANDS.get("validate", ""),
),
RowDefinition(
name="sync",
text=SAM_CLI_COMMANDS.get("sync", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
),
RowDefinition(
name="remote",
text=SAM_CLI_COMMANDS.get("remote", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
),
],
)
with formatter.section("Deploy your App"):
formatter.write_rd(
[
RowDefinition(
name="package",
text=SAM_CLI_COMMANDS.get("package", ""),
),
RowDefinition(
name="deploy",
text=SAM_CLI_COMMANDS.get("deploy", ""),
),
]
)
with formatter.section("Monitor your App"):
formatter.write_rd(
[
RowDefinition(
name="logs",
text=SAM_CLI_COMMANDS.get("logs", ""),
),
RowDefinition(
name="traces",
text=SAM_CLI_COMMANDS.get("traces", ""),
),
],
)
with formatter.section("And More"):
formatter.write_rd(
[
RowDefinition(
name="list",
text=SAM_CLI_COMMANDS.get("list", ""),
extra_row_modifiers=[HighlightNewRowNameModifier()],
),
RowDefinition(
name="delete",
text=SAM_CLI_COMMANDS.get("delete", ""),
),
RowDefinition(
name="pipeline",
text=SAM_CLI_COMMANDS.get("pipeline", ""),
),
RowDefinition(
name="publish",
text=SAM_CLI_COMMANDS.get("publish", ""),
),
],
)
def list_commands(self, ctx):
"""
Overrides a method from Click that returns a list of commands available in the CLI.
:param ctx: Click context
:return: List of commands available in the CLI
"""
return list(self._commands.keys())
def get_command(self, ctx, cmd_name):
"""
Overrides method from ``click.MultiCommand`` that returns Click CLI object for given command name, if found.
:param ctx: Click context
:param cmd_name: Top-level command name
:return: Click object representing the command
"""
if cmd_name not in self._commands:
logger.error("Command %s not available", cmd_name)
return None
pkg_name = self._commands[cmd_name]
mod = None
try:
if ctx.obj:
# NOTE(sriram-mv): Only attempt to import if a relevant `aws sam cli` context has been set.
# `aws sam cli` context is only set after the `samcli.cli.main:cli` has been executed.
mod = importlib.import_module(pkg_name)
except ImportError:
logger.exception("Command '%s' is not configured correctly. Unable to import '%s'", cmd_name, pkg_name)
return None
if mod is not None:
if not hasattr(mod, "cli"):
logger.error(
"Command %s is not configured correctly. It must expose an function called 'cli'", cmd_name
)
return None
return mod.cli if mod else click.Command(name=cmd_name, short_help=SAM_CLI_COMMANDS.get(cmd_name, ""))