Skip to content

Commit

Permalink
{core} Enable extension semantic versioning and join experimental i…
Browse files Browse the repository at this point in the history
…nto `preview` (Azure#27877)

* add new version parse for extension

* joined experimental with preview in list
  • Loading branch information
AllyW authored Nov 30, 2023
1 parent 1597b4f commit ef8ec47
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 15 deletions.
33 changes: 24 additions & 9 deletions src/azure-cli-core/azure/cli/core/extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,17 @@ def preview(self):
"""
try:
if not isinstance(self._preview, bool):
self._preview = bool(self.metadata.get(EXT_METADATA_ISPREVIEW))
self._preview = is_preview_from_extension_meta(self.metadata)
except Exception: # pylint: disable=broad-except
logger.debug("Unable to get extension preview status: %s", traceback.format_exc())
return self._preview

@property
def experimental(self):
"""
Lazy load experimental status.
Returns the experimental status of the extension.
In extension semantic versioning, experimental = preview, experimental deprecated
"""
try:
if not isinstance(self._experimental, bool):
self._experimental = bool(self.metadata.get(EXT_METADATA_ISEXPERIMENTAL))
except Exception: # pylint: disable=broad-except
logger.debug("Unable to get extension experimental status: %s", traceback.format_exc())
return self._experimental
return False

def get_version(self):
raise NotImplementedError()
Expand Down Expand Up @@ -359,3 +353,24 @@ def get_extension_names(ext_type=None):
Returns the extension names of extensions installed in the extensions directory.
"""
return [ext.name for ext in get_extensions(ext_type=ext_type)]


def is_preview_from_extension_meta(extension_meta):
return (bool(extension_meta.get(EXT_METADATA_ISPREVIEW, False)) or
bool(extension_meta.get(EXT_METADATA_ISEXPERIMENTAL, False)) or
is_preview_from_semantic_version(extension_meta.get('version')))


def is_preview_from_semantic_version(version):
"""
pre = [a, b] -> preview
>>> print(parse("1.2.3").pre)
None
>>> parse("1.2.3a1").pre
('a', 1)
>>> parse("1.2.3b1").pre
('b', 1)
"""
from packaging.version import parse
parsed_version = parse(version)
return bool(parsed_version.pre and parsed_version.pre[0] in ["a", "b"])
11 changes: 5 additions & 6 deletions src/azure-cli-core/azure/cli/core/extension/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
from azure.cli.core import CommandIndex
from azure.cli.core.util import CLIError, reload_module, rmtree_with_retry
from azure.cli.core.extension import (extension_exists, build_extension_path, get_extensions, get_extension_modname,
get_extension, ext_compat_with_cli,
EXT_METADATA_ISPREVIEW, EXT_METADATA_ISEXPERIMENTAL,
get_extension, ext_compat_with_cli, is_preview_from_extension_meta,
WheelExtension, DevExtension, ExtensionNotInstalledException, WHEEL_INFO_RE)
from azure.cli.core.telemetry import set_extension_management_detail

Expand Down Expand Up @@ -465,8 +464,8 @@ def list_available_extensions(index_url=None, show_details=False, cli_ctx=None):
'name': name,
'version': latest['metadata']['version'],
'summary': latest['metadata']['summary'],
'preview': latest['metadata'].get(EXT_METADATA_ISPREVIEW, False),
'experimental': latest['metadata'].get(EXT_METADATA_ISEXPERIMENTAL, False),
'preview': is_preview_from_extension_meta(latest['metadata']),
'experimental': False,
'installed': installed
})
return results
Expand Down Expand Up @@ -502,8 +501,8 @@ def list_versions(extension_name, index_url=None, cli_ctx=None):
results.append({
'name': extension_name,
'version': version,
'preview': ext['metadata'].get(EXT_METADATA_ISPREVIEW, False),
'experimental': ext['metadata'].get(EXT_METADATA_ISEXPERIMENTAL, False),
'preview': is_preview_from_extension_meta(ext['metadata']),
'experimental': False,
'installed': installed,
'compatible': compatible
})
Expand Down
Binary file not shown.
72 changes: 72 additions & 0 deletions src/azure-cli-core/azure/cli/core/tests/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
import zipfile

from unittest import mock
from azure.cli.core.mock import DummyCli

from azure.cli.core.extension import (get_extensions, build_extension_path, extension_exists,
get_extension, get_extension_names, get_extension_modname, ext_compat_with_cli,
ExtensionNotInstalledException, WheelExtension,
EXTENSIONS_MOD_PREFIX, EXT_METADATA_MINCLICOREVERSION, EXT_METADATA_MAXCLICOREVERSION)

from azure.cli.core.extension.operations import list_available_extensions, add_extension, show_extension, remove_extension


# The test extension name
EXT_NAME = 'myfirstcliextension'
Expand Down Expand Up @@ -59,6 +62,9 @@ def setUp(self):
mock.patch('azure.cli.core.extension.EXTENSIONS_SYS_DIR', self.ext_sys_dir)]
for patcher in self.patchers:
patcher.start()
cmd = mock.MagicMock()
cmd.cli_ctx = DummyCli()
self.cmd = cmd

def tearDown(self):
for patcher in self.patchers:
Expand Down Expand Up @@ -230,6 +236,72 @@ def test_ext_compat_with_cli_require_ext_min_version(self):
self.assertFalse(is_compatible)
self.assertEqual(min_ext_required, expected_min_ext_required)

def test_list_available_extensions_preview_details(self):
sample_index_extensions = {
'ml': [{
'metadata': {
'name': 'ml',
'summary': 'AzureMachineLearningWorkspaces Extension',
'version': '2.0.0a1',
'azext.isExperimental': True
}
}],
'test_sample_extension1': [{
'metadata': {
'name': 'test_sample_extension1',
'summary': 'my summary',
'version': '1.15.0',
'azext.isPreview': True,
}
}],
'test_sample_extension2': [{
'metadata': {
'name': 'test_sample_extension2',
'summary': 'my summary',
'version': '1.1.0b1'
}
}],
'test_sample_extension3': [{
'metadata': {
'name': 'test_sample_extension3',
'summary': 'my summary',
'version': '2.15.0'
}
}]
}
with mock.patch('azure.cli.core.extension.operations.get_index_extensions',
return_value=sample_index_extensions):
res = list_available_extensions(cli_ctx=self.cmd.cli_ctx)
self.assertIsInstance(res, list)
self.assertEqual(len(res), len(sample_index_extensions))
self.assertEqual(res[0]['name'], 'ml')
self.assertEqual(res[0]['summary'], 'AzureMachineLearningWorkspaces Extension')
self.assertEqual(res[0]['version'], '2.0.0a1')
self.assertEqual(res[0]['preview'], True)
self.assertEqual(res[0]['experimental'], False)
self.assertEqual(res[1]['name'], 'test_sample_extension1')
self.assertEqual(res[1]['version'], '1.15.0')
self.assertEqual(res[1]['preview'], True)
self.assertEqual(res[2]['name'], 'test_sample_extension2')
self.assertEqual(res[2]['version'], '1.1.0b1')
self.assertEqual(res[2]['preview'], True)
self.assertEqual(res[3]['name'], 'test_sample_extension3')
self.assertEqual(res[3]['version'], '2.15.0')
self.assertEqual(res[3]['preview'], False)

def test_add_list_show_preview_extension(self):
test_ext_source = _get_test_data_file('ml-2.0.0a1-py3-none-any.whl')
with mock.patch('azure.cli.core.extension.operations.logger') as mock_logger:
add_extension(cmd=self.cmd, source=test_ext_source)
call_args = mock_logger.warning.call_args
self.assertEqual("The installed extension '%s' is in preview.",call_args[0][0])
self.assertEqual("ml", call_args[0][1])
self.assertEqual(mock_logger.warning.call_count, 1)
ext = show_extension("ml")
self.assertEqual(ext["name"], "ml")
self.assertEqual(ext["version"], "2.0.0a1")
remove_extension("ml")

@mock.patch('sys.stdin.isatty', return_value=True)
def test_ext_dynamic_install_config_tty(self, _):
from azure.cli.core.extension.dynamic_install import _get_extension_use_dynamic_install_config
Expand Down

0 comments on commit ef8ec47

Please sign in to comment.