Skip to content

Commit

Permalink
Extension suppression (Azure#5739)
Browse files Browse the repository at this point in the history
  • Loading branch information
derekbekoe authored Mar 6, 2018
1 parent 5920090 commit b56563f
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 11 deletions.
26 changes: 26 additions & 0 deletions doc/authoring_command_modules/authoring_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ The document provides instructions and guidelines on how to author individual co

[14. Registering Enum Arguments](#registering-enums)

[15. Preventing particular extensions from being loading](#extension-suppression)

Authoring Commands
=============================

Expand Down Expand Up @@ -698,3 +700,27 @@ with self.argument_context('mymod') as c:
```

Above are two examples of how this can be used. In the first instance, an Enum model is reflected from the SDK. In the second instance, a custom choice list is provided. This is preferable to using the native `argparse.choices` kwarg because the choice lists generated by `get_enum_type` will be case insensitive.

## Extension Suppression

It is possible for a command module to suppress specific extensions from being loaded.

This is useful for commands that were once extensions that have now moved inside a command module.

Here, we suppress an extension by name and also by version.

This will allow the extension to be published in the future with the same name and a newer version that will not be suppressed.

This is great for experimental extensions that periodically get incorporated into the product.

```Python
class MyCommandsLoader(AzCommandsLoader):

def __init__(self, cli_ctx=None):
from azure.cli.core import ModExtensionSuppress
# Suppress myextension up to and including version 0.2.0
super(MyCommandsLoader, self).__init__(cli_ctx=cli_ctx,
suppress_extension=ModExtensionSuppress(__name__, 'myextension', '0.2.0',
reason='These commands are now in the CLI.',
recommend_remove=True))
```
1 change: 1 addition & 0 deletions src/azure-cli-core/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Release History
2.0.29
++++++
* Support Autorest 3.0 based SDKs
* Support mechanism for a command module to suppress the loading of particular extensions.

2.0.28
++++++
Expand Down
80 changes: 72 additions & 8 deletions src/azure-cli-core/azure/cli/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import os
import sys
import timeit
from pkg_resources import parse_version

from knack.arguments import ArgumentsContext
from knack.cli import CLI
Expand Down Expand Up @@ -105,7 +106,7 @@ def load_command_table(self, args):
from azure.cli.core.commands import (
_load_module_command_loader, _load_extension_command_loader, BLACKLISTED_MODS, ExtensionCommandSource)
from azure.cli.core.extension import (
get_extension_names, get_extension_path, get_extension_modname)
get_extensions, get_extension_path, get_extension_modname)

cmd_to_mod_map = {}

Expand Down Expand Up @@ -145,12 +146,24 @@ def _update_command_table_from_modules(args):
"(note: there's always an overhead with the first module loaded)",
cumulative_elapsed_time)

def _update_command_table_from_extensions():
def _update_command_table_from_extensions(ext_suppressions):

extensions = get_extension_names()
def _handle_extension_suppressions(extensions):
filtered_extensions = []
for ext in extensions:
should_include = True
for suppression in ext_suppressions:
if should_include and suppression.handle_suppress(ext):
should_include = False
if should_include:
filtered_extensions.append(ext)
return filtered_extensions

extensions = get_extensions()
if extensions:
logger.debug("Found %s extensions: %s", len(extensions), extensions)
for ext_name in extensions:
logger.debug("Found %s extensions: %s", len(extensions), [e.name for e in extensions])
allowed_extensions = _handle_extension_suppressions(extensions)
for ext_name in [e.name for e in allowed_extensions]:
ext_dir = get_extension_path(ext_name)
sys.path.append(ext_dir)
try:
Expand All @@ -173,11 +186,38 @@ def _update_command_table_from_extensions():
logger.warning("Unable to load extension '%s'. Use --debug for more information.", ext_name)
logger.debug(traceback.format_exc())

def _wrap_suppress_extension_func(func, ext):
""" Wrapper method to handle centralization of log messages for extension filters """
res = func(ext)
should_suppress = res
reason = "Use --debug for more information."
if isinstance(res, tuple):
should_suppress, reason = res
suppress_types = (bool, type(None))
if not isinstance(should_suppress, suppress_types):
raise ValueError("Command module authoring error: "
"Valid extension suppression values are {} in {}".format(suppress_types, func))
if should_suppress:
logger.warning("Extension %s (%s) has been suppressed. %s",
ext.name, ext.version, reason)
logger.debug("Extension %s (%s) suppressed from being loaded due "
"to %s", ext.name, ext.version, func)
return should_suppress

def _get_extension_suppressions(mod_loaders):
res = []
for m in mod_loaders:
sup = getattr(m, 'suppress_extension', None)
if sup and isinstance(sup, ModExtensionSuppress):
res.append(sup)
return res

_update_command_table_from_modules(args)
try:
ext_suppressions = _get_extension_suppressions(self.loaders)
# We always load extensions even if the appropriate module has been loaded
# as an extension could override the commands already loaded.
_update_command_table_from_extensions()
_update_command_table_from_extensions(ext_suppressions)
except Exception: # pylint: disable=broad-except
logger.warning("Unable to load extensions. Use --debug for more information.")
logger.debug(traceback.format_exc())
Expand Down Expand Up @@ -206,10 +246,33 @@ def load_arguments(self, command):
loader._update_command_definitions() # pylint: disable=protected-access


class AzCommandsLoader(CLICommandsLoader):
class ModExtensionSuppress(object): # pylint: disable=too-few-public-methods

def __init__(self, mod_name, suppress_extension_name, suppress_up_to_version, reason=None, recommend_remove=False):
self.mod_name = mod_name
self.suppress_extension_name = suppress_extension_name
self.suppress_up_to_version = suppress_up_to_version
self.reason = reason
self.recommend_remove = recommend_remove

def handle_suppress(self, ext):
should_suppress = ext.name == self.suppress_extension_name and ext.version and \
parse_version(ext.version) <= parse_version(self.suppress_up_to_version)
if should_suppress:
reason = self.reason or "Use --debug for more information."
logger.warning("Extension %s (%s) has been suppressed. %s",
ext.name, ext.version, reason)
logger.debug("Extension %s (%s) suppressed from being loaded due "
"to %s", ext.name, ext.version, self.mod_name)
if self.recommend_remove:
logger.warning("Remove this extension with 'az extension remove --name %s'", ext.name)
return should_suppress


class AzCommandsLoader(CLICommandsLoader): # pylint: disable=too-many-instance-attributes

def __init__(self, cli_ctx=None, min_profile=None, max_profile='latest',
command_group_cls=None, argument_context_cls=None,
command_group_cls=None, argument_context_cls=None, suppress_extension=None,
**kwargs):
from azure.cli.core.commands import AzCliCommand, AzCommandGroup, AzArgumentContext

Expand All @@ -218,6 +281,7 @@ def __init__(self, cli_ctx=None, min_profile=None, max_profile='latest',
excluded_command_handler_args=EXCLUDED_PARAMS)
self.min_profile = min_profile
self.max_profile = max_profile
self.suppress_extension = suppress_extension
self.module_kwargs = kwargs
self.command_name = None
self.skip_applicability = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import mock
import unittest
from collections import namedtuple

from azure.cli.core import AzCommandsLoader, MainCommandsLoader
from azure.cli.core.commands import ExtensionCommandSource
Expand Down Expand Up @@ -150,8 +151,10 @@ def _mock_iter_modules(_):
def _mock_extension_modname(ext_name, ext_dir):
return ext_name

def _mock_get_extension_names():
return [__name__ + '.ExtCommandsLoader', __name__ + '.Ext2CommandsLoader']
def _mock_get_extensions():
MockExtension = namedtuple('Extension', ['name'])
return [MockExtension(name=__name__ + '.ExtCommandsLoader'),
MockExtension(name=__name__ + '.Ext2CommandsLoader')]

def _mock_load_command_loader(loader, args, name, prefix):

Expand Down Expand Up @@ -199,7 +202,7 @@ def load_command_table(self, args):
@mock.patch('pkgutil.iter_modules', _mock_iter_modules)
@mock.patch('azure.cli.core.commands._load_command_loader', _mock_load_command_loader)
@mock.patch('azure.cli.core.extension.get_extension_modname', _mock_extension_modname)
@mock.patch('azure.cli.core.extension.get_extension_names', _mock_get_extension_names)
@mock.patch('azure.cli.core.extension.get_extensions', _mock_get_extensions)
def test_register_command_from_extension(self):

from azure.cli.core.commands import _load_command_loader
Expand Down

0 comments on commit b56563f

Please sign in to comment.