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/clidriver.py b/awscli/clidriver.py index f7ba957fd94f..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', {}), diff --git a/awscli/handlers.py b/awscli/handlers.py deleted file mode 100644 index 2dab06ba171a..000000000000 --- a/awscli/handlers.py +++ /dev/null @@ -1,234 +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 register_param_shorthand_parser -from awscli.clidriver import register_no_pager_handler -from awscli.customizations import datapipeline -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 register_init_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 ( - 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 ( - 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 ( - 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 -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_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 ( - 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 ( - 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, - 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 ( - 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 ( - 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_init_uri_param_handler - - -def awscli_initialize(event_handlers): - 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) - register_docs_add_examples(event_handlers) - register_cli_input_args(event_handlers) - register_streaming_output_arg(event_handlers) - register_count_events(event_handlers) - 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) - register_s3_main(event_handlers) - register_s3_sync_strategies(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_codedeploy(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) - 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) - 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/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 index 297a001ce838..ec45094e44ac 100644 --- a/tests/functional/test_handlers_registry.py +++ b/tests/functional/test_handlers_registry.py @@ -101,112 +101,3 @@ def test_all_main_command_table_ops_modules_are_importable(): '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/functional/test_lazy.py b/tests/functional/test_lazy.py index fc8d33edd359..75cba38e5283 100644 --- a/tests/functional/test_lazy.py +++ b/tests/functional/test_lazy.py @@ -12,8 +12,46 @@ # 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 mock +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: 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_emitter.py b/tests/unit/test_lazy_emitter.py index 281b6d38a6d7..3ecac7320cac 100644 --- a/tests/unit/test_lazy_emitter.py +++ b/tests/unit/test_lazy_emitter.py @@ -29,6 +29,8 @@ def mock_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'), @@ -59,6 +61,8 @@ def test_exact_match_entry_initialized(self, mock_module): 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'), @@ -74,6 +78,9 @@ def test_unrelated_entry_not_initialized(self, mock_module): 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': [ @@ -136,7 +143,9 @@ def test_covered_plugins_not_imported(self, mock_module): 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): @@ -197,6 +206,8 @@ def test_add_op_creates_lazy_command(self): 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'), @@ -218,6 +229,9 @@ def test_main_ops_skips_covered_but_initializes_bare_prefix( 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}') 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)