From ddf31b514d291d490963634f183d2b625bccba4b Mon Sep 17 00:00:00 2001
From: aemous
Date: Mon, 11 May 2026 13:30:44 -0400
Subject: [PATCH 01/21] Make PrefixTrie public and implement LazyCommand class
to defer loading plugins until commands are invoked.
---
awscli/botocore/hooks.py | 4 +-
awscli/lazy.py | 60 +++++++++++++
tests/functional/test_lazy.py | 87 +++++++++++++++++++
tests/unit/botocore/test_hooks.py | 54 +++++++++++-
tests/unit/test_lazy.py | 139 ++++++++++++++++++++++++++++++
5 files changed, 341 insertions(+), 3 deletions(-)
create mode 100644 awscli/lazy.py
create mode 100644 tests/functional/test_lazy.py
create mode 100644 tests/unit/test_lazy.py
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/lazy.py b/awscli/lazy.py
new file mode 100644
index 000000000000..4cad0964443d
--- /dev/null
+++ b/awscli/lazy.py
@@ -0,0 +1,60 @@
+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/tests/functional/test_lazy.py b/tests/functional/test_lazy.py
new file mode 100644
index 000000000000..3ba067e13aad
--- /dev/null
+++ b/tests/functional/test_lazy.py
@@ -0,0 +1,87 @@
+# 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
+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] == 'add']
+_RENAME_OPS = [op for op in MAIN_COMMAND_TABLE_OPS if op[0] == '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..4defdd5511dc 100644
--- a/tests/unit/botocore/test_hooks.py
+++ b/tests/unit/botocore/test_hooks.py
@@ -14,7 +14,11 @@
import functools
from functools import partial
-from botocore.hooks import HierarchicalEmitter, first_non_none_response
+from botocore.hooks import (
+ HierarchicalEmitter,
+ PrefixTrie,
+ first_non_none_response,
+)
from tests import unittest
@@ -584,5 +588,53 @@ def handler(a, b, **kwargs):
)
+class TestPrefixTrie(unittest.TestCase):
+ def setUp(self):
+ self.trie = PrefixTrie()
+
+ def test_append_and_prefix_search_exact_match(self):
+ self.trie.append_item('building-command-table.main', 'handler1')
+ results = self.trie.prefix_search('building-command-table.main')
+ self.assertIn('handler1', results)
+
+ def test_prefix_search_matches_parent(self):
+ self.trie.append_item('building-command-table', 'handler1')
+ results = self.trie.prefix_search('building-command-table.main')
+ self.assertIn('handler1', results)
+
+ def test_prefix_search_does_not_match_sibling(self):
+ self.trie.append_item('building-command-table.ecs', 'handler1')
+ results = self.trie.prefix_search('building-command-table.main')
+ self.assertNotIn('handler1', results)
+
+ def test_prefix_search_does_not_match_child(self):
+ self.trie.append_item('building-command-table.main.sub', 'handler1')
+ results = self.trie.prefix_search('building-command-table.main')
+ self.assertNotIn('handler1', results)
+
+ def test_wildcard_match(self):
+ self.trie.append_item('building-command-table.*', 'handler1')
+ results = self.trie.prefix_search('building-command-table.main')
+ self.assertIn('handler1', results)
+
+ def test_multiple_items_at_same_key(self):
+ self.trie.append_item('building-command-table.main', 'handler1')
+ self.trie.append_item('building-command-table.main', 'handler2')
+ results = self.trie.prefix_search('building-command-table.main')
+ self.assertIn('handler1', results)
+ self.assertIn('handler2', results)
+
+ def test_multiple_levels_all_returned(self):
+ self.trie.append_item('building-command-table', 'parent')
+ self.trie.append_item('building-command-table.main', 'exact')
+ results = self.trie.prefix_search('building-command-table.main')
+ self.assertIn('parent', results)
+ self.assertIn('exact', results)
+
+ def test_empty_trie_returns_empty(self):
+ results = self.trie.prefix_search('building-command-table.main')
+ self.assertEqual(len(results), 0)
+
+
if __name__ == '__main__':
unittest.main()
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
From dab19fb02ba42ff07547dab9f07bb1f58f9389d7 Mon Sep 17 00:00:00 2001
From: aemous
Date: Mon, 11 May 2026 13:56:34 -0400
Subject: [PATCH 02/21] Remove parts of functional/test_lazy.py that depend on
LazyInitEmitter.
---
tests/functional/test_lazy.py | 38 +----------------------------------
1 file changed, 1 insertion(+), 37 deletions(-)
diff --git a/tests/functional/test_lazy.py b/tests/functional/test_lazy.py
index 3ba067e13aad..fc8d33edd359 100644
--- a/tests/functional/test_lazy.py
+++ b/tests/functional/test_lazy.py
@@ -12,44 +12,8 @@
# language governing permissions and limitations under the License.
import pytest
-from awscli.handlers_registry import MAIN_COMMAND_TABLE_OPS
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] == 'add']
-_RENAME_OPS = [op for op in MAIN_COMMAND_TABLE_OPS if op[0] == '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
+from awscli.testutils import mock
class TestLazyCommandErrorPaths:
From 494bd6a047a10694478ef663ba9a1ce720ebf156 Mon Sep 17 00:00:00 2001
From: aemous
Date: Mon, 11 May 2026 14:04:28 -0400
Subject: [PATCH 03/21] Add license header to awscli/lazy.py.
---
awscli/lazy.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/awscli/lazy.py b/awscli/lazy.py
index 4cad0964443d..58eccd7df515 100644
--- a/awscli/lazy.py
+++ b/awscli/lazy.py
@@ -1,3 +1,15 @@
+# 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
From 40f4027c699a18d34f28f3d6d0f7bb27090e4dc0 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 13 May 2026 11:18:59 -0400
Subject: [PATCH 04/21] Switch new test suite to use PyTest instead of
unittest.TestCase.
---
tests/unit/botocore/test_hooks.py | 96 ++++++++++++++++---------------
1 file changed, 50 insertions(+), 46 deletions(-)
diff --git a/tests/unit/botocore/test_hooks.py b/tests/unit/botocore/test_hooks.py
index 4defdd5511dc..cb7e1d738131 100644
--- a/tests/unit/botocore/test_hooks.py
+++ b/tests/unit/botocore/test_hooks.py
@@ -14,6 +14,8 @@
import functools
from functools import partial
+import pytest
+
from botocore.hooks import (
HierarchicalEmitter,
PrefixTrie,
@@ -588,52 +590,54 @@ def handler(a, b, **kwargs):
)
-class TestPrefixTrie(unittest.TestCase):
- def setUp(self):
- self.trie = PrefixTrie()
-
- def test_append_and_prefix_search_exact_match(self):
- self.trie.append_item('building-command-table.main', 'handler1')
- results = self.trie.prefix_search('building-command-table.main')
- self.assertIn('handler1', results)
-
- def test_prefix_search_matches_parent(self):
- self.trie.append_item('building-command-table', 'handler1')
- results = self.trie.prefix_search('building-command-table.main')
- self.assertIn('handler1', results)
-
- def test_prefix_search_does_not_match_sibling(self):
- self.trie.append_item('building-command-table.ecs', 'handler1')
- results = self.trie.prefix_search('building-command-table.main')
- self.assertNotIn('handler1', results)
-
- def test_prefix_search_does_not_match_child(self):
- self.trie.append_item('building-command-table.main.sub', 'handler1')
- results = self.trie.prefix_search('building-command-table.main')
- self.assertNotIn('handler1', results)
-
- def test_wildcard_match(self):
- self.trie.append_item('building-command-table.*', 'handler1')
- results = self.trie.prefix_search('building-command-table.main')
- self.assertIn('handler1', results)
-
- def test_multiple_items_at_same_key(self):
- self.trie.append_item('building-command-table.main', 'handler1')
- self.trie.append_item('building-command-table.main', 'handler2')
- results = self.trie.prefix_search('building-command-table.main')
- self.assertIn('handler1', results)
- self.assertIn('handler2', results)
-
- def test_multiple_levels_all_returned(self):
- self.trie.append_item('building-command-table', 'parent')
- self.trie.append_item('building-command-table.main', 'exact')
- results = self.trie.prefix_search('building-command-table.main')
- self.assertIn('parent', results)
- self.assertIn('exact', results)
-
- def test_empty_trie_returns_empty(self):
- results = self.trie.prefix_search('building-command-table.main')
- self.assertEqual(len(results), 0)
+@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__':
From fdddde11812c7f6e9531a0b81c10b5d328d1b04f Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 13 May 2026 11:19:36 -0400
Subject: [PATCH 05/21] Formatting.
---
tests/unit/botocore/test_hooks.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/unit/botocore/test_hooks.py b/tests/unit/botocore/test_hooks.py
index cb7e1d738131..b55e98bd1e77 100644
--- a/tests/unit/botocore/test_hooks.py
+++ b/tests/unit/botocore/test_hooks.py
@@ -15,7 +15,6 @@
from functools import partial
import pytest
-
from botocore.hooks import (
HierarchicalEmitter,
PrefixTrie,
From c8b4b7b671c18afd5daf46f03ee3ae0b497ad6ee Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 13 May 2026 14:06:37 -0400
Subject: [PATCH 06/21] Wrap plugins that directly register against events in
handlers.py in a thin initialization function.
---
awscli/argprocess.py | 7 +
awscli/clidriver.py | 7 +
awscli/customizations/addexamples.py | 14 ++
awscli/customizations/binaryformat.py | 7 +
.../customizations/codedeploy/codedeploy.py | 8 +-
awscli/customizations/ec2/decryptpassword.py | 7 +
awscli/customizations/iot.py | 14 ++
awscli/customizations/s3/s3.py | 21 +--
awscli/customizations/streamingoutputarg.py | 7 +
awscli/handlers.py | 73 ++++-----
awscli/paramfile.py | 7 +
tests/functional/test_handlers_registry.py | 155 ++++++++++++++++++
tests/unit/customizations/s3/test_s3.py | 9 +-
13 files changed, 272 insertions(+), 64 deletions(-)
create mode 100644 tests/functional/test_handlers_registry.py
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..9ccf670ec6e8 100644
--- a/awscli/customizations/addexamples.py
+++ b/awscli/customizations/addexamples.py
@@ -33,6 +33,20 @@
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..bc5234a4a7f9 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,8 @@
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,
+ register_rename_codedeploy as codedeploy_init,
)
from awscli.customizations.configservice.getstatus import register_get_status
from awscli.customizations.configservice.putconfigurationrecorder import (
@@ -58,7 +59,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 +91,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 +116,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 +131,7 @@
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 +139,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 +191,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 +203,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/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..7856fa3caf91
--- /dev/null
+++ b/tests/functional/test_handlers_registry.py
@@ -0,0 +1,155 @@
+# 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 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_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, entry_type 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, entry_type 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")
From 975f57a9aa1b7f7d2ab1bb8e54e8dedb2db486fb Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 13 May 2026 14:24:34 -0400
Subject: [PATCH 07/21] Add handlers_registry.py which maps events to plugins
and declares command-table operations.
---
awscli/handlers_registry.py | 738 ++++++++++++++++++++++++++++++++++++
1 file changed, 738 insertions(+)
create mode 100644 awscli/handlers_registry.py
diff --git a/awscli/handlers_registry.py b/awscli/handlers_registry.py
new file mode 100644
index 000000000000..c20c40183f72
--- /dev/null
+++ b/awscli/handlers_registry.py
@@ -0,0 +1,738 @@
+# 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'),
+]
From 75d5343c8bc95c40d2fdd3824fadeb4247290c41 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 13 May 2026 14:25:16 -0400
Subject: [PATCH 08/21] Formatting.
---
awscli/customizations/addexamples.py | 1 -
awscli/handlers.py | 10 +-
awscli/handlers_registry.py | 217 +++++++++++++++++++++------
3 files changed, 174 insertions(+), 54 deletions(-)
diff --git a/awscli/customizations/addexamples.py b/awscli/customizations/addexamples.py
index 9ccf670ec6e8..5e91cc547aed 100644
--- a/awscli/customizations/addexamples.py
+++ b/awscli/customizations/addexamples.py
@@ -34,7 +34,6 @@
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
diff --git a/awscli/handlers.py b/awscli/handlers.py
index bc5234a4a7f9..2dab06ba171a 100644
--- a/awscli/handlers.py
+++ b/awscli/handlers.py
@@ -39,6 +39,8 @@
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
@@ -60,7 +62,7 @@
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
+ 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
@@ -118,7 +120,7 @@
from awscli.customizations.route53 import register_create_hosted_zone_doc_fix
from awscli.customizations.s3.s3 import (
register_s3_main,
- register_s3_sync_strategies
+ register_s3_sync_strategies,
)
from awscli.customizations.s3errormsg import register_s3_error_msg
from awscli.customizations.s3events import (
@@ -131,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 register_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 (
diff --git a/awscli/handlers_registry.py b/awscli/handlers_registry.py
index c20c40183f72..0aa2bef59223 100644
--- a/awscli/handlers_registry.py
+++ b/awscli/handlers_registry.py
@@ -20,18 +20,28 @@
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')
+ (
+ 'awscli.customizations.ecs.monitormutatinggatewayservice',
+ 'register_monitor_mutating_gateway_service',
+ )
],
'after-call.ecs.DeleteExpressGatewayService': [
- ('awscli.customizations.ecs.monitormutatinggatewayservice', 'register_monitor_mutating_gateway_service')
+ (
+ 'awscli.customizations.ecs.monitormutatinggatewayservice',
+ 'register_monitor_mutating_gateway_service',
+ )
],
'after-call.ecs.UpdateExpressGatewayService': [
- ('awscli.customizations.ecs.monitormutatinggatewayservice', 'register_monitor_mutating_gateway_service')
+ (
+ 'awscli.customizations.ecs.monitormutatinggatewayservice',
+ 'register_monitor_mutating_gateway_service',
+ )
],
'after-call.iam.CreateVirtualMFADevice': [
('awscli.customizations.iamvirtmfa', 'IAMVMFAWrapper')
@@ -40,13 +50,22 @@
('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')
+ (
+ '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')
+ (
+ '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')
+ (
+ 'awscli.customizations.ecs.monitormutatinggatewayservice',
+ 'register_monitor_mutating_gateway_service',
+ )
],
'before-building-argument-table-parser.emr.*': [
('awscli.customizations.emr.emr', 'emr_initialize')
@@ -62,15 +81,21 @@
],
'before-parameter-build.ec2.RunInstances': [
('awscli.customizations.ec2.addcount', 'register_count_events'),
- ('awscli.customizations.ec2.runinstances', 'register_runinstances')
+ ('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')
+ (
+ 'awscli.customizations.generatecliskeleton',
+ 'register_generate_cli_skeleton',
+ ),
],
'building-argument-table.*': [
- ('awscli.customizations.streamingoutputarg', 'register_streaming_output_arg')
+ (
+ 'awscli.customizations.streamingoutputarg',
+ 'register_streaming_output_arg',
+ )
],
'building-argument-table.apigateway.create-rest-api': [
('awscli.customizations.argrename', 'register_arg_renames')
@@ -121,7 +146,10 @@
('awscli.customizations.argrename', 'register_arg_renames')
],
'building-argument-table.configservice.put-configuration-recorder': [
- ('awscli.customizations.configservice.putconfigurationrecorder', 'register_modify_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')
@@ -155,7 +183,7 @@
],
'building-argument-table.ec2.*': [
('awscli.customizations.argrename', 'register_arg_renames'),
- ('awscli.customizations.toplevelbool', 'register_bool_params')
+ ('awscli.customizations.toplevelbool', 'register_bool_params'),
],
'building-argument-table.ec2.authorize-security-group-egress': [
('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup')
@@ -170,7 +198,10 @@
('awscli.customizations.argrename', 'register_arg_renames')
],
'building-argument-table.ec2.get-password-data': [
- ('awscli.customizations.ec2.decryptpassword', 'register_ec2_add_priv_launch_key')
+ (
+ 'awscli.customizations.ec2.decryptpassword',
+ 'register_ec2_add_priv_launch_key',
+ )
],
'building-argument-table.ec2.revoke-security-group-egress': [
('awscli.customizations.ec2.secgroupsimplify', 'register_secgroup')
@@ -180,22 +211,31 @@
],
'building-argument-table.ec2.run-instances': [
('awscli.customizations.ec2.addcount', 'register_count_events'),
- ('awscli.customizations.ec2.runinstances', 'register_runinstances')
+ ('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')
+ (
+ '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')
+ (
+ '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')
+ (
+ 'awscli.customizations.ecs.monitormutatinggatewayservice',
+ 'register_monitor_mutating_gateway_service',
+ )
],
'building-argument-table.eks.create-cluster': [
('awscli.customizations.argrename', 'register_arg_renames')
@@ -258,7 +298,10 @@
('awscli.customizations.argrename', 'register_arg_renames')
],
'building-argument-table.kinesis.list-streams': [
- ('awscli.customizations.kinesis', 'register_kinesis_list_streams_pagination_backcompat')
+ (
+ 'awscli.customizations.kinesis',
+ 'register_kinesis_list_streams_pagination_backcompat',
+ )
],
'building-argument-table.kinesisanalytics.add-application-output': [
('awscli.customizations.argrename', 'register_arg_renames')
@@ -369,7 +412,10 @@
('awscli.customizations.argrename', 'register_arg_renames')
],
'building-argument-table.quicksight.start-asset-bundle-import-job': [
- ('awscli.customizations.quicksight', 'register_quicksight_asset_bundle_customizations')
+ (
+ 'awscli.customizations.quicksight',
+ 'register_quicksight_asset_bundle_customizations',
+ )
],
'building-argument-table.rds.add-option-to-option-group': [
('awscli.customizations.rds', 'register_rds_modify_split')
@@ -378,10 +424,16 @@
('awscli.customizations.rds', 'register_rds_modify_split')
],
'building-argument-table.rekognition.*': [
- ('awscli.customizations.rekognition', 'register_rekognition_detect_labels')
+ (
+ 'awscli.customizations.rekognition',
+ 'register_rekognition_detect_labels',
+ )
],
'building-argument-table.rekognition.compare-faces': [
- ('awscli.customizations.rekognition', 'register_rekognition_detect_labels')
+ (
+ 'awscli.customizations.rekognition',
+ 'register_rekognition_detect_labels',
+ )
],
'building-argument-table.rekognition.create-stream-processor': [
('awscli.customizations.argrename', 'register_arg_renames')
@@ -432,10 +484,16 @@
('awscli.customizations.argrename', 'register_arg_renames')
],
'building-argument-table.translate.import-terminology': [
- ('awscli.customizations.translate', 'register_translate_import_terminology')
+ (
+ 'awscli.customizations.translate',
+ 'register_translate_import_terminology',
+ )
],
'building-argument-table.translate.translate-document': [
- ('awscli.customizations.translate', 'register_translate_import_terminology')
+ (
+ 'awscli.customizations.translate',
+ 'register_translate_import_terminology',
+ )
],
'building-argument-table.workdocs.create-notification-subscription': [
('awscli.customizations.argrename', 'register_arg_renames')
@@ -445,7 +503,7 @@
],
'building-command-table': [
('awscli.customizations.waiters', 'register_add_waiters'),
- ('awscli.alias', 'register_alias_commands')
+ ('awscli.alias', 'register_alias_commands'),
],
'building-command-table.bedrock-agent-runtime': [
('awscli.customizations.removals', 'register_removals')
@@ -472,14 +530,23 @@
('awscli.customizations.cloudwatch', 'register_rename_otel_commands')
],
'building-command-table.codeartifact': [
- ('awscli.customizations.codeartifact', 'register_codeartifact_commands')
+ (
+ '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')
+ (
+ 'awscli.customizations.configservice.subscribe',
+ 'register_subscribe',
+ ),
+ (
+ 'awscli.customizations.configservice.getstatus',
+ 'register_get_status',
+ ),
],
'building-command-table.configure': [
('awscli.customizations.wizard.commands', 'register_wizard_commands')
@@ -509,7 +576,10 @@
('awscli.customizations.removals', 'register_removals')
],
'building-command-table.ec2-instance-connect': [
- ('awscli.customizations.ec2instanceconnect', 'register_ec2_instance_connect_commands')
+ (
+ 'awscli.customizations.ec2instanceconnect',
+ 'register_ec2_instance_connect_commands',
+ )
],
'building-command-table.ecr': [
('awscli.customizations.ecr', 'register_ecr_commands')
@@ -525,7 +595,7 @@
],
'building-command-table.emr': [
('awscli.customizations.removals', 'register_removals'),
- ('awscli.customizations.emr.emr', 'emr_initialize')
+ ('awscli.customizations.emr.emr', 'emr_initialize'),
],
'building-command-table.emr-containers': [
('awscli.customizations.emrcontainers', 'initialize')
@@ -547,7 +617,7 @@
],
'building-command-table.lambda': [
('awscli.customizations.removals', 'register_removals'),
- ('awscli.customizations.wizard.commands', 'register_wizard_commands')
+ ('awscli.customizations.wizard.commands', 'register_wizard_commands'),
],
'building-command-table.lexv2-runtime': [
('awscli.customizations.removals', 'register_removals')
@@ -557,17 +627,26 @@
],
'building-command-table.logs': [
('awscli.customizations.removals', 'register_removals'),
- ('awscli.customizations.logs', 'register_logs_commands')
+ ('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.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')
+ ('awscli.customizations.login', 'register_login_cmds'),
],
'building-command-table.polly': [
('awscli.customizations.removals', 'register_removals')
@@ -577,7 +656,7 @@
],
'building-command-table.rds': [
('awscli.customizations.rds', 'register_rds_modify_split'),
- ('awscli.customizations.rds', 'register_add_generate_db_auth_token')
+ ('awscli.customizations.rds', 'register_add_generate_db_auth_token'),
],
'building-command-table.s3_sync': [
('awscli.customizations.s3.s3', 'register_s3_sync_strategies')
@@ -586,7 +665,10 @@
('awscli.customizations.removals', 'register_removals')
],
'building-command-table.servicecatalog': [
- ('awscli.customizations.servicecatalog', 'register_servicecatalog_commands')
+ (
+ 'awscli.customizations.servicecatalog',
+ 'register_servicecatalog_commands',
+ )
],
'building-command-table.ses': [
('awscli.customizations.removals', 'register_removals')
@@ -598,16 +680,28 @@
('awscli.customizations.sso', 'register_sso_commands')
],
'calling-command.cloudsearchdomain': [
- ('awscli.customizations.cloudsearchdomain', 'register_cloudsearchdomain')
+ (
+ 'awscli.customizations.cloudsearchdomain',
+ 'register_cloudsearchdomain',
+ )
],
'calling-command.dynamodb.*': [
- ('awscli.customizations.dynamodb.paginatorfix', 'register_dynamodb_paginator_fix')
+ (
+ 'awscli.customizations.dynamodb.paginatorfix',
+ 'register_dynamodb_paginator_fix',
+ )
],
'calling-command.ec2.describe-snapshots': [
- ('awscli.customizations.ec2.paginate', 'register_ec2_page_size_injector')
+ (
+ 'awscli.customizations.ec2.paginate',
+ 'register_ec2_page_size_injector',
+ )
],
'calling-command.ec2.describe-volumes': [
- ('awscli.customizations.ec2.paginate', 'register_ec2_page_size_injector')
+ (
+ 'awscli.customizations.ec2.paginate',
+ 'register_ec2_page_size_injector',
+ )
],
'doc-description': [
('awscli.customizations.paginate', 'register_pagination')
@@ -631,7 +725,10 @@
('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')
+ (
+ 'awscli.customizations.route53',
+ 'register_create_hosted_zone_doc_fix',
+ )
],
'doc-output.datapipeline.get-pipeline-definition': [
('awscli.customizations.datapipeline', 'register_customizations')
@@ -676,16 +773,28 @@
('awscli.customizations.ec2.runinstances', 'register_runinstances')
],
'operation-args-parsed.ecs.create-express-gateway-service': [
- ('awscli.customizations.ecs.monitormutatinggatewayservice', 'register_monitor_mutating_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')
+ (
+ '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')
+ (
+ 'awscli.customizations.ecs.monitormutatinggatewayservice',
+ 'register_monitor_mutating_gateway_service',
+ )
],
'operation-args-parsed.kinesis.list-streams': [
- ('awscli.customizations.kinesis', 'register_kinesis_list_streams_pagination_backcompat')
+ (
+ 'awscli.customizations.kinesis',
+ 'register_kinesis_list_streams_pagination_backcompat',
+ )
],
'operation-args-parsed.ses.send-email': [
('awscli.customizations.sessendemail', 'register_ses_send_email')
@@ -698,17 +807,20 @@
],
'session-initialized': [
('awscli.paramfile', 'register_init_uri_param_handler'),
- ('awscli.customizations.binaryformat', 'register_init_binary_formatter'),
+ (
+ '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')
+ ('awscli.customizations.sso', 'register_sso_commands'),
],
'top-level-args-parsed': [
('awscli.customizations.globalargs', 'register_parse_global_args'),
- ('awscli.customizations.cloudfront', 'register')
- ]
+ ('awscli.customizations.cloudfront', 'register'),
+ ],
}
# Declarative model of changes made to the command table by plugins
@@ -728,7 +840,12 @@
('rename', 's3', 's3api'),
('add', 's3', 'awscli.customizations.s3.s3', 'S3'),
('add', 'ddb', 'awscli.customizations.dynamodb.ddb', 'DDB'),
- ('add', 'configure', 'awscli.customizations.configure.configure', 'ConfigureCommand'),
+ (
+ 'add',
+ 'configure',
+ 'awscli.customizations.configure.configure',
+ 'ConfigureCommand',
+ ),
('rename', 'codedeploy', 'deploy'),
('rename', 'config', 'configservice'),
('add', 'history', 'awscli.customizations.history', 'HistoryCommand'),
From 511d6cbdf080725c4dbce6aade0263af9b2bef94 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 13 May 2026 14:39:27 -0400
Subject: [PATCH 09/21] Update test_handlers_registry against the simplified
format of handlers_registry.
---
tests/functional/test_handlers_registry.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py
index 7856fa3caf91..97b53f2097dc 100644
--- a/tests/functional/test_handlers_registry.py
+++ b/tests/functional/test_handlers_registry.py
@@ -59,7 +59,7 @@ def test_main_command_table_plugins_only_register_against_main():
"""
main_entries = PLUGIN_REGISTRY.get('building-command-table.main', [])
violations = []
- for module_path, fn_name, entry_type in main_entries:
+ for module_path, fn_name in main_entries:
emitter = _AuditEmitter()
mod = importlib.import_module(module_path)
fn = getattr(mod, fn_name)
@@ -98,7 +98,7 @@ def test_main_command_table_callbacks_only_add_or_rename():
main_entries = PLUGIN_REGISTRY.get('building-command-table.main', [])
violations = []
- for module_path, fn_name, entry_type in main_entries:
+ 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)
From 11b07cde48c721a200cafc3b3b51a7d8418fc546 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 13 May 2026 15:04:20 -0400
Subject: [PATCH 10/21] Add new functional tests to assert that all modules
referenced in handlers_registry.py are importable.
---
tests/functional/test_handlers_registry.py | 59 +++++++++++++++++++++-
1 file changed, 58 insertions(+), 1 deletion(-)
diff --git a/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py
index 97b53f2097dc..48ac38554035 100644
--- a/tests/functional/test_handlers_registry.py
+++ b/tests/functional/test_handlers_registry.py
@@ -15,7 +15,7 @@
import botocore.session
-from awscli.handlers_registry import PLUGIN_REGISTRY
+from awscli.handlers_registry import MAIN_COMMAND_TABLE_OPS, PLUGIN_REGISTRY
class _AuditEmitter:
@@ -46,6 +46,63 @@ def register(self, event_name, handler, *args, **kwargs):
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.
From 844dcceef7d129a4878222376da17ef4550c6126 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 13 May 2026 15:04:43 -0400
Subject: [PATCH 11/21] Formatting.
---
tests/functional/test_handlers_registry.py | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py
index 48ac38554035..93d0e36f4854 100644
--- a/tests/functional/test_handlers_registry.py
+++ b/tests/functional/test_handlers_registry.py
@@ -64,13 +64,9 @@ def test_all_registry_entries_are_importable():
continue
fn = getattr(mod, fn_name, None)
if fn is None:
- violations.append(
- f'{module_path}.{fn_name} does not exist'
- )
+ violations.append(f'{module_path}.{fn_name} does not exist')
elif not callable(fn):
- violations.append(
- f'{module_path}.{fn_name} is not callable'
- )
+ 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)
From 15dbbb6bcd1c9cc5ad9a0c569e60b33a6183f52c Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 20 May 2026 15:27:05 -0400
Subject: [PATCH 12/21] Update MAIN_COMMAND_TABLE_OPS to use an enum.
---
awscli/handlers_registry.py | 36 ++++++++++++++--------
tests/functional/test_handlers_registry.py | 8 +++--
2 files changed, 29 insertions(+), 15 deletions(-)
diff --git a/awscli/handlers_registry.py b/awscli/handlers_registry.py
index 0aa2bef59223..54d53eb39cec 100644
--- a/awscli/handlers_registry.py
+++ b/awscli/handlers_registry.py
@@ -20,6 +20,14 @@
Entry format:
(module, fn_name) call fn(event_handlers)
"""
+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': [
@@ -833,23 +841,25 @@
# the command is actually invoked.
#
# Entry formats:
-# ('rename', old_name, new_name)
-# ('add', cmd_name, cmd_module, cmd_class)
+# (CommandTableOp.RENAME, old_name, new_name)
+# (CommandTableOp.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'),
+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'),
(
- 'add',
+ CommandTableOp.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'),
+ (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/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py
index 93d0e36f4854..b5587f5664c6 100644
--- a/tests/functional/test_handlers_registry.py
+++ b/tests/functional/test_handlers_registry.py
@@ -15,7 +15,11 @@
import botocore.session
-from awscli.handlers_registry import MAIN_COMMAND_TABLE_OPS, PLUGIN_REGISTRY
+from awscli.handlers_registry import (
+ CommandTableOp,
+ MAIN_COMMAND_TABLE_OPS,
+ PLUGIN_REGISTRY,
+)
class _AuditEmitter:
@@ -79,7 +83,7 @@ def test_all_main_command_table_ops_modules_are_importable():
"""
violations = []
for op in MAIN_COMMAND_TABLE_OPS:
- if op[0] != 'add':
+ if op[0] != CommandTableOp.ADD:
continue
_, cmd_name, cmd_module, cmd_class = op
try:
From 789f94abfde50cd3cd8db58240d932ff2539c979 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 20 May 2026 15:27:48 -0400
Subject: [PATCH 13/21] Formatting.
---
awscli/handlers_registry.py | 30 +++++++++++++++++++---
tests/functional/test_handlers_registry.py | 2 +-
2 files changed, 27 insertions(+), 5 deletions(-)
diff --git a/awscli/handlers_registry.py b/awscli/handlers_registry.py
index 54d53eb39cec..d36e090bc64a 100644
--- a/awscli/handlers_registry.py
+++ b/awscli/handlers_registry.py
@@ -20,6 +20,7 @@
Entry format:
(module, fn_name) call fn(event_handlers)
"""
+
import enum
@@ -29,6 +30,7 @@ class CommandTableOp(enum.Enum):
ADD = 'add'
RENAME = 'rename'
+
PLUGIN_REGISTRY = {
'after-call.data-pipeline.GetPipelineDefinition': [
('awscli.customizations.datapipeline', 'register_customizations')
@@ -858,8 +860,28 @@ class CommandTableOp(enum.Enum):
),
(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'),
+ (
+ 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/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py
index b5587f5664c6..297a001ce838 100644
--- a/tests/functional/test_handlers_registry.py
+++ b/tests/functional/test_handlers_registry.py
@@ -16,9 +16,9 @@
import botocore.session
from awscli.handlers_registry import (
- CommandTableOp,
MAIN_COMMAND_TABLE_OPS,
PLUGIN_REGISTRY,
+ CommandTableOp,
)
From b48b0d63818068935bc2f65431ec08d2f82c0038 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 20 May 2026 18:44:23 -0400
Subject: [PATCH 14/21] Import annotations.
---
awscli/handlers_registry.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/awscli/handlers_registry.py b/awscli/handlers_registry.py
index d36e090bc64a..fcca245f00f7 100644
--- a/awscli/handlers_registry.py
+++ b/awscli/handlers_registry.py
@@ -20,6 +20,7 @@
Entry format:
(module, fn_name) call fn(event_handlers)
"""
+from __future__ import annotations
import enum
From a71fa98c217fcdf114e62555426415521bb3d7a9 Mon Sep 17 00:00:00 2001
From: aemous
Date: Fri, 22 May 2026 16:15:03 -0400
Subject: [PATCH 15/21] Implement LazyInitEmitter for lazily loading plugins
when events they depend on are emitted.
---
awscli/lazy_emitter.py | 157 +++++++++++++++++++++
tests/unit/test_lazy_emitter.py | 236 ++++++++++++++++++++++++++++++++
2 files changed, 393 insertions(+)
create mode 100644 awscli/lazy_emitter.py
create mode 100644 tests/unit/test_lazy_emitter.py
diff --git a/awscli/lazy_emitter.py b/awscli/lazy_emitter.py
new file mode 100644
index 000000000000..8c57367f6ed4
--- /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 (
+ CommandTableOp,
+ PLUGIN_REGISTRY,
+)
+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/tests/unit/test_lazy_emitter.py b/tests/unit/test_lazy_emitter.py
new file mode 100644
index 000000000000..281b6d38a6d7
--- /dev/null
+++ b/tests/unit/test_lazy_emitter.py
@@ -0,0 +1,236 @@
+# 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):
+ 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):
+ 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):
+ 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,
+ )
+
+ imp.assert_not_called()
+ 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
+ ):
+ 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):
+ 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
From 806814a9d45daa1d388251544c84dac544683c37 Mon Sep 17 00:00:00 2001
From: aemous
Date: Fri, 22 May 2026 16:17:18 -0400
Subject: [PATCH 16/21] Formatting.
---
awscli/lazy_emitter.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/awscli/lazy_emitter.py b/awscli/lazy_emitter.py
index 8c57367f6ed4..2043902c77cd 100644
--- a/awscli/lazy_emitter.py
+++ b/awscli/lazy_emitter.py
@@ -26,8 +26,8 @@
from botocore.hooks import HierarchicalEmitter, PrefixTrie
from awscli.handlers_registry import (
- CommandTableOp,
PLUGIN_REGISTRY,
+ CommandTableOp,
)
from awscli.lazy import LazyCommand
From 97db2bbc6aceaffb8ac418f8a911168309777249 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 27 May 2026 10:55:00 -0400
Subject: [PATCH 17/21] Implement lazy plugin initialization.
---
.../enhancement-Performance-9167.json | 5 +
awscli/clidriver.py | 7 +-
awscli/handlers.py | 254 +++---------------
awscli/plugin.py | 8 +-
exe/pyinstaller/hook-awscli.py | 10 +
.../autocomplete/test_server_index.py | 4 +-
.../autoprompt/test_prompttoolkit.py | 2 +-
tests/functional/test_handlers_registry.py | 115 +-------
tests/functional/test_lazy.py | 38 ++-
tests/unit/test_clidriver.py | 5 +-
tests/unit/test_lazy_emitter.py | 14 +
tests/unit/test_plugin.py | 2 +-
12 files changed, 122 insertions(+), 342 deletions(-)
create mode 100644 .changes/next-release/enhancement-Performance-9167.json
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
index 2dab06ba171a..aa335b214d30 100644
--- a/awscli/handlers.py
+++ b/awscli/handlers.py
@@ -1,4 +1,4 @@
-# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# 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
@@ -10,225 +10,45 @@
# 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.
+"""Built-in CLI extensions.
+Load built-in CLI extensions from the plugin registry
+(`awscli/handlers_registry.py`). If the supplied event handler is a
+LazyInitEmitter, we load the registry so that the LazyInitEmitter can handle
+lazy plugin initialization. Otherwise, we fall back to loading all plugins in
+the registry eagerly.
"""
-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
+import importlib
+
+from awscli.handlers_registry import PLUGIN_REGISTRY
+from awscli.lazy_emitter import LazyInitEmitter
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)
+ """Load the plugin registry into the emitter.
+
+ If the emitter is a LazyInitEmitter, the registry is loaded into its
+ initializer trie for on-demand initialization. Otherwise, all built-in
+ plugins are eagerly initialized.
+ """
+ if isinstance(event_handlers, LazyInitEmitter):
+ event_handlers.load_registry(PLUGIN_REGISTRY)
+ else:
+ # Fallback to eagerly initializing all built-in plugins.
+ 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_type = entry
+ mod = importlib.import_module(module_path)
+ fn = getattr(mod, fn_name)
+ if entry_type == 'direct':
+ handler = fn() if isinstance(fn, type) else fn
+ event_handlers.register(event_pattern, handler)
+ else:
+ if isinstance(fn, type):
+ fn(event_handlers)
+ else:
+ fn(event_handlers)
diff --git a/awscli/plugin.py b/awscli/plugin.py
index 46a26a4fc1a7..c2bf967a26f1 100644
--- a/awscli/plugin.py
+++ b/awscli/plugin.py
@@ -14,7 +14,7 @@
import os
import sys
-from botocore.hooks import HierarchicalEmitter
+from awscli.lazy_emitter import LazyInitEmitter
log = logging.getLogger('awscli.plugin')
@@ -31,19 +31,19 @@ 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.
- :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)
plugin_path = plugin_mapping.pop(CLI_LEGACY_PLUGIN_PATH, None)
diff --git a/exe/pyinstaller/hook-awscli.py b/exe/pyinstaller/hook-awscli.py
index a0767694019a..31a72dd3332e 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
+# handlers.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..c982f0d067ef 100644
--- a/tests/functional/test_handlers_registry.py
+++ b/tests/functional/test_handlers_registry.py
@@ -15,11 +15,7 @@
import botocore.session
-from awscli.handlers_registry import (
- MAIN_COMMAND_TABLE_OPS,
- PLUGIN_REGISTRY,
- CommandTableOp,
-)
+from awscli.handlers_registry import PLUGIN_REGISTRY
class _AuditEmitter:
@@ -101,112 +97,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..3ba067e13aad 100644
--- a/tests/functional/test_lazy.py
+++ b/tests/functional/test_lazy.py
@@ -12,8 +12,44 @@
# language governing permissions and limitations under the License.
import pytest
+from awscli.handlers_registry import MAIN_COMMAND_TABLE_OPS
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] == 'add']
+_RENAME_OPS = [op for op in MAIN_COMMAND_TABLE_OPS if op[0] == '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)
From 330ea6155683544d2e337e739c6f4ff200945749 Mon Sep 17 00:00:00 2001
From: aemous
Date: Wed, 27 May 2026 11:51:50 -0400
Subject: [PATCH 18/21] Delete handlers.py and migrate the registry-loading
code into plugin.py.
---
awscli/handlers.py | 54 ----------------------
awscli/plugin.py | 8 ++--
tests/functional/test_handlers_registry.py | 6 ++-
3 files changed, 9 insertions(+), 59 deletions(-)
delete mode 100644 awscli/handlers.py
diff --git a/awscli/handlers.py b/awscli/handlers.py
deleted file mode 100644
index aa335b214d30..000000000000
--- a/awscli/handlers.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# 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.
-"""Built-in CLI extensions.
-
-Load built-in CLI extensions from the plugin registry
-(`awscli/handlers_registry.py`). If the supplied event handler is a
-LazyInitEmitter, we load the registry so that the LazyInitEmitter can handle
-lazy plugin initialization. Otherwise, we fall back to loading all plugins in
-the registry eagerly.
-"""
-
-import importlib
-
-from awscli.handlers_registry import PLUGIN_REGISTRY
-from awscli.lazy_emitter import LazyInitEmitter
-
-
-def awscli_initialize(event_handlers):
- """Load the plugin registry into the emitter.
-
- If the emitter is a LazyInitEmitter, the registry is loaded into its
- initializer trie for on-demand initialization. Otherwise, all built-in
- plugins are eagerly initialized.
- """
- if isinstance(event_handlers, LazyInitEmitter):
- event_handlers.load_registry(PLUGIN_REGISTRY)
- else:
- # Fallback to eagerly initializing all built-in plugins.
- 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_type = entry
- mod = importlib.import_module(module_path)
- fn = getattr(mod, fn_name)
- if entry_type == 'direct':
- handler = fn() if isinstance(fn, type) else fn
- event_handlers.register(event_pattern, handler)
- else:
- if isinstance(fn, type):
- fn(event_handlers)
- else:
- fn(event_handlers)
diff --git a/awscli/plugin.py b/awscli/plugin.py
index c2bf967a26f1..2544d5885403 100644
--- a/awscli/plugin.py
+++ b/awscli/plugin.py
@@ -14,11 +14,11 @@
import os
import sys
+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'
@@ -35,8 +35,8 @@ def load_plugins(plugin_mapping, event_hooks=None, include_builtins=True):
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: LazyInitEmitter
:return: An event emitter object.
@@ -45,7 +45,7 @@ def load_plugins(plugin_mapping, event_hooks=None, include_builtins=True):
if event_hooks is None:
event_hooks = LazyInitEmitter()
if include_builtins:
- _load_plugins(BUILTIN_PLUGINS, event_hooks)
+ event_hooks.load_registry(PLUGIN_REGISTRY)
plugin_path = plugin_mapping.pop(CLI_LEGACY_PLUGIN_PATH, None)
if plugin_path is not None:
_add_plugin_path_to_sys_path(plugin_path)
diff --git a/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py
index c982f0d067ef..3b2049df53dd 100644
--- a/tests/functional/test_handlers_registry.py
+++ b/tests/functional/test_handlers_registry.py
@@ -15,7 +15,11 @@
import botocore.session
-from awscli.handlers_registry import PLUGIN_REGISTRY
+from awscli.handlers_registry import (
+ CommandTableOp,
+ MAIN_COMMAND_TABLE_OPS,
+ PLUGIN_REGISTRY,
+)
class _AuditEmitter:
From 369d8f410a47093bcbf152741cb31180807a1f31 Mon Sep 17 00:00:00 2001
From: aemous
Date: Fri, 29 May 2026 09:29:41 -0400
Subject: [PATCH 19/21] Add eager-loading fallback to plugin.py.
---
awscli/plugin.py | 21 ++++++++++++++++++++-
exe/pyinstaller/hook-awscli.py | 2 +-
tests/functional/test_lazy.py | 6 +++---
3 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/awscli/plugin.py b/awscli/plugin.py
index 2544d5885403..73c44c3c2293 100644
--- a/awscli/plugin.py
+++ b/awscli/plugin.py
@@ -10,6 +10,7 @@
# 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
@@ -45,7 +46,12 @@ def load_plugins(plugin_mapping, event_hooks=None, include_builtins=True):
if event_hooks is None:
event_hooks = LazyInitEmitter()
if include_builtins:
- event_hooks.load_registry(PLUGIN_REGISTRY)
+ if isinstance(event_hooks, LazyInitEmitter):
+ event_hooks.load_registry(PLUGIN_REGISTRY)
+ else:
+ # When the event emitter is not a LazyInitEmitter, we fall back
+ # to eagerly loading all plugins in the registry.
+ _eager_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 +64,19 @@ def load_plugins(plugin_mapping, event_hooks=None, include_builtins=True):
return event_hooks
+def _eager_load_registry(event_hooks):
+ """Eagerly initialize all plugins from the plugin registry."""
+ 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)
+
+
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 31a72dd3332e..5d284fabeae6 100644
--- a/exe/pyinstaller/hook-awscli.py
+++ b/exe/pyinstaller/hook-awscli.py
@@ -30,7 +30,7 @@
) + hooks.collect_submodules('awscli.s3transfer')
hiddenimports += alias_packages_plugins
-# handlers.py uses importlib.import_module at runtime to load customization
+# 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 = {
diff --git a/tests/functional/test_lazy.py b/tests/functional/test_lazy.py
index 3ba067e13aad..be134a98d8c8 100644
--- a/tests/functional/test_lazy.py
+++ b/tests/functional/test_lazy.py
@@ -12,13 +12,13 @@
# language governing permissions and limitations under the License.
import pytest
-from awscli.handlers_registry import MAIN_COMMAND_TABLE_OPS
+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] == 'add']
-_RENAME_OPS = [op for op in MAIN_COMMAND_TABLE_OPS if op[0] == 'rename']
+_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]
From 44c3ce9971ceb6bf88c8fa312535c787c27e83fa Mon Sep 17 00:00:00 2001
From: aemous
Date: Fri, 29 May 2026 09:30:12 -0400
Subject: [PATCH 20/21] Formatting.
---
tests/functional/test_handlers_registry.py | 2 +-
tests/functional/test_lazy.py | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/tests/functional/test_handlers_registry.py b/tests/functional/test_handlers_registry.py
index 3b2049df53dd..ec45094e44ac 100644
--- a/tests/functional/test_handlers_registry.py
+++ b/tests/functional/test_handlers_registry.py
@@ -16,9 +16,9 @@
import botocore.session
from awscli.handlers_registry import (
- CommandTableOp,
MAIN_COMMAND_TABLE_OPS,
PLUGIN_REGISTRY,
+ CommandTableOp,
)
diff --git a/tests/functional/test_lazy.py b/tests/functional/test_lazy.py
index be134a98d8c8..75cba38e5283 100644
--- a/tests/functional/test_lazy.py
+++ b/tests/functional/test_lazy.py
@@ -18,7 +18,9 @@
# 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]
+_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]
From bf9aa0c731ac974fa4853215041d54010bc09b42 Mon Sep 17 00:00:00 2001
From: aemous
Date: Fri, 29 May 2026 11:20:43 -0400
Subject: [PATCH 21/21] Switch to using single dispatch in plugin.py.
---
awscli/plugin.py | 27 +++++++++++++++++++--------
1 file changed, 19 insertions(+), 8 deletions(-)
diff --git a/awscli/plugin.py b/awscli/plugin.py
index 73c44c3c2293..fe41651c3d4f 100644
--- a/awscli/plugin.py
+++ b/awscli/plugin.py
@@ -14,6 +14,9 @@
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
@@ -46,12 +49,7 @@ def load_plugins(plugin_mapping, event_hooks=None, include_builtins=True):
if event_hooks is None:
event_hooks = LazyInitEmitter()
if include_builtins:
- if isinstance(event_hooks, LazyInitEmitter):
- event_hooks.load_registry(PLUGIN_REGISTRY)
- else:
- # When the event emitter is not a LazyInitEmitter, we fall back
- # to eagerly loading all plugins in the registry.
- _eager_load_registry(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)
@@ -64,8 +62,16 @@ def load_plugins(plugin_mapping, event_hooks=None, include_builtins=True):
return event_hooks
-def _eager_load_registry(event_hooks):
- """Eagerly initialize all plugins from the plugin registry."""
+@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:
@@ -77,6 +83,11 @@ def _eager_load_registry(event_hooks):
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):