Skip to content

Commit efc8370

Browse files
huiii99necusjz
andauthored
Fix Invalidate command index after azdev extension add/remove (#543)
* fix: invalidate command index after azdev extension add/remove * fix: ci error * fix: ci error * fix: resolve conflicts * Update HISTORY.rst Co-authored-by: necusjz <necusjz@gmail.com> * Update HISTORY.rst * Update HISTORY.rst --------- Co-authored-by: necusjz <necusjz@gmail.com>
1 parent 654c135 commit efc8370

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
33
Release History
44
===============
5+
0.2.11
6+
++++++
7+
* `azdev extension add/remove`: Invalidate command index after installing or removing extensions.
8+
59
0.2.10
610
++++++
711
* Add support for Python 3.14 and drop support for Python 3.9

azdev/operations/extensions/__init__.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,36 @@
1616

1717
from azdev.utilities import (
1818
cmd, py_cmd, pip_cmd, display, get_ext_repo_paths, find_files, get_azure_config, get_azdev_config,
19-
require_azure_cli, heading, subheading, EXTENSION_PREFIX)
19+
get_azure_config_dir, require_azure_cli, heading, subheading, EXTENSION_PREFIX)
2020
from .version_upgrade import VersionUpgradeMod
2121

2222
logger = get_logger(__name__)
2323

24+
# These are the index files cleared by CommandIndex().invalidate() in azure-cli-core.
25+
# Refer: azure-cli-core/azure/cli/core/__init__.py
26+
_COMMAND_INDEX_FILES = (
27+
'commandIndex.json',
28+
'extensionIndex.json',
29+
'helpIndex.json',
30+
'extensionHelpIndex.json',
31+
)
32+
33+
34+
def _invalidate_command_index():
35+
"""Delete the CLI command index files so they are regenerated on next invocation.
36+
37+
This mirrors the behavior of ``CommandIndex().invalidate()`` in azure-cli-core but
38+
works without requiring a fully initialized CLI session.
39+
"""
40+
azure_config_dir = get_azure_config_dir()
41+
for filename in _COMMAND_INDEX_FILES:
42+
filepath = os.path.join(azure_config_dir, filename)
43+
try:
44+
os.remove(filepath)
45+
logger.debug("Deleted command index file: %s", filepath)
46+
except OSError:
47+
pass
48+
2449

2550
def add_extension(extensions):
2651

@@ -49,6 +74,8 @@ def add_extension(extensions):
4974
if result.error:
5075
raise result.error # pylint: disable=raising-bad-type
5176

77+
_invalidate_command_index()
78+
5279

5380
def remove_extension(extensions):
5481

@@ -83,6 +110,8 @@ def remove_extension(extensions):
83110
display("Removing '{}'...".format(path_to_remove))
84111
shutil.rmtree(path_to_remove)
85112

113+
_invalidate_command_index()
114+
86115

87116
def _get_installed_dev_extensions(dev_sources):
88117
from glob import glob
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# -----------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# -----------------------------------------------------------------------------
6+
7+
import os
8+
import tempfile
9+
import unittest
10+
from unittest.mock import patch
11+
12+
from azdev.operations.extensions import _invalidate_command_index, _COMMAND_INDEX_FILES
13+
14+
15+
class TestInvalidateCommandIndex(unittest.TestCase):
16+
17+
def test_deletes_all_index_files(self):
18+
"""All four command index files should be deleted when they exist."""
19+
with tempfile.TemporaryDirectory() as tmpdir:
20+
for filename in _COMMAND_INDEX_FILES:
21+
filepath = os.path.join(tmpdir, filename)
22+
with open(filepath, 'w') as f:
23+
f.write('{}')
24+
25+
with patch('azdev.operations.extensions.get_azure_config_dir', return_value=tmpdir):
26+
_invalidate_command_index()
27+
28+
for filename in _COMMAND_INDEX_FILES:
29+
self.assertFalse(os.path.exists(os.path.join(tmpdir, filename)),
30+
f'{filename} should have been deleted')
31+
32+
def test_handles_missing_files_gracefully(self):
33+
"""Should not raise when index files do not exist."""
34+
with tempfile.TemporaryDirectory() as tmpdir:
35+
with patch('azdev.operations.extensions.get_azure_config_dir', return_value=tmpdir):
36+
_invalidate_command_index() # should not raise
37+
38+
def test_handles_partial_files(self):
39+
"""Should delete existing files and skip missing ones without error."""
40+
with tempfile.TemporaryDirectory() as tmpdir:
41+
existing = _COMMAND_INDEX_FILES[0]
42+
with open(os.path.join(tmpdir, existing), 'w') as f:
43+
f.write('{}')
44+
45+
with patch('azdev.operations.extensions.get_azure_config_dir', return_value=tmpdir):
46+
_invalidate_command_index()
47+
48+
self.assertFalse(os.path.exists(os.path.join(tmpdir, existing)))
49+
50+
@patch('azdev.operations.extensions._invalidate_command_index')
51+
@patch('azdev.operations.extensions.pip_cmd')
52+
@patch('azdev.operations.extensions.find_files', return_value=['/repo/src/my-ext/setup.py'])
53+
@patch('azdev.operations.extensions.get_ext_repo_paths', return_value=['/repo'])
54+
def test_add_extension_calls_invalidate(self, _mock_paths, _mock_find, mock_pip, mock_invalidate):
55+
"""add_extension should call _invalidate_command_index after installing."""
56+
from unittest.mock import MagicMock
57+
mock_pip.return_value = MagicMock(error=None)
58+
59+
from azdev.operations.extensions import add_extension
60+
add_extension(['my-ext'])
61+
62+
mock_invalidate.assert_called_once()
63+
64+
@patch('azdev.operations.extensions._invalidate_command_index')
65+
@patch('azdev.operations.extensions.pip_cmd')
66+
@patch('azdev.operations.extensions.display')
67+
@patch('azdev.operations.extensions.find_files', return_value=['/repo/src/my-ext/my_ext.egg-info'])
68+
@patch('azdev.operations.extensions.get_ext_repo_paths', return_value=['/repo'])
69+
def test_remove_extension_calls_invalidate(
70+
self, _mock_paths, _mock_find, _mock_display,
71+
_mock_pip, mock_invalidate):
72+
"""remove_extension should call _invalidate_command_index after removing."""
73+
with patch('os.listdir', return_value=[]):
74+
from azdev.operations.extensions import remove_extension
75+
remove_extension(['my-ext'])
76+
77+
mock_invalidate.assert_called_once()
78+
79+
80+
if __name__ == '__main__':
81+
unittest.main()

0 commit comments

Comments
 (0)