diff --git a/.changes/next-release/enhancement-Performance-9167.json b/.changes/next-release/enhancement-Performance-9167.json new file mode 100644 index 000000000000..b60db1fefb24 --- /dev/null +++ b/.changes/next-release/enhancement-Performance-9167.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "Performance", + "description": "Defer loading of built-in plugins until they are actually needed to reduce initialization overhead." +} diff --git a/awscli/argprocess.py b/awscli/argprocess.py index fdc5eee8ba85..529ca1cd53e2 100644 --- a/awscli/argprocess.py +++ b/awscli/argprocess.py @@ -270,6 +270,13 @@ def _is_complex_shape(model): return True +def register_param_shorthand_parser(event_emitter): + event_emitter.register( + 'process-cli-arg', + ParamShorthandParser(), + ) + + class ParamShorthand: def _uses_old_list_case(self, command_name, operation_name, argument_name): """ diff --git a/awscli/botocore/hooks.py b/awscli/botocore/hooks.py index 90db67d6fe69..1530bbf158ee 100644 --- a/awscli/botocore/hooks.py +++ b/awscli/botocore/hooks.py @@ -196,7 +196,7 @@ def __init__(self): # read only access (we never modify self._handlers). # A cache of event name to handler list. self._lookup_cache = {} - self._handlers = _PrefixTrie() + self._handlers = PrefixTrie() # This is used to ensure that unique_id's are only # registered once. self._unique_id_handlers = {} @@ -398,7 +398,7 @@ def __copy__(self): return new_instance -class _PrefixTrie: +class PrefixTrie: """Specialized prefix trie that handles wildcards. The prefixes in this case are based on dot separated diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 643331c50d46..6be1e4b06abb 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -64,11 +64,13 @@ construct_entry_point_handlers_chain, ) from awscli.formatter import get_formatter +from awscli.handlers_registry import MAIN_COMMAND_TABLE_OPS from awscli.help import ( OperationHelpCommand, ProviderHelpCommand, ServiceHelpCommand, ) +from awscli.lazy_emitter import LazyInitEmitter from awscli.logger import ( disable_crt_logging, enable_crt_logging, @@ -117,7 +119,10 @@ def create_clidriver(args=None): parser = FirstPassGlobalArgParser() args, _ = parser.parse_known_args(args) debug = args.debug - session = botocore.session.Session() + lazy_emitter = LazyInitEmitter( + main_command_table_ops=MAIN_COMMAND_TABLE_OPS + ) + session = botocore.session.Session(event_hooks=lazy_emitter) _set_user_agent_for_session(session) load_plugins( session.full_config.get('plugins', {}), @@ -214,6 +219,13 @@ def _set_user_agent_for_session(session): add_session_id_component_to_user_agent_extra(session) +def register_no_pager_handler(event_emitter): + event_emitter.register( + 'session-initialized', + no_pager_handler, + ) + + def no_pager_handler(session, parsed_args, **kwargs): if parsed_args.no_cli_pager: config_store = session.get_component('config_store') diff --git a/awscli/customizations/addexamples.py b/awscli/customizations/addexamples.py index 49a9ed65b55c..5e91cc547aed 100644 --- a/awscli/customizations/addexamples.py +++ b/awscli/customizations/addexamples.py @@ -33,6 +33,19 @@ LOG = logging.getLogger(__name__) +def register_docs_add_examples(event_emitter): + # The following will get fired for every option we are + # documenting. It will attempt to add an example_fn on to + # the parameter object if the parameter supports shorthand + # syntax. The documentation event handlers will then use + # the examplefn to generate the sample shorthand syntax + # in the docs. Registering here should ensure that this + # handler gets called first, but it still feels a bit brittle. + # event_handlers.register('doc-option-example.*.*.*', + # param_shorthand.add_example_fn) + event_emitter.register('doc-examples.*.*', add_examples) + + def add_examples(help_command, **kwargs): doc_path = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'examples' diff --git a/awscli/customizations/binaryformat.py b/awscli/customizations/binaryformat.py index 7ac8030231c7..4812047fdf39 100644 --- a/awscli/customizations/binaryformat.py +++ b/awscli/customizations/binaryformat.py @@ -18,6 +18,13 @@ from awscli.shorthand import ModelVisitor +def register_init_binary_formatter(event_emitter): + event_emitter.register( + 'session-initialized', + add_binary_formatter, + ) + + def add_binary_formatter(session, parsed_args, **kwargs): binary_format = parsed_args.cli_binary_format if binary_format is None: diff --git a/awscli/customizations/codedeploy/codedeploy.py b/awscli/customizations/codedeploy/codedeploy.py index b1c78d648f0e..873511958909 100644 --- a/awscli/customizations/codedeploy/codedeploy.py +++ b/awscli/customizations/codedeploy/codedeploy.py @@ -22,11 +22,11 @@ from awscli.customizations.codedeploy.uninstall import Uninstall -def initialize(cli): - """ - The entry point for CodeDeploy high level commands. - """ +def register_rename_codedeploy(cli): cli.register('building-command-table.main', change_name) + + +def register_codedeploy(cli): cli.register('building-command-table.deploy', inject_commands) cli.register( 'building-argument-table.deploy.get-application-revision', diff --git a/awscli/customizations/ec2/decryptpassword.py b/awscli/customizations/ec2/decryptpassword.py index 4fba23fb59f0..902cd4c800cd 100644 --- a/awscli/customizations/ec2/decryptpassword.py +++ b/awscli/customizations/ec2/decryptpassword.py @@ -27,6 +27,13 @@ password data sent from EC2 will be decrypted before display.

""" +def register_ec2_add_priv_launch_key(event_emitter, **kwargs): + event_emitter.register( + 'building-argument-table.ec2.get-password-data', + ec2_add_priv_launch_key, + ) + + def ec2_add_priv_launch_key( argument_table, operation_model, session, **kwargs ): diff --git a/awscli/customizations/iot.py b/awscli/customizations/iot.py index f4e4b9770513..8b0153d6fcc5 100644 --- a/awscli/customizations/iot.py +++ b/awscli/customizations/iot.py @@ -26,6 +26,20 @@ from awscli.customizations.arguments import QueryOutFileArgument +def register_iot_create_keys_from_csr(event_emitter): + event_emitter.register( + 'building-argument-table.iot.create-certificate-from-csr', + register_create_keys_from_csr_arguments, + ) + + +def register_iot_create_keys_and_cert_args(event_emitter): + event_emitter.register( + 'building-argument-table.iot.create-keys-and-certificate', + register_create_keys_and_cert_arguments, + ) + + def register_create_keys_and_cert_arguments(session, argument_table, **kwargs): """Add outfile save arguments to create-keys-and-certificate diff --git a/awscli/customizations/s3/s3.py b/awscli/customizations/s3/s3.py index 725bd0ca9bf7..9dad2aaa03d1 100644 --- a/awscli/customizations/s3/s3.py +++ b/awscli/customizations/s3/s3.py @@ -28,23 +28,14 @@ ) -def awscli_initialize(cli): - """ - This function is require to use the plugin. It calls the functions - required to add all necessary commands and parameters to the CLI. - This function is necessary to install the plugin using a configuration - file - """ - cli.register("building-command-table.main", add_s3) - cli.register('building-command-table.s3_sync', register_sync_strategies) +def register_s3_main(event_handlers): + event_handlers.register('building-command-table.main', add_s3) -def s3_plugin_initialize(event_handlers): - """ - This is a wrapper to make the plugin built-in to the cli as opposed - to specifying it in the configuration file. - """ - awscli_initialize(event_handlers) +def register_s3_sync_strategies(event_handlers): + event_handlers.register( + 'building-command-table.s3_sync', register_sync_strategies + ) def add_s3(command_table, session, **kwargs): diff --git a/awscli/customizations/streamingoutputarg.py b/awscli/customizations/streamingoutputarg.py index c4decbb6844d..6c289c0ec140 100644 --- a/awscli/customizations/streamingoutputarg.py +++ b/awscli/customizations/streamingoutputarg.py @@ -15,6 +15,13 @@ from awscli.arguments import BaseCLIArgument +def register_streaming_output_arg(event_emitter): + event_emitter.register( + 'building-argument-table.*', + add_streaming_output_arg, + ) + + def add_streaming_output_arg( argument_table, operation_model, session, **kwargs ): diff --git a/awscli/handlers.py b/awscli/handlers.py deleted file mode 100644 index 3f0aa0180078..000000000000 --- a/awscli/handlers.py +++ /dev/null @@ -1,243 +0,0 @@ -# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Builtin CLI extensions. - -This is a collection of built in CLI extensions that can be automatically -registered with the event system. - -""" - -from awscli.alias import register_alias_commands -from awscli.argprocess import ParamShorthandParser -from awscli.clidriver import no_pager_handler -from awscli.customizations import datapipeline -from awscli.customizations.addexamples import add_examples -from awscli.customizations.argrename import register_arg_renames -from awscli.customizations.assumerole import register_assume_role_provider -from awscli.customizations.awslambda import register_lambda_create_function -from awscli.customizations.binaryformat import add_binary_formatter -from awscli.customizations.cliinput import register_cli_input_args -from awscli.customizations.cloudformation import ( - initialize as cloudformation_init, -) -from awscli.customizations.cloudfront import register as register_cloudfront -from awscli.customizations.cloudsearch import initialize as cloudsearch_init -from awscli.customizations.cloudsearchdomain import register_cloudsearchdomain -from awscli.customizations.cloudtrail import initialize as cloudtrail_init -from awscli.customizations.cloudwatch import register_rename_otel_commands -from awscli.customizations.codeartifact import register_codeartifact_commands -from awscli.customizations.codecommit import initialize as codecommit_init -from awscli.customizations.codedeploy.codedeploy import ( - initialize as codedeploy_init, -) -from awscli.customizations.configservice.getstatus import register_get_status -from awscli.customizations.configservice.putconfigurationrecorder import ( - register_modify_put_configuration_recorder, -) -from awscli.customizations.configservice.rename_cmd import ( - register_rename_config, -) -from awscli.customizations.configservice.subscribe import register_subscribe -from awscli.customizations.configure.configure import register_configure_cmd -from awscli.customizations.devcommands import register_dev_commands -from awscli.customizations.dlm.dlm import dlm_initialize -from awscli.customizations.dsql import register_dsql_customizations -from awscli.customizations.dynamodb.ddb import register_ddb -from awscli.customizations.dynamodb.paginatorfix import ( - register_dynamodb_paginator_fix, -) -from awscli.customizations.ec2.addcount import register_count_events -from awscli.customizations.ec2.bundleinstance import register_bundleinstance -from awscli.customizations.ec2.decryptpassword import ec2_add_priv_launch_key -from awscli.customizations.ec2.paginate import register_ec2_page_size_injector -from awscli.customizations.ec2.protocolarg import register_protocol_args -from awscli.customizations.ec2.runinstances import register_runinstances -from awscli.customizations.ec2.secgroupsimplify import register_secgroup -from awscli.customizations.ec2instanceconnect import ( - register_ec2_instance_connect_commands, -) -from awscli.customizations.ecr import register_ecr_commands -from awscli.customizations.ecr_public import register_ecr_public_commands -from awscli.customizations.ecs import initialize as ecs_initialize -from awscli.customizations.ecs.monitormutatinggatewayservice import ( - register_monitor_mutating_gateway_service, -) -from awscli.customizations.eks import initialize as eks_initialize -from awscli.customizations.emr.emr import emr_initialize -from awscli.customizations.emrcontainers import ( - initialize as emrcontainers_initialize, -) -from awscli.customizations.gamelift import register_gamelift_commands -from awscli.customizations.generatecliskeleton import ( - register_generate_cli_skeleton, -) -from awscli.customizations.globalargs import register_parse_global_args -from awscli.customizations.history import ( - register_history_commands, - register_history_mode, -) -from awscli.customizations.iamvirtmfa import IAMVMFAWrapper -from awscli.customizations.iot import ( - register_create_keys_and_cert_arguments, - register_create_keys_from_csr_arguments, -) -from awscli.customizations.iot_data import register_custom_endpoint_note -from awscli.customizations.kinesis import ( - register_kinesis_list_streams_pagination_backcompat, -) -from awscli.customizations.kms import register_fix_kms_create_grant_docs -from awscli.customizations.lightsail import initialize as lightsail_initialize -from awscli.customizations.login import register_login_cmds -from awscli.customizations.logs import register_logs_commands -from awscli.customizations.paginate import register_pagination -from awscli.customizations.putmetricdata import register_put_metric_data -from awscli.customizations.quicksight import ( - register_quicksight_asset_bundle_customizations, -) -from awscli.customizations.rds import ( - register_add_generate_db_auth_token, - register_rds_modify_split, -) -from awscli.customizations.rekognition import ( - register_rekognition_detect_labels, -) -from awscli.customizations.removals import register_removals -from awscli.customizations.route53 import register_create_hosted_zone_doc_fix -from awscli.customizations.s3.s3 import s3_plugin_initialize -from awscli.customizations.s3errormsg import register_s3_error_msg -from awscli.customizations.s3events import ( - register_document_expires_string, - register_event_stream_arg, -) -from awscli.customizations.servicecatalog import ( - register_servicecatalog_commands, -) -from awscli.customizations.sessendemail import register_ses_send_email -from awscli.customizations.sessionmanager import register_ssm_session -from awscli.customizations.sso import register_sso_commands -from awscli.customizations.streamingoutputarg import add_streaming_output_arg -from awscli.customizations.timestampformat import register_timestamp_format -from awscli.customizations.toplevelbool import register_bool_params -from awscli.customizations.translate import ( - register_translate_import_terminology, -) -from awscli.customizations.waiters import register_add_waiters -from awscli.customizations.wizard.commands import register_wizard_commands -from awscli.paramfile import register_uri_param_handler - - -def awscli_initialize(event_handlers): - event_handlers.register('session-initialized', register_uri_param_handler) - event_handlers.register('session-initialized', add_binary_formatter) - event_handlers.register('session-initialized', no_pager_handler) - param_shorthand = ParamShorthandParser() - event_handlers.register('process-cli-arg', param_shorthand) - # The s3 error mesage needs to registered before the - # generic error handler. - register_s3_error_msg(event_handlers) - # # The following will get fired for every option we are - # # documenting. It will attempt to add an example_fn on to - # # the parameter object if the parameter supports shorthand - # # syntax. The documentation event handlers will then use - # # the examplefn to generate the sample shorthand syntax - # # in the docs. Registering here should ensure that this - # # handler gets called first but it still feels a bit brittle. - # event_handlers.register('doc-option-example.*.*.*', - # param_shorthand.add_example_fn) - event_handlers.register('doc-examples.*.*', add_examples) - register_cli_input_args(event_handlers) - event_handlers.register( - 'building-argument-table.*', add_streaming_output_arg - ) - register_count_events(event_handlers) - event_handlers.register( - 'building-argument-table.ec2.get-password-data', - ec2_add_priv_launch_key, - ) - register_parse_global_args(event_handlers) - register_pagination(event_handlers) - register_secgroup(event_handlers) - register_bundleinstance(event_handlers) - s3_plugin_initialize(event_handlers) - register_ddb(event_handlers) - register_runinstances(event_handlers) - register_removals(event_handlers) - register_rds_modify_split(event_handlers) - register_rekognition_detect_labels(event_handlers) - register_add_generate_db_auth_token(event_handlers) - register_dsql_customizations(event_handlers) - register_put_metric_data(event_handlers) - register_ses_send_email(event_handlers) - IAMVMFAWrapper(event_handlers) - register_arg_renames(event_handlers) - register_configure_cmd(event_handlers) - cloudtrail_init(event_handlers) - register_ecr_commands(event_handlers) - register_ecr_public_commands(event_handlers) - register_bool_params(event_handlers) - register_protocol_args(event_handlers) - datapipeline.register_customizations(event_handlers) - cloudsearch_init(event_handlers) - emr_initialize(event_handlers) - emrcontainers_initialize(event_handlers) - eks_initialize(event_handlers) - ecs_initialize(event_handlers) - register_monitor_mutating_gateway_service(event_handlers) - lightsail_initialize(event_handlers) - register_cloudsearchdomain(event_handlers) - register_generate_cli_skeleton(event_handlers) - register_assume_role_provider(event_handlers) - register_add_waiters(event_handlers) - codedeploy_init(event_handlers) - register_subscribe(event_handlers) - register_get_status(event_handlers) - register_rename_config(event_handlers) - register_timestamp_format(event_handlers) - register_lambda_create_function(event_handlers) - register_fix_kms_create_grant_docs(event_handlers) - register_create_hosted_zone_doc_fix(event_handlers) - register_modify_put_configuration_recorder(event_handlers) - register_codeartifact_commands(event_handlers) - codecommit_init(event_handlers) - register_custom_endpoint_note(event_handlers) - event_handlers.register( - 'building-argument-table.iot.create-keys-and-certificate', - register_create_keys_and_cert_arguments, - ) - event_handlers.register( - 'building-argument-table.iot.create-certificate-from-csr', - register_create_keys_from_csr_arguments, - ) - register_cloudfront(event_handlers) - register_gamelift_commands(event_handlers) - register_ec2_page_size_injector(event_handlers) - cloudformation_init(event_handlers) - register_servicecatalog_commands(event_handlers) - register_translate_import_terminology(event_handlers) - register_rename_otel_commands(event_handlers) - register_history_mode(event_handlers) - register_history_commands(event_handlers) - register_event_stream_arg(event_handlers) - register_document_expires_string(event_handlers) - dlm_initialize(event_handlers) - register_ssm_session(event_handlers) - register_logs_commands(event_handlers) - register_dev_commands(event_handlers) - register_wizard_commands(event_handlers) - register_sso_commands(event_handlers) - register_dynamodb_paginator_fix(event_handlers) - register_alias_commands(event_handlers) - register_kinesis_list_streams_pagination_backcompat(event_handlers) - register_quicksight_asset_bundle_customizations(event_handlers) - register_ec2_instance_connect_commands(event_handlers) - register_login_cmds(event_handlers) diff --git a/awscli/handlers_registry.py b/awscli/handlers_registry.py new file mode 100644 index 000000000000..fcca245f00f7 --- /dev/null +++ b/awscli/handlers_registry.py @@ -0,0 +1,888 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Maps event patterns to initializer entries. + +At runtime, the LazyInitEmitter triggers entries on demand: +before emitting event X, it finds entries whose event patterns +match X, calls each init function at most once (passing the +event_handlers emitter), then proceeds with normal event dispatch. + +Entry format: + (module, fn_name) call fn(event_handlers) +""" +from __future__ import annotations + +import enum + + +class CommandTableOp(enum.Enum): + """Valid operation types for MAIN_COMMAND_TABLE_OPS entries.""" + + ADD = 'add' + RENAME = 'rename' + + +PLUGIN_REGISTRY = { + 'after-call.data-pipeline.GetPipelineDefinition': [ + ('awscli.customizations.datapipeline', 'register_customizations') + ], + 'after-call.ecs.CreateExpressGatewayService': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'after-call.ecs.DeleteExpressGatewayService': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'after-call.ecs.UpdateExpressGatewayService': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'after-call.iam.CreateVirtualMFADevice': [ + ('awscli.customizations.iamvirtmfa', 'IAMVMFAWrapper') + ], + 'after-call.s3': [ + ('awscli.customizations.s3errormsg', 'register_s3_error_msg') + ], + 'before-building-argument-table-parser.ecs.create-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'before-building-argument-table-parser.ecs.delete-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'before-building-argument-table-parser.ecs.update-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'before-building-argument-table-parser.emr.*': [ + ('awscli.customizations.emr.emr', 'emr_initialize') + ], + 'before-parameter-build.ec2.BundleInstance': [ + ('awscli.customizations.ec2.bundleinstance', 'register_bundleinstance') + ], + 'before-parameter-build.ec2.CreateNetworkAclEntry': [ + ('awscli.customizations.ec2.protocolarg', 'register_protocol_args') + ], + 'before-parameter-build.ec2.ReplaceNetworkAclEntry': [ + ('awscli.customizations.ec2.protocolarg', 'register_protocol_args') + ], + 'before-parameter-build.ec2.RunInstances': [ + ('awscli.customizations.ec2.addcount', 'register_count_events'), + ('awscli.customizations.ec2.runinstances', 'register_runinstances'), + ], + 'building-argument-table': [ + ('awscli.customizations.cliinput', 'register_cli_input_args'), + ('awscli.customizations.paginate', 'register_pagination'), + ( + 'awscli.customizations.generatecliskeleton', + 'register_generate_cli_skeleton', + ), + ], + 'building-argument-table.*': [ + ( + 'awscli.customizations.streamingoutputarg', + 'register_streaming_output_arg', + ) + ], + 'building-argument-table.apigateway.create-rest-api': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.apigatewayv2.create-api': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.apigatewayv2.update-api': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.clouddirectory.publish-schema': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.cloudfront.create-distribution': [ + ('awscli.customizations.cloudfront', 'register') + ], + 'building-argument-table.cloudfront.create-invalidation': [ + ('awscli.customizations.cloudfront', 'register') + ], + 'building-argument-table.cloudfront.update-distribution': [ + ('awscli.customizations.cloudfront', 'register') + ], + 'building-argument-table.cloudsearch.define-expression': [ + ('awscli.customizations.cloudsearch', 'initialize') + ], + 'building-argument-table.cloudsearch.define-index-field': [ + ('awscli.customizations.cloudsearch', 'initialize') + ], + 'building-argument-table.cloudsearchdomain.search': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.cloudsearchdomain.suggest': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.cloudwatch.put-metric-data': [ + ('awscli.customizations.putmetricdata', 'register_put_metric_data') + ], + 'building-argument-table.codepipeline.create-custom-action-type': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.codepipeline.delete-custom-action-type': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.codepipeline.get-action-type': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.codepipeline.get-pipeline': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.configservice.put-configuration-recorder': [ + ( + 'awscli.customizations.configservice.putconfigurationrecorder', + 'register_modify_put_configuration_recorder', + ) + ], + 'building-argument-table.controltower.create-landing-zone': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.controltower.update-landing-zone': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.datapipeline.*': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.datapipeline.activate-pipeline': [ + ('awscli.customizations.datapipeline', 'register_customizations') + ], + 'building-argument-table.datapipeline.get-pipeline-definition': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.datapipeline.put-pipeline-definition': [ + ('awscli.customizations.datapipeline', 'register_customizations') + ], + 'building-argument-table.deploy.*': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.deploy.create-deployment': [ + ('awscli.customizations.codedeploy.codedeploy', 'register_codedeploy') + ], + 'building-argument-table.deploy.get-application-revision': [ + ('awscli.customizations.codedeploy.codedeploy', 'register_codedeploy') + ], + 'building-argument-table.deploy.register-application-revision': [ + ('awscli.customizations.codedeploy.codedeploy', 'register_codedeploy') + ], + 'building-argument-table.ec2.*': [ + ('awscli.customizations.argrename', 'register_arg_renames'), + ('awscli.customizations.toplevelbool', 'register_bool_params'), + ], + 'building-argument-table.ec2.authorize-security-group-egress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'building-argument-table.ec2.authorize-security-group-ingress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'building-argument-table.ec2.bundle-instance': [ + ('awscli.customizations.ec2.bundleinstance', 'register_bundleinstance') + ], + 'building-argument-table.ec2.create-image': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.ec2.get-password-data': [ + ( + 'awscli.customizations.ec2.decryptpassword', + 'register_ec2_add_priv_launch_key', + ) + ], + 'building-argument-table.ec2.revoke-security-group-egress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'building-argument-table.ec2.revoke-security-group-ingress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'building-argument-table.ec2.run-instances': [ + ('awscli.customizations.ec2.addcount', 'register_count_events'), + ('awscli.customizations.ec2.runinstances', 'register_runinstances'), + ], + 'building-argument-table.ecs.*': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.ecs.create-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'building-argument-table.ecs.delete-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'building-argument-table.ecs.execute-command': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.ecs.update-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'building-argument-table.eks.create-cluster': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.eks.create-nodegroup': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.eks.update-cluster-components-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.eks.update-cluster-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.eks.update-nodegroup-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.elasticache.create-replication-group': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.emr.*': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.emr.add-tags': [ + ('awscli.customizations.emr.emr', 'emr_initialize') + ], + 'building-argument-table.emr.list-clusters': [ + ('awscli.customizations.emr.emr', 'emr_initialize') + ], + 'building-argument-table.gamelift.create-build': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.gamelift.create-script': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.gamelift.update-build': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.gamelift.update-script': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.glue.get-unfiltered-partition-metadata': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.glue.get-unfiltered-partitions-metadata': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.glue.get-unfiltered-table-metadata': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.iam.create-virtual-mfa-device': [ + ('awscli.customizations.iamvirtmfa', 'IAMVMFAWrapper') + ], + 'building-argument-table.iot.create-certificate-from-csr': [ + ('awscli.customizations.iot', 'register_iot_create_keys_from_csr') + ], + 'building-argument-table.iot.create-keys-and-certificate': [ + ('awscli.customizations.iot', 'register_iot_create_keys_and_cert_args') + ], + 'building-argument-table.iotwireless.*': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.kinesis.list-streams': [ + ( + 'awscli.customizations.kinesis', + 'register_kinesis_list_streams_pagination_backcompat', + ) + ], + 'building-argument-table.kinesisanalytics.add-application-output': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.kinesisanalyticsv2.add-application-output': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lambda.create-function': [ + ('awscli.customizations.awslambda', 'register_lambda_create_function') + ], + 'building-argument-table.lambda.publish-layer-version': [ + ('awscli.customizations.awslambda', 'register_lambda_create_function') + ], + 'building-argument-table.lambda.update-function-code': [ + ('awscli.customizations.awslambda', 'register_lambda_create_function') + ], + 'building-argument-table.lex-models.delete-bot': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lex-models.delete-bot-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lex-models.delete-intent': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lex-models.delete-intent-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lex-models.delete-slot-type': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lex-models.delete-slot-type-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lex-models.get-export': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lex-models.get-intent': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.lex-models.get-slot-type': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.license-manager.delete-grant': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.license-manager.get-grant': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.license-manager.get-license': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.mgn.*': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.mturk.list-qualification-types': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.delete-email-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.delete-in-app-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.delete-push-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.delete-sms-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.delete-voice-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.get-campaign-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.get-email-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.get-in-app-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.get-push-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.get-segment-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.get-sms-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.get-voice-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.update-email-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.update-in-app-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.update-push-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.update-sms-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.pinpoint.update-voice-template': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.quicksight.start-asset-bundle-import-job': [ + ( + 'awscli.customizations.quicksight', + 'register_quicksight_asset_bundle_customizations', + ) + ], + 'building-argument-table.rds.add-option-to-option-group': [ + ('awscli.customizations.rds', 'register_rds_modify_split') + ], + 'building-argument-table.rds.remove-option-from-option-group': [ + ('awscli.customizations.rds', 'register_rds_modify_split') + ], + 'building-argument-table.rekognition.*': [ + ( + 'awscli.customizations.rekognition', + 'register_rekognition_detect_labels', + ) + ], + 'building-argument-table.rekognition.compare-faces': [ + ( + 'awscli.customizations.rekognition', + 'register_rekognition_detect_labels', + ) + ], + 'building-argument-table.rekognition.create-stream-processor': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.route53.delete-traffic-policy': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.route53.get-traffic-policy': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.route53.update-traffic-policy-comment': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.route53domains.view-billing': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.s3api.select-object-content': [ + ('awscli.customizations.s3events', 'register_event_stream_arg') + ], + 'building-argument-table.sagemaker.delete-image-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.sagemaker.describe-image-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.sagemaker.list-aliases': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.sagemaker.update-image-version': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.schemas.*': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.ses.send-email': [ + ('awscli.customizations.sessendemail', 'register_ses_send_email') + ], + 'building-argument-table.sns.subscribe': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.stepfunctions.send-task-success': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.swf.register-activity-type': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.swf.register-workflow-type': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.translate.import-terminology': [ + ( + 'awscli.customizations.translate', + 'register_translate_import_terminology', + ) + ], + 'building-argument-table.translate.translate-document': [ + ( + 'awscli.customizations.translate', + 'register_translate_import_terminology', + ) + ], + 'building-argument-table.workdocs.create-notification-subscription': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-argument-table.workdocs.describe-users': [ + ('awscli.customizations.argrename', 'register_arg_renames') + ], + 'building-command-table': [ + ('awscli.customizations.waiters', 'register_add_waiters'), + ('awscli.alias', 'register_alias_commands'), + ], + 'building-command-table.bedrock-agent-runtime': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.bedrock-agentcore': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.bedrock-runtime': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.cli-dev': [ + ('awscli.customizations.wizard.commands', 'register_wizard_commands') + ], + 'building-command-table.cloudformation': [ + ('awscli.customizations.cloudformation', 'initialize') + ], + 'building-command-table.cloudfront': [ + ('awscli.customizations.cloudfront', 'register') + ], + 'building-command-table.cloudtrail': [ + ('awscli.customizations.cloudtrail', 'initialize') + ], + 'building-command-table.cloudwatch': [ + ('awscli.customizations.cloudwatch', 'register_rename_otel_commands') + ], + 'building-command-table.codeartifact': [ + ( + 'awscli.customizations.codeartifact', + 'register_codeartifact_commands', + ) + ], + 'building-command-table.codecommit': [ + ('awscli.customizations.codecommit', 'initialize') + ], + 'building-command-table.configservice': [ + ( + 'awscli.customizations.configservice.subscribe', + 'register_subscribe', + ), + ( + 'awscli.customizations.configservice.getstatus', + 'register_get_status', + ), + ], + 'building-command-table.configure': [ + ('awscli.customizations.wizard.commands', 'register_wizard_commands') + ], + 'building-command-table.connecthealth': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.datapipeline': [ + ('awscli.customizations.datapipeline', 'register_customizations') + ], + 'building-command-table.deploy': [ + ('awscli.customizations.codedeploy.codedeploy', 'register_codedeploy') + ], + 'building-command-table.devops-agent': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.dlm': [ + ('awscli.customizations.dlm.dlm', 'dlm_initialize') + ], + 'building-command-table.dsql': [ + ('awscli.customizations.dsql', 'register_dsql_customizations') + ], + 'building-command-table.dynamodb': [ + ('awscli.customizations.wizard.commands', 'register_wizard_commands') + ], + 'building-command-table.ec2': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.ec2-instance-connect': [ + ( + 'awscli.customizations.ec2instanceconnect', + 'register_ec2_instance_connect_commands', + ) + ], + 'building-command-table.ecr': [ + ('awscli.customizations.ecr', 'register_ecr_commands') + ], + 'building-command-table.ecr-public': [ + ('awscli.customizations.ecr_public', 'register_ecr_public_commands') + ], + 'building-command-table.ecs': [ + ('awscli.customizations.ecs', 'initialize') + ], + 'building-command-table.eks': [ + ('awscli.customizations.eks', 'initialize') + ], + 'building-command-table.emr': [ + ('awscli.customizations.removals', 'register_removals'), + ('awscli.customizations.emr.emr', 'emr_initialize'), + ], + 'building-command-table.emr-containers': [ + ('awscli.customizations.emrcontainers', 'initialize') + ], + 'building-command-table.events': [ + ('awscli.customizations.wizard.commands', 'register_wizard_commands') + ], + 'building-command-table.gamelift': [ + ('awscli.customizations.gamelift', 'register_gamelift_commands') + ], + 'building-command-table.iam': [ + ('awscli.customizations.wizard.commands', 'register_wizard_commands') + ], + 'building-command-table.iotsitewise': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.kinesis': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.lambda': [ + ('awscli.customizations.removals', 'register_removals'), + ('awscli.customizations.wizard.commands', 'register_wizard_commands'), + ], + 'building-command-table.lexv2-runtime': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.lightsail': [ + ('awscli.customizations.lightsail', 'initialize') + ], + 'building-command-table.logs': [ + ('awscli.customizations.removals', 'register_removals'), + ('awscli.customizations.logs', 'register_logs_commands'), + ], + 'building-command-table.main': [ + ('awscli.customizations.s3.s3', 'register_s3_main'), + ('awscli.customizations.dynamodb.ddb', 'register_ddb'), + ( + 'awscli.customizations.configure.configure', + 'register_configure_cmd', + ), + ( + 'awscli.customizations.codedeploy.codedeploy', + 'register_rename_codedeploy', + ), + ( + 'awscli.customizations.configservice.rename_cmd', + 'register_rename_config', + ), + ('awscli.customizations.history', 'register_history_commands'), + ('awscli.customizations.devcommands', 'register_dev_commands'), + ('awscli.customizations.login', 'register_login_cmds'), + ], + 'building-command-table.polly': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.qbusiness': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.rds': [ + ('awscli.customizations.rds', 'register_rds_modify_split'), + ('awscli.customizations.rds', 'register_add_generate_db_auth_token'), + ], + 'building-command-table.s3_sync': [ + ('awscli.customizations.s3.s3', 'register_s3_sync_strategies') + ], + 'building-command-table.sagemaker-runtime': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.servicecatalog': [ + ( + 'awscli.customizations.servicecatalog', + 'register_servicecatalog_commands', + ) + ], + 'building-command-table.ses': [ + ('awscli.customizations.removals', 'register_removals') + ], + 'building-command-table.ssm': [ + ('awscli.customizations.sessionmanager', 'register_ssm_session') + ], + 'building-command-table.sso': [ + ('awscli.customizations.sso', 'register_sso_commands') + ], + 'calling-command.cloudsearchdomain': [ + ( + 'awscli.customizations.cloudsearchdomain', + 'register_cloudsearchdomain', + ) + ], + 'calling-command.dynamodb.*': [ + ( + 'awscli.customizations.dynamodb.paginatorfix', + 'register_dynamodb_paginator_fix', + ) + ], + 'calling-command.ec2.describe-snapshots': [ + ( + 'awscli.customizations.ec2.paginate', + 'register_ec2_page_size_injector', + ) + ], + 'calling-command.ec2.describe-volumes': [ + ( + 'awscli.customizations.ec2.paginate', + 'register_ec2_page_size_injector', + ) + ], + 'doc-description': [ + ('awscli.customizations.paginate', 'register_pagination') + ], + 'doc-description.ec2.authorize-security-group-egress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'doc-description.ec2.authorize-security-group-ingress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'doc-description.ec2.revoke-security-group-ingress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'doc-description.ec2.revoke-security-groupdoc-ingress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'doc-description.iot-data': [ + ('awscli.customizations.iot_data', 'register_custom_endpoint_note') + ], + 'doc-examples.*.*': [ + ('awscli.customizations.addexamples', 'register_docs_add_examples') + ], + 'doc-option.route53.create-hosted-zone.hosted-zone-config': [ + ( + 'awscli.customizations.route53', + 'register_create_hosted_zone_doc_fix', + ) + ], + 'doc-output.datapipeline.get-pipeline-definition': [ + ('awscli.customizations.datapipeline', 'register_customizations') + ], + 'doc-output.s3api': [ + ('awscli.customizations.s3events', 'register_document_expires_string') + ], + 'doc-output.s3api.select-object-content': [ + ('awscli.customizations.s3events', 'register_event_stream_arg') + ], + 'doc-title.kms.create-grant': [ + ('awscli.customizations.kms', 'register_fix_kms_create_grant_docs') + ], + 'operation-args-parsed.cloudfront.create-distribution': [ + ('awscli.customizations.cloudfront', 'register') + ], + 'operation-args-parsed.cloudfront.create-invalidation': [ + ('awscli.customizations.cloudfront', 'register') + ], + 'operation-args-parsed.cloudfront.update-distribution': [ + ('awscli.customizations.cloudfront', 'register') + ], + 'operation-args-parsed.cloudwatch.put-metric-data': [ + ('awscli.customizations.putmetricdata', 'register_put_metric_data') + ], + 'operation-args-parsed.ec2.authorize-security-group-egress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'operation-args-parsed.ec2.authorize-security-group-ingress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'operation-args-parsed.ec2.bundle-instance': [ + ('awscli.customizations.ec2.bundleinstance', 'register_bundleinstance') + ], + 'operation-args-parsed.ec2.revoke-security-group-egress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'operation-args-parsed.ec2.revoke-security-group-ingress': [ + ('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup') + ], + 'operation-args-parsed.ec2.run-instances': [ + ('awscli.customizations.ec2.runinstances', 'register_runinstances') + ], + 'operation-args-parsed.ecs.create-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'operation-args-parsed.ecs.delete-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'operation-args-parsed.ecs.update-express-gateway-service': [ + ( + 'awscli.customizations.ecs.monitormutatinggatewayservice', + 'register_monitor_mutating_gateway_service', + ) + ], + 'operation-args-parsed.kinesis.list-streams': [ + ( + 'awscli.customizations.kinesis', + 'register_kinesis_list_streams_pagination_backcompat', + ) + ], + 'operation-args-parsed.ses.send-email': [ + ('awscli.customizations.sessendemail', 'register_ses_send_email') + ], + 'process-cli-arg': [ + ('awscli.argprocess', 'register_param_shorthand_parser') + ], + 'process-cli-arg.lambda.update-function-code': [ + ('awscli.customizations.awslambda', 'register_lambda_create_function') + ], + 'session-initialized': [ + ('awscli.paramfile', 'register_init_uri_param_handler'), + ( + 'awscli.customizations.binaryformat', + 'register_init_binary_formatter', + ), + ('awscli.clidriver', 'register_no_pager_handler'), + ('awscli.customizations.assumerole', 'register_assume_role_provider'), + ('awscli.customizations.timestampformat', 'register_timestamp_format'), + ('awscli.customizations.history', 'register_history_mode'), + ('awscli.customizations.sso', 'register_sso_commands'), + ], + 'top-level-args-parsed': [ + ('awscli.customizations.globalargs', 'register_parse_global_args'), + ('awscli.customizations.cloudfront', 'register'), + ], +} + +# Declarative model of changes made to the command table by plugins +# that register against building-command-table.main. +# +# At runtime, plugins listed in building-command-table.main above +# are NOT called as init functions. Instead, these pre-computed +# operations are applied directly, allowing added commands to be +# wrapped in LazyCommand and deferring heavy module imports until +# the command is actually invoked. +# +# Entry formats: +# (CommandTableOp.RENAME, old_name, new_name) +# (CommandTableOp.ADD, cmd_name, cmd_module, cmd_class) + +MAIN_COMMAND_TABLE_OPS: list[ + tuple[CommandTableOp, str, str] | tuple[CommandTableOp, str, str, str] +] = [ + (CommandTableOp.RENAME, 's3', 's3api'), + (CommandTableOp.ADD, 's3', 'awscli.customizations.s3.s3', 'S3'), + (CommandTableOp.ADD, 'ddb', 'awscli.customizations.dynamodb.ddb', 'DDB'), + ( + CommandTableOp.ADD, + 'configure', + 'awscli.customizations.configure.configure', + 'ConfigureCommand', + ), + (CommandTableOp.RENAME, 'codedeploy', 'deploy'), + (CommandTableOp.RENAME, 'config', 'configservice'), + ( + CommandTableOp.ADD, + 'history', + 'awscli.customizations.history', + 'HistoryCommand', + ), + ( + CommandTableOp.ADD, + 'cli-dev', + 'awscli.customizations.devcommands', + 'CLIDevCommand', + ), + ( + CommandTableOp.ADD, + 'login', + 'awscli.customizations.login.login', + 'LoginCommand', + ), + ( + CommandTableOp.ADD, + 'logout', + 'awscli.customizations.login.logout', + 'LogoutCommand', + ), +] diff --git a/awscli/lazy.py b/awscli/lazy.py new file mode 100644 index 000000000000..58eccd7df515 --- /dev/null +++ b/awscli/lazy.py @@ -0,0 +1,72 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import importlib + +from awscli.commands import CLICommand + + +class LazyCommand(CLICommand): + """A command-table entry that defers importing its real implementation. + + Sits in the command table like any other CLICommand, but only imports + the actual module (and creates the real command object) when the command + is invoked or its help is accessed. + """ + + def __init__(self, name, session, module_path, class_name): + self._name = name + self._session = session + self._module_path = module_path + self._class_name = class_name + self._real = None + self._lineage = [self] + + def _resolve(self): + if self._real is None: + mod = importlib.import_module(self._module_path) + cls = getattr(mod, self._class_name) + self._real = cls(self._session) + self._real.lineage = self._lineage + return self._real + + def __call__(self, args, parsed_globals): + return self._resolve()(args, parsed_globals) + + def create_help_command(self): + return self._resolve().create_help_command() + + @property + def arg_table(self): + return self._resolve().arg_table + + @property + def subcommand_table(self): + return self._resolve().subcommand_table + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def lineage(self): + return self._lineage + + @lineage.setter + def lineage(self, value): + self._lineage = value + if self._real is not None: + self._real.lineage = value diff --git a/awscli/lazy_emitter.py b/awscli/lazy_emitter.py new file mode 100644 index 000000000000..2043902c77cd --- /dev/null +++ b/awscli/lazy_emitter.py @@ -0,0 +1,157 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Lazy-initializing event emitter for the AWS CLI plugin system. + +LazyInitEmitter wraps a HierarchicalEmitter and uses the plugin registry +to initialize plugins on demand. Before emitting event X, it finds all +initializer entries whose event patterns match X (using the same +prefix/wildcard semantics as HierarchicalEmitter), calls each initializer +at most once, then delegates to the underlying emitter for normal dispatch. +""" + +import copy +import importlib +import logging + +from botocore.hooks import HierarchicalEmitter, PrefixTrie + +from awscli.handlers_registry import ( + PLUGIN_REGISTRY, + CommandTableOp, +) +from awscli.lazy import LazyCommand + +logger = logging.getLogger(__name__) + + +class LazyInitEmitter(HierarchicalEmitter): + """HierarchicalEmitter that lazily initializes plugins from a registry. + + The registry maps event patterns to lists of (module, fn_name) tuples. + Before emitting any event, this emitter checks whether there are + uninitialised plugins whose event patterns match the event being emitted. + If so, it imports and calls them, then proceeds with normal dispatch. + """ + + def __init__(self, plugin_registry=None, main_command_table_ops=None): + super().__init__() + self._init_trie = PrefixTrie() + # set of (module, fn_name) + self._initialized = set() + # number of entries not yet initialized + self._pending_count = 0 + # event_name -> list of entries from init trie + self._init_cache: dict[str, list] = {} + self._main_ops = main_command_table_ops + self._main_ops_applied = False + if plugin_registry: + self.load_registry(plugin_registry) + + def load_registry(self, registry): + unique = set() + for event_pattern, entries in registry.items(): + for entry in entries: + self._init_trie.append_item(event_pattern, entry) + if entry not in unique: + unique.add(entry) + self._pending_count += 1 + self._init_cache = {} + + @property + def initialized_count(self): + return len(self._initialized) + + def _apply_main_command_table_ops(self, kwargs): + """Apply pre-computed renames and LazyCommand additions. + + This replaces the normal lazy-init path for + building-command-table.main entries, avoiding the import of + heavy plugin modules until the command is actually invoked. + """ + command_table = kwargs.get('command_table') + session = kwargs.get('session') + if command_table is None or session is None: + return + + for op in self._main_ops: + if op[0] == CommandTableOp.RENAME: + _, old_name, new_name = op + if old_name in command_table: + current = command_table[old_name] + command_table[new_name] = current + current.name = new_name + del command_table[old_name] + elif op[0] == CommandTableOp.ADD: + _, cmd_name, cmd_module, cmd_class = op + command_table[cmd_name] = LazyCommand( + cmd_name, + session, + cmd_module, + cmd_class, + ) + else: + raise RuntimeError(f'Unknown command table ops entry: {op}') + + # Mark all building-command-table.main entries as initialized so + # _ensure_initialized never imports them. The ops list fully + # accounts for all plugins registered against this event. + for entry in PLUGIN_REGISTRY.get('building-command-table.main', []): + entry = tuple(entry) + if entry not in self._initialized: + self._initialized.add(entry) + self._pending_count -= 1 + + def _ensure_initialized(self, event_name): + """Initialize any plugins whose event patterns match event_name.""" + if self._pending_count == 0: + return + candidates = self._init_cache.get(event_name) + if candidates is None: + candidates = self._init_trie.prefix_search(event_name) + self._init_cache[event_name] = candidates + for entry in candidates: + if entry not in self._initialized: + self._initialized.add(entry) + self._pending_count -= 1 + module_path, fn_name = entry + logger.debug( + 'Lazy-initializing plugin %s.%s for event %s', + module_path, + fn_name, + event_name, + ) + mod = importlib.import_module(module_path) + fn = getattr(mod, fn_name) + fn(self) + + def _emit(self, event_name, kwargs, stop_on_response=False): + if ( + self._main_ops + and not self._main_ops_applied + and event_name == 'building-command-table.main' + ): + self._main_ops_applied = True + self._apply_main_command_table_ops(kwargs) + self._ensure_initialized(event_name) + return super()._emit(event_name, kwargs, stop_on_response) + + def __copy__(self): + new_instance = self.__class__() + new_state = self.__dict__.copy() + new_state['_handlers'] = copy.copy(self._handlers) + new_state['_unique_id_handlers'] = copy.copy(self._unique_id_handlers) + new_state['_init_trie'] = copy.copy(self._init_trie) + new_state['_initialized'] = copy.copy(self._initialized) + new_state['_main_ops_applied'] = self._main_ops_applied + new_instance.__dict__ = new_state + return new_instance diff --git a/awscli/paramfile.py b/awscli/paramfile.py index 975470062594..a14b3c56b3fa 100644 --- a/awscli/paramfile.py +++ b/awscli/paramfile.py @@ -24,6 +24,13 @@ class ResourceLoadingError(Exception): pass +def register_init_uri_param_handler(event_emitter): + event_emitter.register( + 'session-initialized', + register_uri_param_handler, + ) + + def register_uri_param_handler(session, **kwargs): prefix_map = copy.deepcopy(LOCAL_PREFIX_MAP) handler = URIArgumentHandler(prefix_map) diff --git a/awscli/plugin.py b/awscli/plugin.py index 46a26a4fc1a7..fe41651c3d4f 100644 --- a/awscli/plugin.py +++ b/awscli/plugin.py @@ -10,15 +10,19 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import importlib import logging import os import sys +from functools import singledispatch from botocore.hooks import HierarchicalEmitter +from awscli.handlers_registry import PLUGIN_REGISTRY +from awscli.lazy_emitter import LazyInitEmitter + log = logging.getLogger('awscli.plugin') -BUILTIN_PLUGINS = {'__builtin__': 'awscli.handlers'} CLI_LEGACY_PLUGIN_PATH = 'cli_legacy_plugin_path' @@ -31,21 +35,21 @@ def load_plugins(plugin_mapping, event_hooks=None, include_builtins=True): :type event_hooks: ``EventHooks`` :param event_hooks: Event hook emitter. If one if not provided, - an emitter will be created and returned. Otherwise, the + a LazyInitEmitter will be created and returned. Otherwise, the passed in ``event_hooks`` will be used to initialize plugins. :type include_builtins: bool - :param include_builtins: If True, the builtin awscli plugins (specified in - ``BUILTIN_PLUGINS``) will be included in the list of plugins to load. + :param include_builtins: If True, the built-in plugin registry will be + loaded into the emitter. - :rtype: HierarchicalEmitter + :rtype: LazyInitEmitter :return: An event emitter object. """ if event_hooks is None: - event_hooks = HierarchicalEmitter() + event_hooks = LazyInitEmitter() if include_builtins: - _load_plugins(BUILTIN_PLUGINS, event_hooks) + _load_registry(event_hooks) plugin_path = plugin_mapping.pop(CLI_LEGACY_PLUGIN_PATH, None) if plugin_path is not None: _add_plugin_path_to_sys_path(plugin_path) @@ -58,6 +62,32 @@ def load_plugins(plugin_mapping, event_hooks=None, include_builtins=True): return event_hooks +@singledispatch +def _load_registry(event_hooks): + raise NotImplementedError( + f'No _load_registry implementation for ' + f'{type(event_hooks).__name__}' + ) + + +@_load_registry.register +def _(event_hooks: HierarchicalEmitter): + seen = set() + for event_pattern, entries in PLUGIN_REGISTRY.items(): + for entry in entries: + if entry not in seen: + seen.add(entry) + module_path, fn_name = entry + mod = importlib.import_module(module_path) + fn = getattr(mod, fn_name) + fn(event_hooks) + + +@_load_registry.register +def _(event_hooks: LazyInitEmitter): + event_hooks.load_registry(PLUGIN_REGISTRY) + + def _load_plugins(plugin_mapping, event_hooks): modules = _import_plugins(plugin_mapping) for name, plugin in zip(plugin_mapping.keys(), modules): diff --git a/exe/pyinstaller/hook-awscli.py b/exe/pyinstaller/hook-awscli.py index a0767694019a..5d284fabeae6 100644 --- a/exe/pyinstaller/hook-awscli.py +++ b/exe/pyinstaller/hook-awscli.py @@ -2,6 +2,8 @@ from PyInstaller.utils import hooks +from awscli.handlers_registry import PLUGIN_REGISTRY + hiddenimports = [ 'docutils', 'urllib', @@ -28,6 +30,14 @@ ) + hooks.collect_submodules('awscli.s3transfer') hiddenimports += alias_packages_plugins +# plugin.py uses importlib.import_module at runtime to load customization +# modules, so PyInstaller cannot discover them statically. Collect all module +# paths referenced in handlers_registry.py as hidden imports. +registry_modules = { + entry[0] for entries in PLUGIN_REGISTRY.values() for entry in entries +} +hiddenimports += registry_modules + # Completion model files are only used at build time to generate the # ac.index SQLite database. They are not needed at runtime and can be diff --git a/tests/functional/autocomplete/test_server_index.py b/tests/functional/autocomplete/test_server_index.py index ea3a31508b0b..1d7369f32a32 100644 --- a/tests/functional/autocomplete/test_server_index.py +++ b/tests/functional/autocomplete/test_server_index.py @@ -25,7 +25,7 @@ def setUpClass(cls): ], ) driver = clidriver.create_clidriver() - driver.session.register( + driver.session.get_component('event_emitter').register_last( 'building-command-table.main', _ddb_only_command_table ) index_generator.generate_index(driver) @@ -111,7 +111,7 @@ def test_no_errors_when_missing_completion_data(self): # This will result in the loader not being able to find any # completion data, which allows us to verify the behavior when # there's no completion data. - driver.session.register( + driver.session.get_component('event_emitter').register_last( 'building-command-table.main', _ddb_only_command_table ) driver.session.register( diff --git a/tests/functional/autoprompt/test_prompttoolkit.py b/tests/functional/autoprompt/test_prompttoolkit.py index 37932c1be368..0ed5f9e2c4bf 100644 --- a/tests/functional/autoprompt/test_prompttoolkit.py +++ b/tests/functional/autoprompt/test_prompttoolkit.py @@ -44,7 +44,7 @@ def _generate_index_if_needed(db_connection): [indexer.ModelIndexer(db_connection)], ) driver = create_clidriver() - driver.session.register( + driver.session.get_component('event_emitter').register_last( 'building-command-table.main', _cloudwatch_only_command_table ) index_generator.generate_index(driver) diff --git a/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py new file mode 100644 index 000000000000..ec45094e44ac --- /dev/null +++ b/tests/functional/test_handlers_registry.py @@ -0,0 +1,103 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import importlib +from collections import OrderedDict + +import botocore.session + +from awscli.handlers_registry import ( + MAIN_COMMAND_TABLE_OPS, + PLUGIN_REGISTRY, + CommandTableOp, +) + + +class _AuditEmitter: + """Minimal emitter that records event names without side effects.""" + + def __init__(self): + self.registrations = [] + + def register(self, event_name, *args, **kwargs): + self.registrations.append(event_name) + + register_first = register + register_last = register + + +class _CallbackCollector: + """Emitter that captures callbacks registered against a specific event.""" + + def __init__(self, target_event): + self._target_event = target_event + self.callbacks = [] + + def register(self, event_name, handler, *args, **kwargs): + if event_name == self._target_event: + self.callbacks.append(handler) + + register_first = register + register_last = register + + +def test_all_registry_entries_are_importable(): + """Every (module, fn_name) in PLUGIN_REGISTRY must resolve to a + callable. This catches typos, stale entries, and missing modules. + """ + violations = [] + seen = set() + for entries in PLUGIN_REGISTRY.values(): + for module_path, fn_name in entries: + if (module_path, fn_name) in seen: + continue + seen.add((module_path, fn_name)) + try: + mod = importlib.import_module(module_path) + except ImportError as e: + violations.append(f'{module_path}: {e}') + continue + fn = getattr(mod, fn_name, None) + if fn is None: + violations.append(f'{module_path}.{fn_name} does not exist') + elif not callable(fn): + violations.append(f'{module_path}.{fn_name} is not callable') + assert not violations, ( + 'The following PLUGIN_REGISTRY entries are invalid:\n' + + '\n'.join(f' - {v}' for v in violations) + ) + + +def test_all_main_command_table_ops_modules_are_importable(): + """Every module referenced in MAIN_COMMAND_TABLE_OPS 'add' entries + must be importable and contain the specified class. + """ + violations = [] + for op in MAIN_COMMAND_TABLE_OPS: + if op[0] != CommandTableOp.ADD: + continue + _, cmd_name, cmd_module, cmd_class = op + try: + mod = importlib.import_module(cmd_module) + except ImportError as e: + violations.append(f'{cmd_module}: {e}') + continue + cls_name = cmd_class.split('.')[-1] + if not hasattr(mod, cls_name): + violations.append( + f'{cmd_module}.{cls_name} does not exist ' + f'(referenced by add {cmd_name!r})' + ) + assert not violations, ( + 'The following MAIN_COMMAND_TABLE_OPS entries are invalid:\n' + + '\n'.join(f' - {v}' for v in violations) + ) diff --git a/tests/functional/test_lazy.py b/tests/functional/test_lazy.py new file mode 100644 index 000000000000..75cba38e5283 --- /dev/null +++ b/tests/functional/test_lazy.py @@ -0,0 +1,89 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import pytest + +from awscli.handlers_registry import MAIN_COMMAND_TABLE_OPS, CommandTableOp +from awscli.lazy import LazyCommand +from awscli.testutils import BaseAWSHelpOutputTest, mock + +# Derive test parameters from MAIN_COMMAND_TABLE_OPS. +_ADD_OPS = [op for op in MAIN_COMMAND_TABLE_OPS if op[0] == CommandTableOp.ADD] +_RENAME_OPS = [ + op for op in MAIN_COMMAND_TABLE_OPS if op[0] == CommandTableOp.RENAME +] +_ADD_CMD_NAMES = [op[1] for op in _ADD_OPS] +_RENAME_NEW_NAMES = [op[2] for op in _RENAME_OPS] + + +class TestLazyCommandHelpRenders(BaseAWSHelpOutputTest): + def test_added_command_help_renders(self): + for cmd_name in _ADD_CMD_NAMES: + with self.subTest(cmd_name=cmd_name): + self.driver.main([cmd_name, 'help']) + self.assert_contains(cmd_name) + + def test_renamed_command_help_renders(self): + for new_name in _RENAME_NEW_NAMES: + with self.subTest(new_name=new_name): + self.driver.main([new_name, 'help']) + self.assert_contains(new_name) + + +class TestLazyCommandIsTransparent(BaseAWSHelpOutputTest): + def test_added_commands_appear_in_top_level_help(self): + self.driver.main(['help']) + for cmd_name in _ADD_CMD_NAMES: + self.assert_contains(cmd_name) + + def test_lazy_command_has_subcommands(self): + command_table = self.driver.subcommand_table + s3_cmd = command_table['s3'] + assert isinstance(s3_cmd, LazyCommand) + subcommands = s3_cmd.subcommand_table + assert 'ls' in subcommands + assert 'cp' in subcommands + + +class TestLazyCommandErrorPaths: + def test_invalid_module_path_raises_on_resolve(self): + session = mock.MagicMock() + cmd = LazyCommand( + 'bad-cmd', + session, + 'awscli.nonexistent.module', + 'FakeCommand', + ) + with pytest.raises(ModuleNotFoundError): + cmd([], mock.MagicMock()) + + def test_invalid_class_name_raises_on_resolve(self): + session = mock.MagicMock() + cmd = LazyCommand( + 'bad-cmd', + session, + 'awscli.customizations.dynamodb.ddb', + 'NonexistentClass', + ) + with pytest.raises(AttributeError): + cmd([], mock.MagicMock()) + + def test_invalid_module_path_raises_on_help(self): + session = mock.MagicMock() + cmd = LazyCommand( + 'bad-cmd', + session, + 'awscli.nonexistent.module', + 'FakeCommand', + ) + with pytest.raises(ModuleNotFoundError): + cmd.create_help_command() diff --git a/tests/unit/botocore/test_hooks.py b/tests/unit/botocore/test_hooks.py index 23d9b4985d6c..b55e98bd1e77 100644 --- a/tests/unit/botocore/test_hooks.py +++ b/tests/unit/botocore/test_hooks.py @@ -14,7 +14,12 @@ import functools from functools import partial -from botocore.hooks import HierarchicalEmitter, first_non_none_response +import pytest +from botocore.hooks import ( + HierarchicalEmitter, + PrefixTrie, + first_non_none_response, +) from tests import unittest @@ -584,5 +589,55 @@ def handler(a, b, **kwargs): ) +@pytest.fixture +def trie(): + return PrefixTrie() + + +class TestPrefixTrie: + def test_append_and_prefix_search_exact_match(self, trie): + trie.append_item('building-command-table.main', 'handler1') + results = trie.prefix_search('building-command-table.main') + assert 'handler1' in results + + def test_prefix_search_matches_parent(self, trie): + trie.append_item('building-command-table', 'handler1') + results = trie.prefix_search('building-command-table.main') + assert 'handler1' in results + + def test_prefix_search_does_not_match_sibling(self, trie): + trie.append_item('building-command-table.ecs', 'handler1') + results = trie.prefix_search('building-command-table.main') + assert 'handler1' not in results + + def test_prefix_search_does_not_match_child(self, trie): + trie.append_item('building-command-table.main.sub', 'handler1') + results = trie.prefix_search('building-command-table.main') + assert 'handler1' not in results + + def test_wildcard_match(self, trie): + trie.append_item('building-command-table.*', 'handler1') + results = trie.prefix_search('building-command-table.main') + assert 'handler1' in results + + def test_multiple_items_at_same_key(self, trie): + trie.append_item('building-command-table.main', 'handler1') + trie.append_item('building-command-table.main', 'handler2') + results = trie.prefix_search('building-command-table.main') + assert 'handler1' in results + assert 'handler2' in results + + def test_multiple_levels_all_returned(self, trie): + trie.append_item('building-command-table', 'parent') + trie.append_item('building-command-table.main', 'exact') + results = trie.prefix_search('building-command-table.main') + assert 'parent' in results + assert 'exact' in results + + def test_empty_trie_returns_empty(self, trie): + results = trie.prefix_search('building-command-table.main') + assert len(results) == 0 + + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/customizations/s3/test_s3.py b/tests/unit/customizations/s3/test_s3.py index 29f7fbf151b7..a8a4e4870178 100644 --- a/tests/unit/customizations/s3/test_s3.py +++ b/tests/unit/customizations/s3/test_s3.py @@ -10,7 +10,11 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from awscli.customizations.s3.s3 import add_s3, awscli_initialize +from awscli.customizations.s3.s3 import ( + add_s3, + register_s3_main, + register_s3_sync_strategies, +) from awscli.testutils import BaseAWSCommandParamsTest, mock, unittest @@ -24,7 +28,8 @@ def setUp(self): self.cli = mock.Mock() def test_initialize(self): - awscli_initialize(self.cli) + register_s3_main(self.cli) + register_s3_sync_strategies(self.cli) reference = [] reference.append("building-command-table.main") reference.append("building-command-table.s3_sync") diff --git a/tests/unit/test_clidriver.py b/tests/unit/test_clidriver.py index cd5315212728..bcc5a1ca7c8e 100644 --- a/tests/unit/test_clidriver.py +++ b/tests/unit/test_clidriver.py @@ -663,7 +663,10 @@ def test_custom_arg_paramfile(self, mock_handler): mock_handler.return_value = mock_paramfile driver = create_clidriver() - driver.session.register( + # We use register_last to ensure unknown-arg is added to the argument + # table after all plugin-added arguments, so that its load-cli-arg + # event fires last in call_args_list. + driver.session.get_component('event_emitter').register_last( 'building-argument-table', self.inject_new_param ) diff --git a/tests/unit/test_lazy.py b/tests/unit/test_lazy.py new file mode 100644 index 000000000000..63a1ff5ea44b --- /dev/null +++ b/tests/unit/test_lazy.py @@ -0,0 +1,139 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from unittest.mock import MagicMock, patch + +import pytest + +from awscli.lazy import LazyCommand + + +@pytest.fixture +def session(): + return MagicMock() + + +@pytest.fixture +def mock_command_class(): + cls = MagicMock() + instance = MagicMock() + cls.return_value = instance + return cls + + +@pytest.fixture +def mock_module(mock_command_class): + module = MagicMock() + module.MyCommand = mock_command_class + return module + + +class TestLazyCommandResolution: + def test_does_not_import_on_construction(self, session): + with patch('importlib.import_module') as imp: + LazyCommand('cmd', session, 'some.module', 'MyCommand') + imp.assert_not_called() + + def test_imports_module_on_call(self, session, mock_module): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + with patch('importlib.import_module', return_value=mock_module): + cmd(['arg1'], MagicMock()) + mock_module.MyCommand.assert_called_once_with(session) + + def test_imports_module_on_help(self, session, mock_module): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + with patch('importlib.import_module', return_value=mock_module): + cmd.create_help_command() + mock_module.MyCommand.assert_called_once_with(session) + + def test_imports_module_on_arg_table(self, session, mock_module): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + with patch('importlib.import_module', return_value=mock_module): + _ = cmd.arg_table + mock_module.MyCommand.assert_called_once_with(session) + + def test_imports_module_on_subcommand_table(self, session, mock_module): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + with patch('importlib.import_module', return_value=mock_module): + _ = cmd.subcommand_table + mock_module.MyCommand.assert_called_once_with(session) + + def test_resolves_only_once(self, session, mock_module): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + with patch('importlib.import_module', return_value=mock_module) as imp: + cmd(['arg1'], MagicMock()) + cmd(['arg2'], MagicMock()) + imp.assert_called_once_with('some.module') + + def test_delegates_call_to_real_command(self, session, mock_module): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + args = ['arg1'] + parsed_globals = MagicMock() + with patch('importlib.import_module', return_value=mock_module): + cmd(args, parsed_globals) + mock_module.MyCommand.return_value.assert_called_once_with( + args, parsed_globals + ) + + def test_delegates_help_to_real_command(self, session, mock_module): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + with patch('importlib.import_module', return_value=mock_module): + result = cmd.create_help_command() + assert ( + result == mock_module.MyCommand.return_value.create_help_command() + ) + + +class TestLazyCommandProperties: + def test_name_returns_initial_name(self, session): + cmd = LazyCommand('my-cmd', session, 'some.module', 'MyCommand') + assert cmd.name == 'my-cmd' + + def test_name_setter_updates_name(self, session): + cmd = LazyCommand('old-name', session, 'some.module', 'MyCommand') + cmd.name = 'new-name' + assert cmd.name == 'new-name' + + def test_lineage_defaults_to_self(self, session): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + assert cmd.lineage == [cmd] + + def test_lineage_setter_updates_lineage(self, session): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + new_lineage = [MagicMock(), cmd] + cmd.lineage = new_lineage + assert cmd.lineage == new_lineage + + def test_lineage_propagated_to_real_on_resolve(self, session, mock_module): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + new_lineage = [MagicMock(), cmd] + cmd.lineage = new_lineage + with patch('importlib.import_module', return_value=mock_module): + cmd.create_help_command() + assert mock_module.MyCommand.return_value.lineage == new_lineage + + def test_lineage_setter_propagates_to_already_resolved( + self, session, mock_module + ): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + with patch('importlib.import_module', return_value=mock_module): + cmd.create_help_command() + new_lineage = [MagicMock(), cmd] + cmd.lineage = new_lineage + assert mock_module.MyCommand.return_value.lineage == new_lineage + + def test_lineage_not_propagated_if_not_resolved(self, session): + cmd = LazyCommand('cmd', session, 'some.module', 'MyCommand') + new_lineage = [MagicMock(), cmd] + # Should not raise even though underlying command is not resolved. + cmd.lineage = new_lineage + assert cmd.lineage == new_lineage diff --git a/tests/unit/test_lazy_emitter.py b/tests/unit/test_lazy_emitter.py new file mode 100644 index 000000000000..3ecac7320cac --- /dev/null +++ b/tests/unit/test_lazy_emitter.py @@ -0,0 +1,250 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from unittest.mock import MagicMock, patch + +import pytest + +from awscli.handlers_registry import CommandTableOp +from awscli.lazy import LazyCommand +from awscli.lazy_emitter import LazyInitEmitter + + +@pytest.fixture +def mock_module(): + """Create a mock module with a callable init function.""" + module = MagicMock() + module.my_init = MagicMock() + return module + + +class TestLazyInitEmitterPrefixMatching: + def test_bare_prefix_entry_initialized_on_dotted_emit(self, mock_module): + # Entry registered against 'building-command-table' (bare prefix) + # should be initialized when 'building-command-table.main' is emitted. + registry = { + 'building-command-table': [ + ('test.module', 'my_init'), + ], + } + emitter = LazyInitEmitter() + emitter.load_registry(registry) + + with patch('importlib.import_module', return_value=mock_module): + emitter.emit('building-command-table.main', command_table={}) + + assert emitter.initialized_count == 1 + mock_module.my_init.assert_called_once() + + def test_exact_match_entry_initialized(self, mock_module): + registry = { + 'building-command-table.main': [ + ('test.module', 'my_init'), + ], + } + emitter = LazyInitEmitter() + emitter.load_registry(registry) + + with patch('importlib.import_module', return_value=mock_module): + emitter.emit('building-command-table.main', command_table={}) + + assert emitter.initialized_count == 1 + mock_module.my_init.assert_called_once() + + def test_unrelated_entry_not_initialized(self, mock_module): + # Entry registered against 'building-command-table.ecs' should NOT + # be initialized when 'building-command-table.main' is emitted. + registry = { + 'building-command-table.ecs': [ + ('test.module', 'my_init'), + ], + } + emitter = LazyInitEmitter() + emitter.load_registry(registry) + + with patch('importlib.import_module', return_value=mock_module): + emitter.emit('building-command-table.main', command_table={}) + + assert emitter.initialized_count == 0 + mock_module.my_init.assert_not_called() + + def test_multiple_prefix_levels_all_initialized(self, mock_module): + # Both 'building-command-table' and 'building-command-table.main' + # entries should be initialized when 'building-command-table.main' + # is emitted. + mock_module.other_init = MagicMock() + registry = { + 'building-command-table': [ + ('test.module', 'my_init'), + ], + 'building-command-table.main': [ + ('test.module', 'other_init'), + ], + } + emitter = LazyInitEmitter() + emitter.load_registry(registry) + + with patch('importlib.import_module', return_value=mock_module): + emitter.emit('building-command-table.main', command_table={}) + + assert emitter.initialized_count == 2 + mock_module.my_init.assert_called_once() + mock_module.other_init.assert_called_once() + + def test_entry_initialized_only_once(self, mock_module): + registry = { + 'building-command-table': [ + ('test.module', 'my_init'), + ], + } + emitter = LazyInitEmitter() + emitter.load_registry(registry) + + with patch('importlib.import_module', return_value=mock_module): + emitter.emit('building-command-table.main', command_table={}) + emitter.emit('building-command-table.ecs', command_table={}) + + assert emitter.initialized_count == 1 + mock_module.my_init.assert_called_once() + + +class TestMainCommandTableOps: + def test_covered_plugins_not_imported(self, mock_module): + registry = { + 'building-command-table.main': [ + ('heavy.module', 'register_add_cmd'), + ], + } + main_ops = [ + (CommandTableOp.ADD, 'my-cmd', 'heavy.module', 'MyCommand'), + ] + emitter = LazyInitEmitter(main_command_table_ops=main_ops) + emitter.load_registry(registry) + + command_table = {'existing': MagicMock(name='existing')} + session = MagicMock() + + with ( + patch('awscli.lazy_emitter.PLUGIN_REGISTRY', registry), + patch('importlib.import_module', return_value=mock_module) as imp, + ): + emitter.emit( + 'building-command-table.main', + command_table=command_table, + session=session, + ) + + # The heavy module should NOT have been imported via _ensure_initialized + imp.assert_not_called() + # But the entry should be marked as initialized + assert emitter.initialized_count == 1 + + def test_rename_op_applied_to_command_table(self): + registry = { + 'building-command-table.main': [ + ('rename.module', 'register_rename'), + ], + } + main_ops = [ + (CommandTableOp.RENAME, 'old-name', 'new-name'), + ] + emitter = LazyInitEmitter(main_command_table_ops=main_ops) + emitter.load_registry(registry) + + cmd = MagicMock() + cmd.name = 'old-name' + command_table = {'old-name': cmd} + session = MagicMock() + + with patch('awscli.lazy_emitter.PLUGIN_REGISTRY', registry): + emitter.emit( + 'building-command-table.main', + command_table=command_table, + session=session, + ) + + assert 'old-name' not in command_table + assert 'new-name' in command_table + assert command_table['new-name'] is cmd + assert cmd.name == 'new-name' + + def test_add_op_creates_lazy_command(self): + registry = { + 'building-command-table.main': [ + ('add.module', 'register_add'), + ], + } + main_ops = [ + (CommandTableOp.ADD, 'my-cmd', 'add.module.impl', 'MyCommand'), + ] + emitter = LazyInitEmitter(main_command_table_ops=main_ops) + emitter.load_registry(registry) + + command_table = {} + session = MagicMock() + + with patch('awscli.lazy_emitter.PLUGIN_REGISTRY', registry): + emitter.emit( + 'building-command-table.main', + command_table=command_table, + session=session, + ) + + assert 'my-cmd' in command_table + assert isinstance(command_table['my-cmd'], LazyCommand) + assert command_table['my-cmd'].name == 'my-cmd' + + def test_main_ops_skips_covered_but_initializes_bare_prefix( + self, mock_module + ): + # Entries registered against bare 'building-command-table' must + # still be initialized even when main_ops are applied. + registry = { + 'building-command-table': [ + ('global.module', 'register_global'), + ], + 'building-command-table.main': [ + ('heavy.module', 'register_add_cmd'), + ], + } + main_ops = [ + (CommandTableOp.ADD, 'my-cmd', 'heavy.module', 'MyCommand'), + ] + emitter = LazyInitEmitter(main_command_table_ops=main_ops) + emitter.load_registry(registry) + + command_table = {} + session = MagicMock() + + mock_global = MagicMock() + mock_global.register_global = MagicMock() + + def import_side_effect(mod_path): + # Ensures that no module besides global.module + # (including heavy.module) are imported. heavy.module should not be + # imported because it is present in main_command_table_ops. + if mod_path == 'global.module': + return mock_global + raise AssertionError(f'Unexpected import of {mod_path!r}') + + with ( + patch('awscli.lazy_emitter.PLUGIN_REGISTRY', registry), + patch('importlib.import_module', side_effect=import_side_effect), + ): + emitter.emit( + 'building-command-table.main', + command_table=command_table, + session=session, + ) + + mock_global.register_global.assert_called_once() + assert emitter.initialized_count == 2 diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index 1d1e8957463d..3a31f53e258f 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -54,7 +54,7 @@ def test_plugin_register(self): ) def test_event_hooks_can_be_passed_in(self): - hooks = plugin.HierarchicalEmitter() + hooks = plugin.LazyInitEmitter() emitter = plugin.load_plugins(self.plugin_mapping, event_hooks=hooks) emitter.emit('before_operation') self.assertEqual(len(self.fake_module.events_seen), 1)