Skip to content

Commit ed29aa4

Browse files
fix: fix top-level help and fix help cache write conditions
1 parent 9263570 commit ed29aa4

File tree

2 files changed

+97
-19
lines changed

2 files changed

+97
-19
lines changed

src/azure-cli-core/azure/cli/core/__init__.py

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,14 @@ def _get_extension_suppressions(mod_loaders):
476476
# The index won't contain suppressed extensions
477477
_update_command_table_from_extensions([], index_extensions)
478478

479+
# If we loaded all extensions as a safety fallback, refresh extension overlay cache
480+
# so subsequent runs can use blended targeted loading.
481+
if use_command_index and index_extensions is None and command_index.cloud_profile == 'latest':
482+
command_index.update_extension_index(self.command_table)
483+
# We already paid the cost to load all extensions. Refresh help overlay as well so
484+
# top-level help can stay on packaged base + extension overlay fast path.
485+
self._cache_help_index(command_index)
486+
479487
logger.debug("Loaded %d groups, %d commands.", len(self.command_group_table), len(self.command_table))
480488
from azure.cli.core.util import roughly_parse_command
481489
# The index may be outdated. Make sure the command appears in the loaded command table
@@ -517,6 +525,15 @@ def _get_extension_suppressions(mod_loaders):
517525

518526
logger.debug("Could not find a match in the command or command group table for '%s'. "
519527
"The index may be outdated.", raw_cmd)
528+
529+
if command_index.cloud_profile == 'latest' and lookup_args and \
530+
not self.cli_ctx.data['completer_active']:
531+
top_command = lookup_args[0]
532+
packaged_core_index = command_index._get_packaged_command_index(ignore_extensions=True) or {}
533+
if top_command != 'help' and top_command not in packaged_core_index:
534+
logger.debug("Top-level command '%s' is not in packaged core index. "
535+
"Skipping full core module reload.", top_command)
536+
return self.command_table
520537
else:
521538
logger.debug("No module found from index for '%s'", args)
522539

@@ -1048,12 +1065,19 @@ def get(self, args):
10481065
if result:
10491066
return result
10501067

1051-
if force_load_all_extensions and normalized_args and not normalized_args[0].startswith('-') and \
1052-
not self.cli_ctx.data['completer_active']:
1053-
logger.debug("No match found in blended latest index for '%s'. Loading all extensions.",
1054-
normalized_args[0])
1055-
# Load all extensions to resolve extension-only top-level commands without rebuilding all modules.
1056-
return [], None
1068+
if normalized_args and not normalized_args[0].startswith('-') and \
1069+
not self.cli_ctx.data['completer_active'] and not force_packaged_for_version and \
1070+
top_command != 'help':
1071+
# Unknown top-level command on latest should prefer extension-only retry and avoid
1072+
# full core module rebuild to preserve packaged-index startup benefit.
1073+
if has_non_always_loaded_extensions:
1074+
logger.debug("No match found in blended latest index for '%s'. Loading all extensions.",
1075+
normalized_args[0])
1076+
return [], None
1077+
1078+
logger.debug("No match found in latest index for '%s' and no dynamic extensions are installed. "
1079+
"Skipping core module rebuild.", normalized_args[0])
1080+
return [], []
10571081

10581082
logger.debug("No match found in blended latest index. Falling back to local command index.")
10591083

@@ -1225,21 +1249,27 @@ def update(self, command_table):
12251249
elapsed_time = timeit.default_timer() - start_time
12261250
self.INDEX[self._COMMAND_INDEX] = index
12271251

1228-
# Maintain extension-only overlay for latest profile so packaged core can be blended.
1229-
if self.cloud_profile == 'latest':
1230-
extension_index = defaultdict(list)
1231-
for command_name, command in command_table.items():
1232-
top_command = command_name.split()[0]
1233-
module_name = command.loader.__module__
1234-
if module_name.startswith('azext_') and module_name not in extension_index[top_command]:
1235-
extension_index[top_command].append(module_name)
1236-
1237-
self.EXTENSION_INDEX[self._COMMAND_INDEX_VERSION] = __version__
1238-
self.EXTENSION_INDEX[self._COMMAND_INDEX_CLOUD_PROFILE] = self.cloud_profile
1239-
self.EXTENSION_INDEX[self._COMMAND_INDEX] = extension_index
1252+
self.update_extension_index(command_table)
12401253

12411254
logger.debug("Updated command index in %.3f seconds.", elapsed_time)
12421255

1256+
def update_extension_index(self, command_table):
1257+
"""Update extension-only overlay index from a command table (latest profile only)."""
1258+
if self.cloud_profile != 'latest':
1259+
return
1260+
1261+
from collections import defaultdict
1262+
extension_index = defaultdict(list)
1263+
for command_name, command in command_table.items():
1264+
top_command = command_name.split()[0]
1265+
module_name = command.loader.__module__
1266+
if module_name.startswith('azext_') and module_name not in extension_index[top_command]:
1267+
extension_index[top_command].append(module_name)
1268+
1269+
self.EXTENSION_INDEX[self._COMMAND_INDEX_VERSION] = __version__
1270+
self.EXTENSION_INDEX[self._COMMAND_INDEX_CLOUD_PROFILE] = self.cloud_profile
1271+
self.EXTENSION_INDEX[self._COMMAND_INDEX] = extension_index
1272+
12431273
def invalidate(self):
12441274
"""Invalidate the command index.
12451275

src/azure-cli-core/azure/cli/core/tests/test_command_registration.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ def test_command_index_handles_leading_output_option(self):
535535
@mock.patch('azure.cli.core.extension.get_extension_modname', _mock_get_extension_modname)
536536
@mock.patch('azure.cli.core.extension.get_extensions', _mock_get_extensions)
537537
def test_command_index_loads_all_extensions_when_overlay_missing(self):
538-
from azure.cli.core._session import INDEX
538+
from azure.cli.core._session import INDEX, EXTENSION_INDEX, EXTENSION_HELP_INDEX
539539
from azure.cli.core import CommandIndex, __version__
540540

541541
cli = DummyCli()
@@ -560,6 +560,54 @@ def test_command_index_loads_all_extensions_when_overlay_missing(self):
560560
# Missing overlay triggers loading all extensions, but avoids full module rebuild.
561561
self.assertEqual(list(cmd_tbl), ['hello mod-only', 'hello overridden', 'hello ext-only'])
562562
self.assertEqual(INDEX[CommandIndex._COMMAND_INDEX], {})
563+
self.assertEqual(EXTENSION_INDEX[CommandIndex._COMMAND_INDEX_VERSION], __version__)
564+
self.assertEqual(EXTENSION_INDEX[CommandIndex._COMMAND_INDEX_CLOUD_PROFILE], cli.cloud.profile)
565+
self.assertIn('hello', EXTENSION_INDEX[CommandIndex._COMMAND_INDEX])
566+
self.assertIn('azext_hello1', EXTENSION_INDEX[CommandIndex._COMMAND_INDEX]['hello'])
567+
self.assertIn('azext_hello2', EXTENSION_INDEX[CommandIndex._COMMAND_INDEX]['hello'])
568+
self.assertEqual(EXTENSION_HELP_INDEX[CommandIndex._COMMAND_INDEX_VERSION], __version__)
569+
self.assertEqual(EXTENSION_HELP_INDEX[CommandIndex._COMMAND_INDEX_CLOUD_PROFILE], cli.cloud.profile)
570+
self.assertIn('groups', EXTENSION_HELP_INDEX[CommandIndex._HELP_INDEX])
571+
self.assertIn('commands', EXTENSION_HELP_INDEX[CommandIndex._HELP_INDEX])
572+
573+
@mock.patch('importlib.import_module', _mock_import_lib)
574+
@mock.patch('pkgutil.iter_modules', _mock_iter_modules)
575+
@mock.patch('azure.cli.core.commands._load_command_loader', _mock_load_command_loader)
576+
@mock.patch('azure.cli.core.extension.get_extension_modname', _mock_get_extension_modname)
577+
@mock.patch('azure.cli.core.extension.get_extensions', _mock_get_extensions)
578+
def test_command_index_latest_unknown_non_core_skips_full_core_reload(self):
579+
from azure.cli.core._session import INDEX, EXTENSION_INDEX
580+
from azure.cli.core import CommandIndex, __version__
581+
582+
cli = DummyCli()
583+
loader = cli.commands_loader
584+
585+
INDEX[CommandIndex._COMMAND_INDEX_VERSION] = ""
586+
INDEX[CommandIndex._COMMAND_INDEX_CLOUD_PROFILE] = ""
587+
INDEX[CommandIndex._COMMAND_INDEX] = {}
588+
EXTENSION_INDEX[CommandIndex._COMMAND_INDEX_VERSION] = ""
589+
EXTENSION_INDEX[CommandIndex._COMMAND_INDEX_CLOUD_PROFILE] = ""
590+
EXTENSION_INDEX[CommandIndex._COMMAND_INDEX] = {}
591+
592+
packaged_index = {
593+
CommandIndex._COMMAND_INDEX_VERSION: __version__,
594+
CommandIndex._COMMAND_INDEX_CLOUD_PROFILE: cli.cloud.profile,
595+
CommandIndex._COMMAND_INDEX: {
596+
'hello': ['azure.cli.command_modules.hello'],
597+
'extra': ['azure.cli.command_modules.extra']
598+
}
599+
}
600+
601+
with mock.patch.object(CommandIndex, '_load_packaged_command_index', return_value=packaged_index):
602+
cmd_tbl = loader.load_command_table(["foobar", "list"])
603+
604+
# Unknown non-core top-level command should try extensions without rebuilding all core modules.
605+
self.assertNotIn('hello mod-only', cmd_tbl)
606+
self.assertNotIn('extra final', cmd_tbl)
607+
self.assertEqual(INDEX[CommandIndex._COMMAND_INDEX], {})
608+
self.assertEqual(EXTENSION_INDEX[CommandIndex._COMMAND_INDEX_VERSION], __version__)
609+
self.assertEqual(EXTENSION_INDEX[CommandIndex._COMMAND_INDEX_CLOUD_PROFILE], cli.cloud.profile)
610+
self.assertIn('hello', EXTENSION_INDEX[CommandIndex._COMMAND_INDEX])
563611

564612
@mock.patch('importlib.import_module', _mock_import_lib)
565613
@mock.patch('pkgutil.iter_modules', _mock_iter_modules)

0 commit comments

Comments
 (0)