Skip to content

Commit 6d54aed

Browse files
authored
feat: implement lazy loading for CLI groups (aws#8472)
* feat: implement lazy loading for CLI groups * chore: consolidate schema generation
1 parent a329d0e commit 6d54aed

14 files changed

Lines changed: 434 additions & 107 deletions

File tree

samcli/cli/lazy_group.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
LazyGroup implementation for Click CLI performance optimization
3+
"""
4+
5+
import importlib
6+
7+
import click
8+
from click import ClickException
9+
10+
11+
class LazyGroup(click.Group):
12+
def __init__(self, *args, lazy_subcommands=None, **kwargs):
13+
super().__init__(*args, **kwargs)
14+
# lazy_subcommands is a map of the form:
15+
# {command-name} -> {module-name}.{command-object-name}
16+
self.lazy_subcommands = lazy_subcommands or {}
17+
18+
def list_commands(self, ctx):
19+
base = super().list_commands(ctx)
20+
lazy = sorted(self.lazy_subcommands.keys())
21+
return base + lazy
22+
23+
def get_command(self, ctx, cmd_name):
24+
if cmd_name in self.lazy_subcommands:
25+
return self._lazy_load(cmd_name)
26+
return super().get_command(ctx, cmd_name)
27+
28+
def _lazy_load(self, cmd_name):
29+
# lazily loading a command, first get the module name and attribute name
30+
import_path = self.lazy_subcommands[cmd_name]
31+
modname, cmd_object_name = import_path.rsplit(".", 1)
32+
# do the import
33+
try:
34+
mod = importlib.import_module(modname)
35+
except ImportError as e:
36+
raise ClickException(f"Failed to load command '{cmd_name}': {str(e)}")
37+
# get the Command object from that module
38+
try:
39+
cmd_object = getattr(mod, cmd_object_name)
40+
except AttributeError:
41+
raise ClickException(f"Command '{cmd_name}' not found in module '{modname}'")
42+
# check the result to make debugging easier
43+
if not isinstance(cmd_object, click.Command):
44+
raise ClickException(f"Lazy loading of {import_path} failed by returning a non-command object")
45+
return cmd_object

samcli/commands/list/list.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@
44

55
import click
66

7-
from samcli.commands.list.endpoints.command import cli as testable_resources_cli
8-
from samcli.commands.list.resources.command import cli as resources_cli
9-
from samcli.commands.list.stack_outputs.command import cli as stack_outputs_cli
7+
from samcli.cli.lazy_group import LazyGroup
108

119

12-
@click.group()
10+
@click.group(
11+
cls=LazyGroup,
12+
lazy_subcommands={
13+
"endpoints": "samcli.commands.list.endpoints.command.cli",
14+
"resources": "samcli.commands.list.resources.command.cli",
15+
"stack-outputs": "samcli.commands.list.stack_outputs.command.cli",
16+
},
17+
)
1318
def cli():
1419
"""
1520
Get local and deployed state of serverless application.
1621
"""
17-
18-
19-
# Add individual commands under this group
20-
cli.add_command(resources_cli)
21-
cli.add_command(stack_outputs_cli)
22-
cli.add_command(testable_resources_cli)

samcli/commands/local/local.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,19 @@
55

66
import click
77

8-
from .generate_event.cli import cli as generate_event_cli
9-
from .invoke.cli import cli as invoke_cli
10-
from .start_api.cli import cli as start_api_cli
11-
from .start_lambda.cli import cli as start_lambda_cli
8+
from samcli.cli.lazy_group import LazyGroup
129

1310

14-
@click.group()
11+
@click.group(
12+
cls=LazyGroup,
13+
lazy_subcommands={
14+
"invoke": "samcli.commands.local.invoke.cli.cli",
15+
"start-api": "samcli.commands.local.start_api.cli.cli",
16+
"start-lambda": "samcli.commands.local.start_lambda.cli.cli",
17+
"generate-event": "samcli.commands.local.generate_event.cli.cli",
18+
},
19+
)
1520
def cli():
1621
"""
1722
Run your Serverless application locally for quick development & testing
1823
"""
19-
20-
21-
# Add individual commands under this group
22-
cli.add_command(invoke_cli)
23-
cli.add_command(start_api_cli)
24-
cli.add_command(generate_event_cli)
25-
cli.add_command(start_lambda_cli)

samcli/commands/pipeline/pipeline.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55

66
import click
77

8-
from .bootstrap.cli import cli as bootstrap_cli
9-
from .init.cli import cli as init_cli
8+
from samcli.cli.lazy_group import LazyGroup
109

1110

12-
@click.group()
11+
@click.group(
12+
cls=LazyGroup,
13+
lazy_subcommands={
14+
"bootstrap": "samcli.commands.pipeline.bootstrap.cli.cli",
15+
"init": "samcli.commands.pipeline.init.cli.cli",
16+
},
17+
)
1318
def cli() -> None:
1419
"""
1520
Manage the continuous delivery of the application
1621
"""
17-
18-
19-
# Add individual commands under this group
20-
cli.add_command(bootstrap_cli)
21-
cli.add_command(init_cli)

samcli/commands/remote/remote.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55

66
import click
77

8-
from samcli.commands.remote.invoke.cli import cli as invoke_cli
9-
from samcli.commands.remote.test_event.test_event import cli as event_cli
8+
from samcli.cli.lazy_group import LazyGroup
109

1110

12-
@click.group()
11+
@click.group(
12+
cls=LazyGroup,
13+
lazy_subcommands={
14+
"invoke": "samcli.commands.remote.invoke.cli.cli",
15+
"test-event": "samcli.commands.remote.test_event.test_event.cli",
16+
},
17+
)
1318
def cli():
1419
"""
1520
Interact with your Serverless application in the cloud for quick development & testing
1621
"""
17-
18-
19-
# Add individual commands under this group
20-
cli.add_command(invoke_cli)
21-
cli.add_command(event_cli)

samcli/commands/remote/test_event/test_event.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22

33
import click
44

5-
from samcli.commands.remote.test_event.delete.cli import cli as delete_cli
6-
from samcli.commands.remote.test_event.get.cli import cli as get_cli
7-
from samcli.commands.remote.test_event.list.cli import cli as list_cli
8-
from samcli.commands.remote.test_event.put.cli import cli as put_cli
5+
from samcli.cli.lazy_group import LazyGroup
96

107

11-
@click.group("test-event")
8+
@click.group(
9+
"test-event",
10+
cls=LazyGroup,
11+
lazy_subcommands={
12+
"delete": "samcli.commands.remote.test_event.delete.cli.cli",
13+
"get": "samcli.commands.remote.test_event.get.cli.cli",
14+
"put": "samcli.commands.remote.test_event.put.cli.cli",
15+
"list": "samcli.commands.remote.test_event.list.cli.cli",
16+
},
17+
)
1218
def cli():
1319
"""
1420
Manage remote test events
1521
"""
16-
17-
18-
cli.add_command(delete_cli)
19-
cli.add_command(get_cli)
20-
cli.add_command(put_cli)
21-
cli.add_command(list_cli)

schema/make_schema.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -186,18 +186,23 @@ def retrieve_command_structure(package_name: str) -> List[SamCliCommandSchema]:
186186
subcommands within the package.
187187
"""
188188
module = importlib.import_module(package_name)
189+
190+
def create_command_schema(module, subcommand):
191+
cmd_name = SamConfig.to_key([module.__name__.split(".")[-1], str(subcommand.name)])
192+
return SamCliCommandSchema(
193+
cmd_name,
194+
clean_text(subcommand.help or subcommand.short_help or ""),
195+
get_params_from_command(subcommand),
196+
)
197+
189198
command = []
190199

191200
if isinstance(module.cli, click.core.Group): # command has subcommands (e.g. local invoke)
192-
for subcommand in module.cli.commands.values():
193-
cmd_name = SamConfig.to_key([module.__name__.split(".")[-1], str(subcommand.name)])
194-
command.append(
195-
SamCliCommandSchema(
196-
cmd_name,
197-
clean_text(subcommand.help or subcommand.short_help or ""),
198-
get_params_from_command(subcommand),
199-
)
200-
)
201+
ctx = click.Context(module.cli)
202+
for subcommand_name in module.cli.list_commands(ctx):
203+
subcommand = module.cli.get_command(ctx, subcommand_name)
204+
if subcommand:
205+
command.append(create_command_schema(module, subcommand))
201206
else:
202207
cmd_name = SamConfig.to_key([module.__name__.split(".")[-1]])
203208
command.append(

schema/samcli.json

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,21 @@
408408
"parameters"
409409
]
410410
},
411+
"local_generate_event": {
412+
"title": "Local Generate Event command",
413+
"description": "Generate events for Lambda functions.",
414+
"properties": {
415+
"parameters": {
416+
"title": "Parameters for the local generate event command",
417+
"description": "Available parameters for the local generate event command:\n* ",
418+
"type": "object",
419+
"properties": {}
420+
}
421+
},
422+
"required": [
423+
"parameters"
424+
]
425+
},
411426
"local_invoke": {
412427
"title": "Local Invoke command",
413428
"description": "Invoke AWS serverless functions locally.",
@@ -835,21 +850,6 @@
835850
"parameters"
836851
]
837852
},
838-
"local_generate_event": {
839-
"title": "Local Generate Event command",
840-
"description": "Generate events for Lambda functions.",
841-
"properties": {
842-
"parameters": {
843-
"title": "Parameters for the local generate event command",
844-
"description": "Available parameters for the local generate event command:\n* ",
845-
"type": "object",
846-
"properties": {}
847-
}
848-
},
849-
"required": [
850-
"parameters"
851-
]
852-
},
853853
"local_start_lambda": {
854854
"title": "Local Start Lambda command",
855855
"description": "Emulate AWS serverless functions locally.",
@@ -2040,13 +2040,13 @@
20402040
"parameters"
20412041
]
20422042
},
2043-
"list_resources": {
2044-
"title": "List Resources command",
2045-
"description": "Get a list of resources that will be deployed to CloudFormation.\n\nIf a stack name is provided, the corresponding physical IDs of each\nresource will be mapped to the logical ID of each resource.",
2043+
"list_endpoints": {
2044+
"title": "List Endpoints command",
2045+
"description": "Get a summary of the cloud endpoints in the stack.\n\nThis command will show both the cloud and local endpoints that can\nbe used with sam local and sam sync. Currently the endpoint resources\nare Lambda functions and API Gateway API resources.",
20462046
"properties": {
20472047
"parameters": {
2048-
"title": "Parameters for the list resources command",
2049-
"description": "Available parameters for the list resources command:\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* stack_name:\nName of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)\n* output:\nOutput the results from the command in a given output format (json or table).\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
2048+
"title": "Parameters for the list endpoints command",
2049+
"description": "Available parameters for the list endpoints command:\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* stack_name:\nName of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)\n* output:\nOutput the results from the command in a given output format (json or table).\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
20502050
"type": "object",
20512051
"properties": {
20522052
"parameter_overrides": {
@@ -2113,19 +2113,30 @@
21132113
"parameters"
21142114
]
21152115
},
2116-
"list_stack_outputs": {
2117-
"title": "List Stack Outputs command",
2118-
"description": "Get the stack outputs as defined in the SAM/CloudFormation template.",
2116+
"list_resources": {
2117+
"title": "List Resources command",
2118+
"description": "Get a list of resources that will be deployed to CloudFormation.\n\nIf a stack name is provided, the corresponding physical IDs of each\nresource will be mapped to the logical ID of each resource.",
21192119
"properties": {
21202120
"parameters": {
2121-
"title": "Parameters for the list stack outputs command",
2122-
"description": "Available parameters for the list stack outputs command:\n* stack_name:\nName of corresponding deployed stack.\n* output:\nOutput the results from the command in a given output format (json or table).\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
2121+
"title": "Parameters for the list resources command",
2122+
"description": "Available parameters for the list resources command:\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* stack_name:\nName of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)\n* output:\nOutput the results from the command in a given output format (json or table).\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
21232123
"type": "object",
21242124
"properties": {
2125+
"parameter_overrides": {
2126+
"title": "parameter_overrides",
2127+
"type": [
2128+
"array",
2129+
"string"
2130+
],
2131+
"description": "String that contains AWS CloudFormation parameter overrides encoded as key=value pairs.",
2132+
"items": {
2133+
"type": "string"
2134+
}
2135+
},
21252136
"stack_name": {
21262137
"title": "stack_name",
21272138
"type": "string",
2128-
"description": "Name of corresponding deployed stack."
2139+
"description": "Name of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)"
21292140
},
21302141
"output": {
21312142
"title": "output",
@@ -2137,6 +2148,12 @@
21372148
"table"
21382149
]
21392150
},
2151+
"template_file": {
2152+
"title": "template_file",
2153+
"type": "string",
2154+
"description": "AWS SAM template file.",
2155+
"default": "template.[yaml|yml|json]"
2156+
},
21402157
"profile": {
21412158
"title": "profile",
21422159
"type": "string",
@@ -2169,30 +2186,19 @@
21692186
"parameters"
21702187
]
21712188
},
2172-
"list_endpoints": {
2173-
"title": "List Endpoints command",
2174-
"description": "Get a summary of the cloud endpoints in the stack.\n\nThis command will show both the cloud and local endpoints that can\nbe used with sam local and sam sync. Currently the endpoint resources\nare Lambda functions and API Gateway API resources.",
2189+
"list_stack_outputs": {
2190+
"title": "List Stack Outputs command",
2191+
"description": "Get the stack outputs as defined in the SAM/CloudFormation template.",
21752192
"properties": {
21762193
"parameters": {
2177-
"title": "Parameters for the list endpoints command",
2178-
"description": "Available parameters for the list endpoints command:\n* parameter_overrides:\nString that contains AWS CloudFormation parameter overrides encoded as key=value pairs.\n* stack_name:\nName of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)\n* output:\nOutput the results from the command in a given output format (json or table).\n* template_file:\nAWS SAM template file.\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
2194+
"title": "Parameters for the list stack outputs command",
2195+
"description": "Available parameters for the list stack outputs command:\n* stack_name:\nName of corresponding deployed stack.\n* output:\nOutput the results from the command in a given output format (json or table).\n* profile:\nSelect a specific profile from your credential file to get AWS credentials.\n* region:\nSet the AWS Region of the service. (e.g. us-east-1)\n* beta_features:\nEnable/Disable beta features.\n* debug:\nTurn on debug logging to print debug message generated by AWS SAM CLI and display timestamps.\n* save_params:\nSave the parameters provided via the command line to the configuration file.",
21792196
"type": "object",
21802197
"properties": {
2181-
"parameter_overrides": {
2182-
"title": "parameter_overrides",
2183-
"type": [
2184-
"array",
2185-
"string"
2186-
],
2187-
"description": "String that contains AWS CloudFormation parameter overrides encoded as key=value pairs.",
2188-
"items": {
2189-
"type": "string"
2190-
}
2191-
},
21922198
"stack_name": {
21932199
"title": "stack_name",
21942200
"type": "string",
2195-
"description": "Name of corresponding deployed stack.(Not including a stack name will only show local resources defined in the template.)"
2201+
"description": "Name of corresponding deployed stack."
21962202
},
21972203
"output": {
21982204
"title": "output",
@@ -2204,12 +2210,6 @@
22042210
"table"
22052211
]
22062212
},
2207-
"template_file": {
2208-
"title": "template_file",
2209-
"type": "string",
2210-
"description": "AWS SAM template file.",
2211-
"default": "template.[yaml|yml|json]"
2212-
},
22132213
"profile": {
22142214
"title": "profile",
22152215
"type": "string",

0 commit comments

Comments
 (0)