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/clidriver.py b/awscli/clidriver.py index 643331c50d46..f7ba957fd94f 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -214,6 +214,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 index 3f0aa0180078..2dab06ba171a 100644 --- a/awscli/handlers.py +++ b/awscli/handlers.py @@ -18,14 +18,14 @@ """ from awscli.alias import register_alias_commands -from awscli.argprocess import ParamShorthandParser -from awscli.clidriver import no_pager_handler +from awscli.argprocess import register_param_shorthand_parser +from awscli.clidriver import register_no_pager_handler from awscli.customizations import datapipeline -from awscli.customizations.addexamples import add_examples +from awscli.customizations.addexamples import register_docs_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.binaryformat import register_init_binary_formatter from awscli.customizations.cliinput import register_cli_input_args from awscli.customizations.cloudformation import ( initialize as cloudformation_init, @@ -38,7 +38,10 @@ 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, + register_codedeploy, +) +from awscli.customizations.codedeploy.codedeploy import ( + register_rename_codedeploy as codedeploy_init, ) from awscli.customizations.configservice.getstatus import register_get_status from awscli.customizations.configservice.putconfigurationrecorder import ( @@ -58,7 +61,9 @@ ) 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.decryptpassword import ( + register_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 @@ -88,8 +93,8 @@ ) from awscli.customizations.iamvirtmfa import IAMVMFAWrapper from awscli.customizations.iot import ( - register_create_keys_and_cert_arguments, - register_create_keys_from_csr_arguments, + register_iot_create_keys_and_cert_args, + register_iot_create_keys_from_csr, ) from awscli.customizations.iot_data import register_custom_endpoint_note from awscli.customizations.kinesis import ( @@ -113,7 +118,10 @@ ) 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.s3.s3 import ( + register_s3_main, + register_s3_sync_strategies, +) from awscli.customizations.s3errormsg import register_s3_error_msg from awscli.customizations.s3events import ( register_document_expires_string, @@ -125,7 +133,9 @@ 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.streamingoutputarg import ( + register_streaming_output_arg, +) from awscli.customizations.timestampformat import register_timestamp_format from awscli.customizations.toplevelbool import register_bool_params from awscli.customizations.translate import ( @@ -133,42 +143,28 @@ ) 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 +from awscli.paramfile import register_init_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 + register_init_uri_param_handler(event_handlers) + register_init_binary_formatter(event_handlers) + register_no_pager_handler(event_handlers) + register_param_shorthand_parser(event_handlers) + # The s3 error message 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_docs_add_examples(event_handlers) register_cli_input_args(event_handlers) - event_handlers.register( - 'building-argument-table.*', add_streaming_output_arg - ) + register_streaming_output_arg(event_handlers) register_count_events(event_handlers) - event_handlers.register( - 'building-argument-table.ec2.get-password-data', - ec2_add_priv_launch_key, - ) + register_ec2_add_priv_launch_key(event_handlers) register_parse_global_args(event_handlers) register_pagination(event_handlers) register_secgroup(event_handlers) register_bundleinstance(event_handlers) - s3_plugin_initialize(event_handlers) + register_s3_main(event_handlers) + register_s3_sync_strategies(event_handlers) register_ddb(event_handlers) register_runinstances(event_handlers) register_removals(event_handlers) @@ -199,6 +195,7 @@ def awscli_initialize(event_handlers): register_assume_role_provider(event_handlers) register_add_waiters(event_handlers) codedeploy_init(event_handlers) + register_codedeploy(event_handlers) register_subscribe(event_handlers) register_get_status(event_handlers) register_rename_config(event_handlers) @@ -210,14 +207,8 @@ def awscli_initialize(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_iot_create_keys_and_cert_args(event_handlers) + register_iot_create_keys_from_csr(event_handlers) register_cloudfront(event_handlers) register_gamelift_commands(event_handlers) register_ec2_page_size_injector(event_handlers) diff --git a/awscli/handlers_registry.py b/awscli/handlers_registry.py new file mode 100644 index 000000000000..0aa2bef59223 --- /dev/null +++ b/awscli/handlers_registry.py @@ -0,0 +1,855 @@ +# 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) +""" + +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: +# ('rename', old_name, new_name) +# ('add', cmd_name, cmd_module, cmd_class) + +MAIN_COMMAND_TABLE_OPS = [ + ('rename', 's3', 's3api'), + ('add', 's3', 'awscli.customizations.s3.s3', 'S3'), + ('add', 'ddb', 'awscli.customizations.dynamodb.ddb', 'DDB'), + ( + 'add', + 'configure', + 'awscli.customizations.configure.configure', + 'ConfigureCommand', + ), + ('rename', 'codedeploy', 'deploy'), + ('rename', 'config', 'configservice'), + ('add', 'history', 'awscli.customizations.history', 'HistoryCommand'), + ('add', 'cli-dev', 'awscli.customizations.devcommands', 'CLIDevCommand'), + ('add', 'login', 'awscli.customizations.login.login', 'LoginCommand'), + ('add', 'logout', 'awscli.customizations.login.logout', 'LogoutCommand'), +] 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/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py new file mode 100644 index 000000000000..93d0e36f4854 --- /dev/null +++ b/tests/functional/test_handlers_registry.py @@ -0,0 +1,208 @@ +# 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 + + +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] != '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) + ) + + +def test_main_command_table_plugins_only_register_against_main(): + """Plugins listed under building-command-table.main must not register + against any other events. + + This invariant allows the lazy-loading system to skip importing these + plugin modules entirely and instead apply pre-computed renames and + LazyCommand additions from MAIN_COMMAND_TABLE_OPS. If a plugin + mixes building-command-table.main registrations with other events, + split it into separate functions: one that only registers against + building-command-table.main, and another for the remaining events. + """ + main_entries = PLUGIN_REGISTRY.get('building-command-table.main', []) + violations = [] + for module_path, fn_name in main_entries: + emitter = _AuditEmitter() + mod = importlib.import_module(module_path) + fn = getattr(mod, fn_name) + fn(emitter) + non_main = [ + e + for e in emitter.registrations + if e != 'building-command-table.main' + ] + if non_main: + violations.append( + f'{module_path}.{fn_name} also registers against: ' + f'{non_main}' + ) + assert not violations, ( + 'The following building-command-table.main plugins register ' + 'against additional events. Split each into separate functions ' + 'so that the building-command-table.main function only registers ' + 'against that single event:\n' + + '\n'.join(f' - {v}' for v in violations) + ) + + +def test_main_command_table_callbacks_only_add_or_rename(): + """Callbacks registered against building-command-table.main must only + add new commands or rename existing ones. + + MAIN_COMMAND_TABLE_OPS replaces these callbacks at runtime with + LazyCommand additions and direct renames. If a callback also + modifies existing command table entries (e.g. changes properties on + a command object), that modification would be silently lost. + """ + session = botocore.session.Session() + services = session.get_available_services() + + main_entries = PLUGIN_REGISTRY.get('building-command-table.main', []) + violations = [] + + for module_path, fn_name in main_entries: + collector = _CallbackCollector('building-command-table.main') + mod = importlib.import_module(module_path) + fn = getattr(mod, fn_name) + fn(collector) + + for callback in collector.callbacks: + cb_name = f'{callback.__module__}.{callback.__qualname__}' + + # Build a fresh command table for each callback. + class _Placeholder: + def __init__(self, name): + self.name = name + + command_table = OrderedDict() + for svc in services: + command_table[svc] = _Placeholder(svc) + + snap_id_to_key = {id(v): k for k, v in command_table.items()} + snap_id_to_name = {id(v): v.name for k, v in command_table.items()} + + callback(command_table=command_table, session=session) + + # Classify every change. + new_id_to_key = {id(v): k for k, v in command_table.items()} + renamed_ids = set() + + # Detect renames. + for obj_id, new_key in new_id_to_key.items(): + old_key = snap_id_to_key.get(obj_id) + if old_key is not None and old_key != new_key: + renamed_ids.add(obj_id) + + # Detect modifications: an existing (non-renamed) entry whose + # .name property changed, or any entry that was removed. + for obj_id, old_key in snap_id_to_key.items(): + if obj_id in renamed_ids: + continue + if obj_id not in new_id_to_key: + violations.append(f'{cb_name} removed command {old_key!r}') + continue + new_key = new_id_to_key[obj_id] + cmd = command_table[new_key] + if cmd.name != snap_id_to_name[obj_id]: + violations.append( + f'{cb_name} modified .name on {new_key!r} ' + f'without renaming' + ) + + assert not violations, ( + 'Callbacks registered against building-command-table.main must ' + 'only add or rename commands. The following callbacks perform ' + 'other modifications that would be lost when replaced by ' + 'MAIN_COMMAND_TABLE_OPS:\n' + '\n'.join(f' - {v}' for v in violations) + ) 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")