Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions src/azure-cli-core/azure/cli/core/extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@

EXTENSIONS_MOD_PREFIX = 'azext_'

WHL_METADATA_FILENAME = 'metadata.json'
EGG_INFO_METADATA_FILE_NAME = 'PKG-INFO' # used for dev packages
AZEXT_METADATA_FILENAME = 'azext_metadata.json'

EXT_METADATA_MINCLICOREVERSION = 'azext.minCliCoreVersion'
Expand Down Expand Up @@ -135,24 +133,34 @@ def get_version(self):

def get_metadata(self):
from glob import glob

metadata = {}
ext_dir = self.path or get_extension_path(self.name)

if not ext_dir or not os.path.isdir(ext_dir):
return None
info_dirs = glob(os.path.join(ext_dir, self.name.replace('-', '_') + '-' + '*.dist-info'))

# include *.egg-info and *.dist-info
info_dirs = glob(os.path.join(ext_dir, self.name.replace('-', '_') + '*.*-info'))
if not info_dirs:
return None

azext_metadata = WheelExtension.get_azext_metadata(ext_dir)
if azext_metadata:
metadata.update(azext_metadata)

for dist_info_dirname in info_dirs:
try:
ext_whl_metadata = pkginfo.Wheel(dist_info_dirname)
if dist_info_dirname.endswith('.egg-info'):
ext_whl_metadata = pkginfo.Develop(dist_info_dirname)
elif dist_info_dirname.endswith('.dist-info'):
ext_whl_metadata = pkginfo.Wheel(dist_info_dirname)
else:
raise ValueError()

if self.name == ext_whl_metadata.name:
metadata.update(vars(ext_whl_metadata))
except ValueError:
logger.warning('extension % contains invalid metadata for Python Package', self.name)
logger.warning('extension %s contains invalid metadata for Python Package', self.name)

return metadata

Expand All @@ -179,13 +187,13 @@ def get_all():
if os.path.isdir(EXTENSIONS_DIR):
for ext_name in os.listdir(EXTENSIONS_DIR):
ext_path = os.path.join(EXTENSIONS_DIR, ext_name)
pattern = os.path.join(ext_path, '*.dist-info')
pattern = os.path.join(ext_path, '*.*-info') # include *.egg-info and *.dist-info
if os.path.isdir(ext_path) and glob(pattern):
exts.append(WheelExtension(ext_name, ext_path))
if os.path.isdir(EXTENSIONS_SYS_DIR):
for ext_name in os.listdir(EXTENSIONS_SYS_DIR):
ext_path = os.path.join(EXTENSIONS_SYS_DIR, ext_name)
pattern = os.path.join(ext_path, '*.dist-info')
pattern = os.path.join(ext_path, '*.*-info') # include *.egg-info and *.dist-info
if os.path.isdir(ext_path) and glob(pattern):
ext = WheelExtension(ext_name, ext_path)
if ext not in exts:
Expand All @@ -205,7 +213,11 @@ def get_metadata(self):
ext_dir = self.path
if not ext_dir or not os.path.isdir(ext_dir):
return None

egg_info_dirs = [f for f in os.listdir(ext_dir) if f.endswith('.egg-info')]
if not egg_info_dirs:
return None

azext_metadata = DevExtension.get_azext_metadata(ext_dir)
if azext_metadata:
metadata.update(azext_metadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,21 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import unittest
import tempfile
import shutil


def get_test_data_file(filename):
return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', filename)


class ExtensionTypeTestMixin(unittest.TestCase):

def setUp(self):
self.ext_dir = tempfile.mkdtemp()

def tearDown(self):
shutil.rmtree(self.ext_dir, ignore_errors=True)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import tempfile
import zipfile
import tarfile
import shutil
import unittest

from azure.cli.core.extension import DevExtension
from azure.cli.core.extension.tests.latest import ExtensionTypeTestMixin, get_test_data_file


class TestWheelTypeExtensionMetadata(ExtensionTypeTestMixin):

def test_reading_wheel_type_extension_from_develop_mode(self):
"""
Test develop type extension.
For scenario that user are developing extension via azdev
"""

source_code_packaged = get_test_data_file('hello-0.1.0.tar.gz')

with tarfile.open(source_code_packaged, 'r:gz') as tar:
tar.extractall(self.ext_dir)

ext_name, ext_version = 'hello', '0.1.0'

ext_extension = DevExtension(ext_name, os.path.join(self.ext_dir, ext_name + '-' + ext_version))
metadata = ext_extension.get_metadata() # able to read metadata from source code

# assert Python metadata
self.assertEqual(metadata['name'], ext_name)
self.assertEqual(metadata['version'], ext_version)
self.assertEqual(metadata['author'], 'Microsoft Corporation')
self.assertNotIn('metadata.json', os.listdir(os.path.join(self.ext_dir, ext_name + '-' + ext_version)))

# assert Azure CLI extended metadata
self.assertTrue(metadata['azext.isPreview'])
self.assertTrue(metadata['azext.isExperimental'])
Comment on lines +42 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember these two tags are not supposed to be both True in the future. @zhoxing-ms for confirmation.

Copy link
Contributor

@zhoxing-ms zhoxing-ms May 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, according to the previous design of @jiasli for isExperimental, an error will be reported if both isPreview and isExperimental are True. @jiasli Please help to see if my understanding is correct~

Copy link
Contributor Author

@haroldrandom haroldrandom May 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing this, I just learnt that.
I think the class WheelExtension and DevExtension is to represent data model not validation. Validation should be a higher level business.
This PR is about to test reading the metadata correctly, not validation for isExperimental and isPreview unless we decide to make validation here.

Please @jiasli make sure that the concern above has been made in CLI.

self.assertEqual(metadata['azext.minCliCoreVersion'], '2.0.67')
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import tempfile
import zipfile
import tarfile
import shutil
import unittest

from azure.cli.core.extension import WheelExtension
from azure.cli.core.extension.tests.latest import ExtensionTypeTestMixin, get_test_data_file


class TestWheelTypeExtensionMetadata(ExtensionTypeTestMixin):

def test_reading_wheel_type_0_30_0_extension_metadata(self):
"""
Test wheel==0.30.0 containing metadata.json and we can handle it properly.
For scenario like 'az extenion add'.
"""

# this wheel contains metadata.json and METADATA
wheel_0_30_0_packed = get_test_data_file('wheel_0_30_0_packed_extension-0.1.0-py3-none-any.whl')

zf = zipfile.ZipFile(wheel_0_30_0_packed)
zf.extractall(self.ext_dir)

ext_name, ext_version = 'hello', '0.1.0'

whl_extension = WheelExtension(ext_name, self.ext_dir)
metadata = whl_extension.get_metadata() # able to read metadata from wheel==0.30.0 built extension

# wheel type extension generates .dist-info
dist_info = ext_name + '-' + ext_version + '.dist-info'

# assert Python metadata
self.assertEqual(metadata['name'], ext_name)
self.assertEqual(metadata['version'], ext_version)
self.assertEqual(metadata['author'], 'Microsoft Corporation')
self.assertIn('metadata.json', os.listdir(os.path.join(self.ext_dir, dist_info)))

# assert Azure CLI extended metadata
self.assertTrue(metadata['azext.isPreview'])
self.assertTrue(metadata['azext.isExperimental'])
self.assertEqual(metadata['azext.minCliCoreVersion'], '2.0.67')

def test_reading_wheel_type_0_31_0_extension_metadata(self):
"""
Test wheel>=0.31.0 not containing metadata.json but we can still handle it properly.
For scenario like 'az extenion add'.
"""

# this wheel contains METADATA only
wheel_0_31_0_packed = get_test_data_file('wheel_0_31_0_packed_extension-0.1.0-py3-none-any.whl')

zf = zipfile.ZipFile(wheel_0_31_0_packed)
zf.extractall(self.ext_dir)

ext_name, ext_version = 'hello', '0.1.0'

whl_extension = WheelExtension(ext_name, self.ext_dir)
metadata = whl_extension.get_metadata() # able to read metadata from wheel==0.30.0 built extension

# wheel type extension generates .dist-info
dist_info = ext_name + '-' + ext_version + '.dist-info'

# assert Python metadata
self.assertEqual(metadata['name'], ext_name)
self.assertEqual(metadata['version'], ext_version)
self.assertEqual(metadata['author'], 'Microsoft Corporation')
self.assertNotIn('metadata.json', os.listdir(os.path.join(self.ext_dir, dist_info)))

# assert Azure CLI extended metadata
self.assertTrue(metadata['azext.isPreview'])
self.assertTrue(metadata['azext.isExperimental'])
self.assertEqual(metadata['azext.minCliCoreVersion'], '2.0.67')

def test_reading_wheel_type_extension_from_develop_mode(self):
"""
Test wheel type extension but installing from source code.
For scenario that user are developing extension via 'pip install -e' directlly
and load it from _CUSTOM_EXT_DIR or GLOBAL_CONFIG_DIR
"""

source_code_packaged = get_test_data_file('hello-0.1.0.tar.gz')

with tarfile.open(source_code_packaged, 'r:gz') as tar:
tar.extractall(self.ext_dir)

ext_name, ext_version = 'hello', '0.1.0'

ext_extension = WheelExtension(ext_name, os.path.join(self.ext_dir, ext_name + '-' + ext_version))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not use DevExtension?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to test WheelExtension class not DevExtension.
The user using this way to develop Azure CLI extension is to hack the machenism. I think it's doable but not recommended. And supporting .egg-info is just one month ago.

metadata = ext_extension.get_metadata() # able to read metadata from source code even in wheel type extension

# assert Python metadata
self.assertEqual(metadata['name'], ext_name)
self.assertEqual(metadata['version'], ext_version)
self.assertEqual(metadata['author'], 'Microsoft Corporation')
self.assertNotIn('metadata.json', os.listdir(os.path.join(self.ext_dir, ext_name + '-' + ext_version)))

# assert Azure CLI extended metadata
self.assertTrue(metadata['azext.isPreview'])
self.assertTrue(metadata['azext.isExperimental'])
self.assertEqual(metadata['azext.minCliCoreVersion'], '2.0.67')