From c02ed91c2dcd3e41a446a8bf87534e1315369815 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 12:46:14 +0800 Subject: [PATCH 01/19] Add command that collects upcoming breaking changes --- azdev/commands.py | 3 + azdev/operations/breaking_change/__init__.py | 301 ++++++++++++++++++ .../breaking_change/markdown_template.jinja2 | 28 ++ azdev/params.py | 10 + 4 files changed, 342 insertions(+) create mode 100644 azdev/operations/breaking_change/__init__.py create mode 100644 azdev/operations/breaking_change/markdown_template.jinja2 diff --git a/azdev/commands.py b/azdev/commands.py index b646b744f..f66d4139c 100644 --- a/azdev/commands.py +++ b/azdev/commands.py @@ -77,3 +77,6 @@ def operation_group(name): with CommandGroup(self, 'extension', operation_group('help')) as g: g.command('generate-docs', 'generate_extension_ref_docs') + + with CommandGroup(self, 'breaking-change', operation_group('breaking_change')) as g: + g.command('collect', 'collect_upcoming_breaking_changes') diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py new file mode 100644 index 000000000..d145ba45a --- /dev/null +++ b/azdev/operations/breaking_change/__init__.py @@ -0,0 +1,301 @@ +from collections import defaultdict +from importlib import import_module +import packaging.version +import time + +from knack.log import get_logger + +from azdev.operations.statistics import _create_invoker_and_load_cmds # pylint: disable=protected-access +from azdev.utilities import require_azure_cli, get_path_table, display, heading, output + +logger = get_logger(__name__) + + +class BreakingChangeItem: + def __init__(self, module, command, detail, target_version): + self.module = module + self.command = command + self.detail = detail + self.target_version = target_version + + +def _calc_selected_mod_names(modules=None): + # allow user to run only on CLI or extensions + cli_only = modules == ['CLI'] + ext_only = modules == ['EXT'] + if cli_only or ext_only: + modules = None + + selected_modules = get_path_table(include_only=modules) + + if cli_only: + selected_modules['ext'] = {} + if ext_only: + selected_modules['core'] = {} + selected_modules['mod'] = {} + + if not any(selected_modules.values()): + logger.warning('No commands selected to check.') + + selected_mod_names = list(selected_modules['mod'].keys()) + selected_mod_names += list(selected_modules['ext'].keys()) + selected_mod_names += list(selected_modules['core'].keys()) + return selected_mod_names + + +def _load_commands(): + start = time.time() + display('Initializing with loading command table...') + from azure.cli.core import get_default_cli # pylint: disable=import-error + az_cli = get_default_cli() + + # load commands, args, and help + _create_invoker_and_load_cmds(az_cli) + + stop = time.time() + logger.info('Commands loaded in %i sec', stop - start) + display('Commands loaded in {} sec'.format(stop - start)) + command_loader = az_cli.invocation.commands_loader + + if not command_loader.command_table: + logger.warning('No commands selected to check.') + return command_loader + + +def _handle_custom_breaking_changes(module, command): + """ + Collect Custom Pre-Announcement defined in `_breaking_change.py` + :param module: module name + :param command: command name + :return: A generated returns Custom Pre-Announcements defined in `_breaking_change.py` + """ + from azure.cli.core.breaking_change import upcoming_breaking_changes + yield from _handle_custom_breaking_change(module, command, upcoming_breaking_changes.get(command)) + for key in upcoming_breaking_changes: + if key.startswith(command + ':'): + yield from _handle_custom_breaking_change(module, command, upcoming_breaking_changes[key]) + + +def _handle_custom_breaking_change(module, command, breaking_change): + """ + Handle a BreakingChange item defined in `_breaking_change.py`. We need this method because the item stored could + be a list or object + """ + from azure.cli.core.breaking_change import BreakingChange + if isinstance(breaking_change, str): + yield BreakingChangeItem(module, command, breaking_change, None) + elif isinstance(breaking_change, BreakingChange): + yield BreakingChangeItem(module, command, breaking_change.message, breaking_change.target_version.version()) + elif isinstance(breaking_change, list): + for bc in breaking_change: + yield from _handle_custom_breaking_change(module, command, bc) + + +def _handle_command_deprecation(module, command, deprecate_info): + redirect = f' and replaced by `{deprecate_info.redirect}`' if deprecate_info.redirect else '' + version = deprecate_info.expiration if hasattr(deprecate_info, "expiration") else None + expiration = f' This command would be removed in {version}.' if version else '' + yield BreakingChangeItem(module, command, f'This command is deprecated{redirect}.{expiration}', version) + + +def _calc_target_of_arg_deprecation(arg_name, arg_settings): + from knack.deprecation import Deprecated + option_str_list = [] + depr = arg_settings.get('deprecate_info') + for option in arg_settings.get('option_list', []): + if isinstance(option, str): + option_str_list.append(option) + elif isinstance(option, Deprecated): + option_str_list.append(option.target) + if option_str_list: + return '/'.join(option_str_list) + elif hasattr(depr, 'target'): + return depr.target + else: + return arg_name + + +def _handle_arg_deprecation(module, command, target, deprecation_info): + redirect = f' and replaced by `{deprecation_info.redirect}`' if deprecation_info.redirect else '' + version = deprecation_info.expiration if hasattr(deprecation_info, "expiration") else None + expiration = f' This parameter would be removed in {version}.' if version else '' + yield BreakingChangeItem(module, command, f'This parameter `{target}` is deprecated{redirect}.{expiration}', + version) + + +def _handle_options_deprecation(module, command, options): + from knack.deprecation import Deprecated + deprecate_option_map = defaultdict(lambda: []) + for option in options: + if isinstance(option, Deprecated): + key = f'{option.redirect}|{option.expiration}|{option.hide}' + deprecate_option_map[key].append(option) + for _, depr_list in deprecate_option_map.items(): + target = '/'.join([depr.target for depr in depr_list]) + depr = depr_list[0] + redirect = f' and replaced by `{depr.redirect}`' if depr.redirect else '' + version = depr.expiration if hasattr(depr, "expiration") else None + expiration = f' This command would be removed in {version}.' if version else '' + yield BreakingChangeItem(module, command, f'This option `{target}` is deprecated{redirect}.{expiration}', + version) + + +def _handle_command_breaking_changes(module, command, command_info): + if hasattr(command_info, "deprecate_info") and command_info.deprecate_info: + yield from _handle_command_deprecation(module, command, command_info.deprecate_info) + + for argument_name, argument in command_info.arguments.items(): + arg_settings = argument.type.settings + depr = arg_settings.get('deprecate_info') + if depr: + bc_target = _calc_target_of_arg_deprecation(argument_name, arg_settings) + yield from _handle_arg_deprecation(module, command, bc_target, depr) + yield from _handle_options_deprecation(module, command, arg_settings.get('options', [])) + + yield from _handle_custom_breaking_changes(module, command) + + +def _handle_command_group_deprecation(module, command, deprecate_info): + redirect = f' and replaced by `{deprecate_info.redirect}`' if deprecate_info.redirect else '' + version = deprecate_info.expiration if hasattr(deprecate_info, "expiration") else None + expiration = f' This command would be removed in {version}.' if version else '' + yield BreakingChangeItem(module, command, f'This command group is deprecated{redirect}.{expiration}', version) + + +def _handle_command_group_breaking_changes(module, command_group_name, command_group_info): + if hasattr(command_group_info, 'group_kwargs') and command_group_info.group_kwargs.get('deprecate_info'): + yield from _handle_command_group_deprecation(module, command_group_name, + command_group_info.group_kwargs.get('deprecate_info')) + + yield from _handle_custom_breaking_changes(module, command_group_name) + + +def _get_module_name(loader): + module_source = next(iter(loader.command_table.values())).command_source + if isinstance(module_source, str): + return module_source + else: + return module_source.extension_name + + +def _iter_and_prepare_module_loader(command_loader, selected_mod_names): + for loader in command_loader.loaders: + module_path = loader.__class__.__module__ + module_name = _get_module_name(loader) + if module_name not in selected_mod_names: + continue + + _breaking_change_module = f'{module_path}._breaking_change' + try: + import_module(_breaking_change_module) + except ImportError: + pass + loader.skip_applicability = True + + yield module_name, loader + + +def _handle_module(module, loader, main_loader): + start = time.time() + + for command, command_info in loader.command_table.items(): + main_loader.load_arguments(command) + + yield from _handle_command_breaking_changes(module, command, command_info) + + for command_group_name, command_group in loader.command_group_table.items(): + yield from _handle_command_group_breaking_changes(module, command_group_name, command_group) + + stop = time.time() + logger.info('Module %s finished in %i sec', module, stop - start) + display('Module {} finished loaded in {} sec'.format(module, stop - start)) + + +def _handle_core(): + start = time.time() + core_module = 'azure.cli.core' + _breaking_change_module = f'{core_module}._breaking_change' + try: + import_module(_breaking_change_module) + except ImportError: + pass + + yield from _handle_custom_breaking_changes('core', 'core') + + stop = time.time() + logger.info('Core finished in %i sec', stop - start) + display('Core finished loaded in {} sec'.format(stop - start)) + + +def _handle_upcoming_breaking_changes(selected_mod_names): + command_loader = _load_commands() + + if 'core' in selected_mod_names or 'azure-cli-core' in selected_mod_names: + yield from _handle_core() + + for module, loader in _iter_and_prepare_module_loader(command_loader, selected_mod_names): + yield from _handle_module(module, loader, command_loader) + + +def _filter_breaking_changes(iterator, max_version=None): + if not max_version: + yield from iterator + try: + max_version = packaging.version.parse(max_version) + except packaging.version.InvalidVersion: + logger.warning(f'Invalid target version: {max_version}; ' + f'Will present all upcoming breaking changes as alternative.') + for item in iterator: + if item.target_version: + try: + target_version = packaging.version.parse(item.target_version) + if target_version <= max_version: + yield item + except packaging.version.InvalidVersion: + logger.warning(f'Invalid version from `{item.command}`: {item.target_version}') + + +def _group_breaking_change_items(iterator, group_by_version=False): + if group_by_version: + version_value = lambda: [] + command_value = lambda: defaultdict(version_value) + else: + command_value = lambda: [] + module_value = lambda: defaultdict(command_value) + upcoming_breaking_changes = defaultdict(module_value) + for item in iterator: + version = item.target_version if item.target_version else 'Unspecific' + if group_by_version: + upcoming_breaking_changes[item.module][item.command][version].append(item.detail) + else: + upcoming_breaking_changes[item.module][item.command].append(item.detail) + return upcoming_breaking_changes + + +def collect_upcoming_breaking_changes(modules=None, target_version='NextWindow', group_by_version=None, + output_format='structure'): + if target_version == 'NextWindow': + from azure.cli.core.breaking_change import NEXT_BREAKING_CHANGE_RELEASE + target_version = NEXT_BREAKING_CHANGE_RELEASE + elif target_version.lower() == 'none': + target_version = None + + require_azure_cli() + + selected_mod_names = _calc_selected_mod_names(modules) + + if selected_mod_names: + display('Modules selected: {}\n'.format(', '.join(selected_mod_names))) + + heading('Collecting Breaking Change Pre-announcement') + breaking_changes = _handle_upcoming_breaking_changes(selected_mod_names) + breaking_changes = _filter_breaking_changes(breaking_changes, target_version) + breaking_changes = _group_breaking_change_items(breaking_changes, group_by_version) + if output_format == 'structure': + return breaking_changes + elif output_format == 'markdown': + from jinja2 import Environment, PackageLoader + env = Environment(loader=PackageLoader('azdev', 'operations/breaking_change'), + trim_blocks=True) + template = env.get_template('markdown_template.jinja2') + output(template.render({'module_bc': breaking_changes})) diff --git a/azdev/operations/breaking_change/markdown_template.jinja2 b/azdev/operations/breaking_change/markdown_template.jinja2 new file mode 100644 index 000000000..613c7d49f --- /dev/null +++ b/azdev/operations/breaking_change/markdown_template.jinja2 @@ -0,0 +1,28 @@ +# Upcoming breaking changes in Azure CLI + +{% for module, command_bc in module_bc.items() -%} +## {{ module }} + +{% for command, multi_version_bcs in command_bc.items() -%} +{% if not (module == 'core' and command == 'core') -%} +### `{{ command }}` + +{% endif -%} +{% if multi_version_bcs is mapping -%} +{% for version, bcs in multi_version_bcs | dictsort -%} +###{%- if not (module == 'core' and command == 'core') -%}#{%- endif %} Deprecated in {{ version }} + +{% for bc in bcs -%} +- {{ bc }} +{% endfor %} + +{% endfor -%} +{% else -%} + +{% for bc in multi_version_bcs -%} +- {{ bc }} +{% endfor %} + +{% endif -%} +{% endfor -%} +{% endfor -%} \ No newline at end of file diff --git a/azdev/params.py b/azdev/params.py index 511dd1e55..32253a6ce 100644 --- a/azdev/params.py +++ b/azdev/params.py @@ -208,3 +208,13 @@ def load_arguments(self, _): 'If the base directory does not exist, it will be created') c.argument('output_type', choices=['xml', 'html', 'text', 'man', 'latex'], default="xml", help='Output type of the generated docs.') + + with ArgumentsContext(self, 'breaking-change collect') as c: + c.positional('modules', modules_type) + c.argument('target_version', default='NextWindow', + help='Only the breaking changes scheduled prior to the specified version will be displayed. ' + 'The value could be `NextWindow`, `None` or a specified version like `3.0.0`') + c.argument('group_by_version', action='store_true', + help='If specified, breaking changes would be grouped by their target version as well.') + c.argument('output_format', choices=['structure', 'markdown'], default='structure', + help='Output format of the collected breaking changes.') From b956a213584e82757744906ac68308b21724ef1a Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 13:00:47 +0800 Subject: [PATCH 02/19] Fix when specifying version. Add version --- HISTORY.rst | 4 ++++ azdev/__init__.py | 2 +- azdev/operations/breaking_change/__init__.py | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2387d8398..2ae32d48c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,10 @@ Release History =============== +0.1.68 +++++++ +* `azdev breaking-change collect`: New command to collect upcoming breaking changes from codebase. + 0.1.67 ++++++ * `azdev extension cal-next-version`: Justify preview/exp tag operation based on last version's tag and next version's stable/preview tag. diff --git a/azdev/__init__.py b/azdev/__init__.py index 5b8a3ab3c..92f9060d8 100644 --- a/azdev/__init__.py +++ b/azdev/__init__.py @@ -4,4 +4,4 @@ # license information. # ----------------------------------------------------------------------------- -__VERSION__ = '0.1.67' +__VERSION__ = '0.1.68' diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index d145ba45a..2897d7500 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -240,6 +240,7 @@ def _handle_upcoming_breaking_changes(selected_mod_names): def _filter_breaking_changes(iterator, max_version=None): if not max_version: yield from iterator + return try: max_version = packaging.version.parse(max_version) except packaging.version.InvalidVersion: From 533388ec56be36227f034673cfe0fe8d77404455 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 13:38:49 +0800 Subject: [PATCH 03/19] Add license header --- azdev/operations/breaking_change/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 2897d7500..9b209ba99 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -1,3 +1,8 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- from collections import defaultdict from importlib import import_module import packaging.version From 3c5eb76550839f87ead22fd6564d2e46a51939ba Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 13:47:30 +0800 Subject: [PATCH 04/19] Fix style --- azdev/operations/breaking_change/__init__.py | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 9b209ba99..56f21c911 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -3,11 +3,14 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ----------------------------------------------------------------------------- + +# pylint: disable=no-else-return + +import time from collections import defaultdict from importlib import import_module -import packaging.version -import time +import packaging.version from knack.log import get_logger from azdev.operations.statistics import _create_invoker_and_load_cmds # pylint: disable=protected-access @@ -247,21 +250,24 @@ def _filter_breaking_changes(iterator, max_version=None): yield from iterator return try: - max_version = packaging.version.parse(max_version) + parsed_max_version = packaging.version.parse(max_version) except packaging.version.InvalidVersion: - logger.warning(f'Invalid target version: {max_version}; ' - f'Will present all upcoming breaking changes as alternative.') + logger.warning(f'Invalid target version: %s; ' + f'Will present all upcoming breaking changes as alternative.', max_version) + yield from iterator + return for item in iterator: if item.target_version: try: target_version = packaging.version.parse(item.target_version) - if target_version <= max_version: + if target_version <= parsed_max_version: yield item except packaging.version.InvalidVersion: - logger.warning(f'Invalid version from `{item.command}`: {item.target_version}') + logger.warning(f'Invalid version from `%s`: %s', item.command, item.target_version) def _group_breaking_change_items(iterator, group_by_version=False): + # pylint: disable=unnecessary-lambda-assignment if group_by_version: version_value = lambda: [] command_value = lambda: defaultdict(version_value) @@ -305,3 +311,4 @@ def collect_upcoming_breaking_changes(modules=None, target_version='NextWindow', trim_blocks=True) template = env.get_template('markdown_template.jinja2') output(template.render({'module_bc': breaking_changes})) + return None From 779e966056e01f209033a1007a38c7bfe8aa421d Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 13:52:38 +0800 Subject: [PATCH 05/19] Fix style --- azdev/operations/breaking_change/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 56f21c911..22f7e557e 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -252,8 +252,8 @@ def _filter_breaking_changes(iterator, max_version=None): try: parsed_max_version = packaging.version.parse(max_version) except packaging.version.InvalidVersion: - logger.warning(f'Invalid target version: %s; ' - f'Will present all upcoming breaking changes as alternative.', max_version) + logger.warning('Invalid target version: %s; ' + 'Will present all upcoming breaking changes as alternative.', max_version) yield from iterator return for item in iterator: @@ -263,7 +263,7 @@ def _filter_breaking_changes(iterator, max_version=None): if target_version <= parsed_max_version: yield item except packaging.version.InvalidVersion: - logger.warning(f'Invalid version from `%s`: %s', item.command, item.target_version) + logger.warning('Invalid version from `%s`: %s', item.command, item.target_version) def _group_breaking_change_items(iterator, group_by_version=False): From 444a7e1ecd057af956d8e9f1b46ab1e1ff1a36c7 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 13:58:34 +0800 Subject: [PATCH 06/19] Fix style --- azdev/operations/breaking_change/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 22f7e557e..c6224d447 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -266,8 +266,8 @@ def _filter_breaking_changes(iterator, max_version=None): logger.warning('Invalid version from `%s`: %s', item.command, item.target_version) +# pylint: disable=unnecessary-lambda-assignment def _group_breaking_change_items(iterator, group_by_version=False): - # pylint: disable=unnecessary-lambda-assignment if group_by_version: version_value = lambda: [] command_value = lambda: defaultdict(version_value) From 914151b0b7163afa34039248ecfff351cab86d58 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 14:05:45 +0800 Subject: [PATCH 07/19] Fix style --- azdev/operations/breaking_change/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index c6224d447..4809ee9b2 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -269,12 +269,14 @@ def _filter_breaking_changes(iterator, max_version=None): # pylint: disable=unnecessary-lambda-assignment def _group_breaking_change_items(iterator, group_by_version=False): if group_by_version: - version_value = lambda: [] - command_value = lambda: defaultdict(version_value) + upcoming_breaking_changes = defaultdict( # module to command + lambda: defaultdict( # command to version + lambda: defaultdict( # version to list of breaking changes + lambda: []))) else: - command_value = lambda: [] - module_value = lambda: defaultdict(command_value) - upcoming_breaking_changes = defaultdict(module_value) + upcoming_breaking_changes = defaultdict( # module to command + lambda: defaultdict( # command to list of breaking changes + lambda: [])) for item in iterator: version = item.target_version if item.target_version else 'Unspecific' if group_by_version: From 3793ad8f515b7d3d36e40086277e0dfce064082e Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 17:26:28 +0800 Subject: [PATCH 08/19] Align with the design in doc. bar foo:NAME => bar foo.NAME --- azdev/operations/breaking_change/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 4809ee9b2..1b54f9e32 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -80,7 +80,7 @@ def _handle_custom_breaking_changes(module, command): from azure.cli.core.breaking_change import upcoming_breaking_changes yield from _handle_custom_breaking_change(module, command, upcoming_breaking_changes.get(command)) for key in upcoming_breaking_changes: - if key.startswith(command + ':'): + if key.startswith(command + '.'): yield from _handle_custom_breaking_change(module, command, upcoming_breaking_changes[key]) From 3f541531144618c655c5d485994b7086afcbdaf9 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Wed, 15 May 2024 09:52:04 +0800 Subject: [PATCH 09/19] Add new parameter --source to filter only deprecation info or only custom bc --- azdev/operations/breaking_change/__init__.py | 97 +++++++++++--------- azdev/params.py | 4 + 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 1b54f9e32..9572b676d 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -148,19 +148,20 @@ def _handle_options_deprecation(module, command, options): version) -def _handle_command_breaking_changes(module, command, command_info): - if hasattr(command_info, "deprecate_info") and command_info.deprecate_info: - yield from _handle_command_deprecation(module, command, command_info.deprecate_info) - - for argument_name, argument in command_info.arguments.items(): - arg_settings = argument.type.settings - depr = arg_settings.get('deprecate_info') - if depr: - bc_target = _calc_target_of_arg_deprecation(argument_name, arg_settings) - yield from _handle_arg_deprecation(module, command, bc_target, depr) - yield from _handle_options_deprecation(module, command, arg_settings.get('options', [])) - - yield from _handle_custom_breaking_changes(module, command) +def _handle_command_breaking_changes(module, command, command_info, source): + if source in ['all', 'deprecate']: + if hasattr(command_info, "deprecate_info") and command_info.deprecate_info: + yield from _handle_command_deprecation(module, command, command_info.deprecate_info) + + for argument_name, argument in command_info.arguments.items(): + arg_settings = argument.type.settings + depr = arg_settings.get('deprecate_info') + if depr: + bc_target = _calc_target_of_arg_deprecation(argument_name, arg_settings) + yield from _handle_arg_deprecation(module, command, bc_target, depr) + yield from _handle_options_deprecation(module, command, arg_settings.get('options', [])) + if source in ['all', 'custom']: + yield from _handle_custom_breaking_changes(module, command) def _handle_command_group_deprecation(module, command, deprecate_info): @@ -170,27 +171,38 @@ def _handle_command_group_deprecation(module, command, deprecate_info): yield BreakingChangeItem(module, command, f'This command group is deprecated{redirect}.{expiration}', version) -def _handle_command_group_breaking_changes(module, command_group_name, command_group_info): - if hasattr(command_group_info, 'group_kwargs') and command_group_info.group_kwargs.get('deprecate_info'): - yield from _handle_command_group_deprecation(module, command_group_name, - command_group_info.group_kwargs.get('deprecate_info')) +def _handle_command_group_breaking_changes(module, command_group_name, command_group_info, source): + if source in ['all', 'deprecate']: + if hasattr(command_group_info, 'group_kwargs') and command_group_info.group_kwargs.get('deprecate_info'): + yield from _handle_command_group_deprecation(module, command_group_name, + command_group_info.group_kwargs.get('deprecate_info')) - yield from _handle_custom_breaking_changes(module, command_group_name) + if source in ['all', 'custom']: + yield from _handle_custom_breaking_changes(module, command_group_name) -def _get_module_name(loader): - module_source = next(iter(loader.command_table.values())).command_source - if isinstance(module_source, str): - return module_source - else: - return module_source.extension_name +def _get_mod_ext_name(loader): + # There could be different name with module name in extension. + # For example, module name of `application-insights` is azext_applicationinsights + try: + module_source = next(iter(loader.command_table.values())).command_source + if isinstance(module_source, str): + return module_source + else: + return module_source.extension_name + except StopIteration: + logger.warning('There is no command in Loader(%s)', loader) + mod_path = loader.__class__.__module__ + mod_name = mod_path.rsplit('.', maxsplit=1)[-1] + mod_name = mod_name.replace('azext_', '', 1) + return mod_name def _iter_and_prepare_module_loader(command_loader, selected_mod_names): for loader in command_loader.loaders: module_path = loader.__class__.__module__ - module_name = _get_module_name(loader) - if module_name not in selected_mod_names: + module_name = module_path.rsplit('.', maxsplit=1)[-1] + if module_name and module_name not in selected_mod_names: continue _breaking_change_module = f'{module_path}._breaking_change' @@ -203,46 +215,47 @@ def _iter_and_prepare_module_loader(command_loader, selected_mod_names): yield module_name, loader -def _handle_module(module, loader, main_loader): +def _handle_module(module, loader, main_loader, source): start = time.time() for command, command_info in loader.command_table.items(): main_loader.load_arguments(command) - yield from _handle_command_breaking_changes(module, command, command_info) + yield from _handle_command_breaking_changes(module, command, command_info, source) for command_group_name, command_group in loader.command_group_table.items(): - yield from _handle_command_group_breaking_changes(module, command_group_name, command_group) + yield from _handle_command_group_breaking_changes(module, command_group_name, command_group, source) stop = time.time() logger.info('Module %s finished in %i sec', module, stop - start) display('Module {} finished loaded in {} sec'.format(module, stop - start)) -def _handle_core(): +def _handle_core(source): start = time.time() - core_module = 'azure.cli.core' - _breaking_change_module = f'{core_module}._breaking_change' - try: - import_module(_breaking_change_module) - except ImportError: - pass + if source in ['all', 'custom']: + core_module = 'azure.cli.core' + _breaking_change_module = f'{core_module}._breaking_change' + try: + import_module(_breaking_change_module) + except ImportError: + pass - yield from _handle_custom_breaking_changes('core', 'core') + yield from _handle_custom_breaking_changes('core', 'core') stop = time.time() logger.info('Core finished in %i sec', stop - start) display('Core finished loaded in {} sec'.format(stop - start)) -def _handle_upcoming_breaking_changes(selected_mod_names): +def _handle_upcoming_breaking_changes(selected_mod_names, source): command_loader = _load_commands() if 'core' in selected_mod_names or 'azure-cli-core' in selected_mod_names: - yield from _handle_core() + yield from _handle_core(source) for module, loader in _iter_and_prepare_module_loader(command_loader, selected_mod_names): - yield from _handle_module(module, loader, command_loader) + yield from _handle_module(module, loader, command_loader, source) def _filter_breaking_changes(iterator, max_version=None): @@ -286,7 +299,7 @@ def _group_breaking_change_items(iterator, group_by_version=False): return upcoming_breaking_changes -def collect_upcoming_breaking_changes(modules=None, target_version='NextWindow', group_by_version=None, +def collect_upcoming_breaking_changes(modules=None, target_version='NextWindow', source=None, group_by_version=None, output_format='structure'): if target_version == 'NextWindow': from azure.cli.core.breaking_change import NEXT_BREAKING_CHANGE_RELEASE @@ -302,7 +315,7 @@ def collect_upcoming_breaking_changes(modules=None, target_version='NextWindow', display('Modules selected: {}\n'.format(', '.join(selected_mod_names))) heading('Collecting Breaking Change Pre-announcement') - breaking_changes = _handle_upcoming_breaking_changes(selected_mod_names) + breaking_changes = _handle_upcoming_breaking_changes(selected_mod_names, source) breaking_changes = _filter_breaking_changes(breaking_changes, target_version) breaking_changes = _group_breaking_change_items(breaking_changes, group_by_version) if output_format == 'structure': diff --git a/azdev/params.py b/azdev/params.py index 32253a6ce..b5cf0e1b8 100644 --- a/azdev/params.py +++ b/azdev/params.py @@ -214,6 +214,10 @@ def load_arguments(self, _): c.argument('target_version', default='NextWindow', help='Only the breaking changes scheduled prior to the specified version will be displayed. ' 'The value could be `NextWindow`, `None` or a specified version like `3.0.0`') + c.argument('source', choices=['all', 'deprecate', 'custom'], default='all', + help='The source of pre-announced breaking changes. `deprecate` represents the breaking changes ' + 'marked through `deprecation_info`; `custom` represents the breaking changes announced in ' + '`breaking_change.py` file.') c.argument('group_by_version', action='store_true', help='If specified, breaking changes would be grouped by their target version as well.') c.argument('output_format', choices=['structure', 'markdown'], default='structure', From ab708ecbfebbccbb7bb38ff121aaa820155ee4a3 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 17 May 2024 14:13:21 +0800 Subject: [PATCH 10/19] Update version after merging --- azdev/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azdev/__init__.py b/azdev/__init__.py index 92f9060d8..6e2b9bff5 100644 --- a/azdev/__init__.py +++ b/azdev/__init__.py @@ -4,4 +4,4 @@ # license information. # ----------------------------------------------------------------------------- -__VERSION__ = '0.1.68' +__VERSION__ = '0.1.69' From 5cd392870cc320475685f57cfb8c30ac9b713333 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Thu, 30 May 2024 12:00:13 +0800 Subject: [PATCH 11/19] Update version number --- azdev/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azdev/__init__.py b/azdev/__init__.py index 6e2b9bff5..db461ad3e 100644 --- a/azdev/__init__.py +++ b/azdev/__init__.py @@ -4,4 +4,4 @@ # license information. # ----------------------------------------------------------------------------- -__VERSION__ = '0.1.69' +__VERSION__ = '0.1.70' From 5741d12b8e2370b9bfb096caa391736cf79a883c Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Thu, 30 May 2024 13:41:04 +0800 Subject: [PATCH 12/19] Abstract common calc_selected_mod_names log --- azdev/operations/breaking_change/__init__.py | 28 ++------------------ azdev/operations/command_change/__init__.py | 24 +++-------------- azdev/utilities/__init__.py | 3 ++- azdev/utilities/path.py | 28 +++++++++++++++++++- 4 files changed, 34 insertions(+), 49 deletions(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 9572b676d..7fb461177 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -14,7 +14,7 @@ from knack.log import get_logger from azdev.operations.statistics import _create_invoker_and_load_cmds # pylint: disable=protected-access -from azdev.utilities import require_azure_cli, get_path_table, display, heading, output +from azdev.utilities import require_azure_cli, get_path_table, display, heading, output, calc_selected_mod_names logger = get_logger(__name__) @@ -27,30 +27,6 @@ def __init__(self, module, command, detail, target_version): self.target_version = target_version -def _calc_selected_mod_names(modules=None): - # allow user to run only on CLI or extensions - cli_only = modules == ['CLI'] - ext_only = modules == ['EXT'] - if cli_only or ext_only: - modules = None - - selected_modules = get_path_table(include_only=modules) - - if cli_only: - selected_modules['ext'] = {} - if ext_only: - selected_modules['core'] = {} - selected_modules['mod'] = {} - - if not any(selected_modules.values()): - logger.warning('No commands selected to check.') - - selected_mod_names = list(selected_modules['mod'].keys()) - selected_mod_names += list(selected_modules['ext'].keys()) - selected_mod_names += list(selected_modules['core'].keys()) - return selected_mod_names - - def _load_commands(): start = time.time() display('Initializing with loading command table...') @@ -309,7 +285,7 @@ def collect_upcoming_breaking_changes(modules=None, target_version='NextWindow', require_azure_cli() - selected_mod_names = _calc_selected_mod_names(modules) + selected_mod_names = calc_selected_mod_names(modules) if selected_mod_names: display('Modules selected: {}\n'.format(', '.join(selected_mod_names))) diff --git a/azdev/operations/command_change/__init__.py b/azdev/operations/command_change/__init__.py index 07d140414..cc4d1dde8 100644 --- a/azdev/operations/command_change/__init__.py +++ b/azdev/operations/command_change/__init__.py @@ -10,7 +10,8 @@ from knack.log import get_logger import azure_cli_diff_tool -from azdev.utilities import display, require_azure_cli, heading, get_path_table, filter_by_git_diff +from azdev.utilities import display, require_azure_cli, heading, get_path_table, filter_by_git_diff, \ + calc_selected_mod_names from .custom import DiffExportFormat, get_commands_meta, STORED_DEPRECATION_KEY from .util import export_commands_meta, dump_command_tree, add_to_command_tree from ..statistics import _create_invoker_and_load_cmds, _get_command_source, \ @@ -134,26 +135,7 @@ def cmp_command_meta(base_meta_file, diff_meta_file, only_break=False, output_ty def export_command_tree(modules, output_file=None): require_azure_cli() - # allow user to run only on CLI or extensions - cli_only = modules == ['CLI'] - ext_only = modules == ['EXT'] - if cli_only or ext_only: - modules = None - - selected_modules = get_path_table(include_only=modules) - - if cli_only: - selected_modules['ext'] = {} - if ext_only: - selected_modules['core'] = {} - selected_modules['mod'] = {} - - if not any(selected_modules.values()): - logger.warning('No commands selected to check.') - - selected_mod_names = list(selected_modules['mod'].keys()) - selected_mod_names += list(selected_modules['ext'].keys()) - selected_mod_names += list(selected_modules['core'].keys()) + selected_mod_names = calc_selected_mod_names(modules) if selected_mod_names: display('Modules selected: {}\n'.format(', '.join(selected_mod_names))) diff --git a/azdev/utilities/__init__.py b/azdev/utilities/__init__.py index e00924255..6fd3da6c8 100644 --- a/azdev/utilities/__init__.py +++ b/azdev/utilities/__init__.py @@ -47,7 +47,7 @@ get_cli_repo_path, get_ext_repo_paths, get_path_table, - get_name_index + get_name_index, calc_selected_mod_names ) from .testing import test_cmd from .tools import ( @@ -93,4 +93,5 @@ 'require_virtual_env', 'require_azure_cli', 'diff_branches_detail', + 'calc_selected_mod_names', ] diff --git a/azdev/utilities/path.py b/azdev/utilities/path.py index 1eea797c1..72a840bc6 100644 --- a/azdev/utilities/path.py +++ b/azdev/utilities/path.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ----------------------------------------------------------------------------- - +import logging import os from glob import glob @@ -11,6 +11,8 @@ from .const import COMMAND_MODULE_PREFIX, EXTENSION_PREFIX, ENV_VAR_VIRTUAL_ENV +logger = logging.getLogger(__name__) + def extract_module_name(path): @@ -261,3 +263,27 @@ def _update_table(package_paths, key): raise CLIError('unrecognized modules: [ {} ]'.format(', '.join(include_only))) return table + + +def calc_selected_mod_names(modules=None): + # allow user to run only on CLI or extensions + cli_only = modules == ['CLI'] + ext_only = modules == ['EXT'] + if cli_only or ext_only: + modules = None + + selected_modules = get_path_table(include_only=modules) + + if cli_only: + selected_modules['ext'] = {} + if ext_only: + selected_modules['core'] = {} + selected_modules['mod'] = {} + + if not any(selected_modules.values()): + logger.warning('No commands selected to check.') + + selected_mod_names = list(selected_modules['mod'].keys()) + selected_mod_names += list(selected_modules['ext'].keys()) + selected_mod_names += list(selected_modules['core'].keys()) + return selected_mod_names From 8aaf69f166128782350e3f900b1c40fe09ac9b07 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Wed, 28 Aug 2024 13:48:06 +0800 Subject: [PATCH 13/19] Rename the new command & change argument source since we could not distinguish the deprecate_info generated by pre-announcement --- azdev/commands.py | 4 ++-- azdev/operations/breaking_change/__init__.py | 10 +++++----- azdev/params.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/azdev/commands.py b/azdev/commands.py index 9a98eed8a..672c27bba 100644 --- a/azdev/commands.py +++ b/azdev/commands.py @@ -79,5 +79,5 @@ def operation_group(name): with CommandGroup(self, 'extension', operation_group('help')) as g: g.command('generate-docs', 'generate_extension_ref_docs') - with CommandGroup(self, 'breaking-change', operation_group('breaking_change')) as g: - g.command('collect', 'collect_upcoming_breaking_changes') + with CommandGroup(self, '', operation_group('breaking_change')) as g: + g.command('generate-breaking-change-report', 'collect_upcoming_breaking_changes') diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 7fb461177..30457609f 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -125,7 +125,7 @@ def _handle_options_deprecation(module, command, options): def _handle_command_breaking_changes(module, command, command_info, source): - if source in ['all', 'deprecate']: + if source == "deprecate_info": if hasattr(command_info, "deprecate_info") and command_info.deprecate_info: yield from _handle_command_deprecation(module, command, command_info.deprecate_info) @@ -136,7 +136,7 @@ def _handle_command_breaking_changes(module, command, command_info, source): bc_target = _calc_target_of_arg_deprecation(argument_name, arg_settings) yield from _handle_arg_deprecation(module, command, bc_target, depr) yield from _handle_options_deprecation(module, command, arg_settings.get('options', [])) - if source in ['all', 'custom']: + if source == "pre_announce": yield from _handle_custom_breaking_changes(module, command) @@ -148,12 +148,12 @@ def _handle_command_group_deprecation(module, command, deprecate_info): def _handle_command_group_breaking_changes(module, command_group_name, command_group_info, source): - if source in ['all', 'deprecate']: + if source == "deprecate_info": if hasattr(command_group_info, 'group_kwargs') and command_group_info.group_kwargs.get('deprecate_info'): yield from _handle_command_group_deprecation(module, command_group_name, command_group_info.group_kwargs.get('deprecate_info')) - if source in ['all', 'custom']: + if source == "pre_announce": yield from _handle_custom_breaking_changes(module, command_group_name) @@ -209,7 +209,7 @@ def _handle_module(module, loader, main_loader, source): def _handle_core(source): start = time.time() - if source in ['all', 'custom']: + if source == "pre_announce": core_module = 'azure.cli.core' _breaking_change_module = f'{core_module}._breaking_change' try: diff --git a/azdev/params.py b/azdev/params.py index 5e42ee244..17e2d4201 100644 --- a/azdev/params.py +++ b/azdev/params.py @@ -213,14 +213,14 @@ def load_arguments(self, _): c.argument('output_type', choices=['xml', 'html', 'text', 'man', 'latex'], default="xml", help='Output type of the generated docs.') - with ArgumentsContext(self, 'breaking-change collect') as c: + with ArgumentsContext(self, 'generate-breaking-change-report') as c: c.positional('modules', modules_type) c.argument('target_version', default='NextWindow', help='Only the breaking changes scheduled prior to the specified version will be displayed. ' 'The value could be `NextWindow`, `None` or a specified version like `3.0.0`') - c.argument('source', choices=['all', 'deprecate', 'custom'], default='all', - help='The source of pre-announced breaking changes. `deprecate` represents the breaking changes ' - 'marked through `deprecation_info`; `custom` represents the breaking changes announced in ' + c.argument('source', choices=['deprecate_info', 'pre_announce'], default='pre_announce', + help='The source of pre-announced breaking changes. `deprecate_info` represents all breaking changes ' + 'marked through `deprecation_info`; `pre_announce` represents the breaking changes announced in ' '`breaking_change.py` file.') c.argument('group_by_version', action='store_true', help='If specified, breaking changes would be grouped by their target version as well.') From 831321973045a0da8ef8a33f01759fe5e4c1c82b Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 30 Aug 2024 15:18:04 +0800 Subject: [PATCH 14/19] Adjust the pre-announcement extraction from `deprecation_info` --- azdev/operations/breaking_change/__init__.py | 59 ++++++++++++-------- azdev/operations/statistics/__init__.py | 10 ++-- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 30457609f..d2076d350 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ----------------------------------------------------------------------------- - +import re # pylint: disable=no-else-return import time @@ -11,10 +11,12 @@ from importlib import import_module import packaging.version +from knack.deprecation import Deprecated from knack.log import get_logger from azdev.operations.statistics import _create_invoker_and_load_cmds # pylint: disable=protected-access from azdev.utilities import require_azure_cli, get_path_table, display, heading, output, calc_selected_mod_names +from azure.cli.core.breaking_change import MergedStatusTag, UpcomingBreakingChangeTag, TargetVersion logger = get_logger(__name__) @@ -34,7 +36,11 @@ def _load_commands(): az_cli = get_default_cli() # load commands, args, and help - _create_invoker_and_load_cmds(az_cli) + # The arguments must be loaded before the `EVENT_INVOKER_POST_CMD_TBL_CREATE` event. + # This is because we generate the `deprecate_info` and `upcoming_breaking_change` tags from pre-announcement data + # during the event. + # If the arguments are not loaded beforehand, this information will not be included. + _create_invoker_and_load_cmds(az_cli, load_arguments=True) stop = time.time() logger.info('Commands loaded in %i sec', stop - start) @@ -75,11 +81,29 @@ def _handle_custom_breaking_change(module, command, breaking_change): yield from _handle_custom_breaking_change(module, command, bc) +def _handle_status_tag(module, command, status_tag): + if isinstance(status_tag, MergedStatusTag): + for tag in status_tag.tags: + yield from _handle_status_tag(module, command, tag) + else: + detail = status_tag._get_message(status_tag) + version = None + if isinstance(status_tag, Deprecated): + version = status_tag.expiration + elif isinstance(status_tag, UpcomingBreakingChangeTag): + if isinstance(status_tag.target_version, TargetVersion): + version = status_tag.target_version.version() + elif isinstance(status_tag.target_version, str): + version = status_tag.target_version + if version is None: + version_match = re.search(r'\d+\.\d+\.\d+', detail) + if version_match: + version = version_match.group(0) + yield BreakingChangeItem(module, command, detail, version) + + def _handle_command_deprecation(module, command, deprecate_info): - redirect = f' and replaced by `{deprecate_info.redirect}`' if deprecate_info.redirect else '' - version = deprecate_info.expiration if hasattr(deprecate_info, "expiration") else None - expiration = f' This command would be removed in {version}.' if version else '' - yield BreakingChangeItem(module, command, f'This command is deprecated{redirect}.{expiration}', version) + yield from _handle_status_tag(module, command, deprecate_info) def _calc_target_of_arg_deprecation(arg_name, arg_settings): @@ -100,11 +124,8 @@ def _calc_target_of_arg_deprecation(arg_name, arg_settings): def _handle_arg_deprecation(module, command, target, deprecation_info): - redirect = f' and replaced by `{deprecation_info.redirect}`' if deprecation_info.redirect else '' - version = deprecation_info.expiration if hasattr(deprecation_info, "expiration") else None - expiration = f' This parameter would be removed in {version}.' if version else '' - yield BreakingChangeItem(module, command, f'This parameter `{target}` is deprecated{redirect}.{expiration}', - version) + deprecation_info.target = target + yield from _handle_status_tag(module, command, deprecation_info) def _handle_options_deprecation(module, command, options): @@ -117,11 +138,8 @@ def _handle_options_deprecation(module, command, options): for _, depr_list in deprecate_option_map.items(): target = '/'.join([depr.target for depr in depr_list]) depr = depr_list[0] - redirect = f' and replaced by `{depr.redirect}`' if depr.redirect else '' - version = depr.expiration if hasattr(depr, "expiration") else None - expiration = f' This command would be removed in {version}.' if version else '' - yield BreakingChangeItem(module, command, f'This option `{target}` is deprecated{redirect}.{expiration}', - version) + depr.target = target + yield from _handle_status_tag(module, command, depr) def _handle_command_breaking_changes(module, command, command_info, source): @@ -135,16 +153,13 @@ def _handle_command_breaking_changes(module, command, command_info, source): if depr: bc_target = _calc_target_of_arg_deprecation(argument_name, arg_settings) yield from _handle_arg_deprecation(module, command, bc_target, depr) - yield from _handle_options_deprecation(module, command, arg_settings.get('options', [])) + yield from _handle_options_deprecation(module, command, arg_settings.get('options_list', [])) if source == "pre_announce": yield from _handle_custom_breaking_changes(module, command) def _handle_command_group_deprecation(module, command, deprecate_info): - redirect = f' and replaced by `{deprecate_info.redirect}`' if deprecate_info.redirect else '' - version = deprecate_info.expiration if hasattr(deprecate_info, "expiration") else None - expiration = f' This command would be removed in {version}.' if version else '' - yield BreakingChangeItem(module, command, f'This command group is deprecated{redirect}.{expiration}', version) + yield from _handle_status_tag(module, command, deprecate_info) def _handle_command_group_breaking_changes(module, command_group_name, command_group_info, source): @@ -195,7 +210,7 @@ def _handle_module(module, loader, main_loader, source): start = time.time() for command, command_info in loader.command_table.items(): - main_loader.load_arguments(command) + # main_loader.load_arguments(command) yield from _handle_command_breaking_changes(module, command, command_info, source) diff --git a/azdev/operations/statistics/__init__.py b/azdev/operations/statistics/__init__.py index 3467f2793..a50523b11 100644 --- a/azdev/operations/statistics/__init__.py +++ b/azdev/operations/statistics/__init__.py @@ -194,7 +194,7 @@ def _get_command_source(command_name, command): } -def _create_invoker_and_load_cmds(cli_ctx): +def _create_invoker_and_load_cmds(cli_ctx, load_arguments=False): from knack.events import ( EVENT_INVOKER_PRE_CMD_TBL_CREATE, EVENT_INVOKER_POST_CMD_TBL_CREATE) from azure.cli.core.commands import register_cache_arguments @@ -215,9 +215,11 @@ def _create_invoker_and_load_cmds(cli_ctx): invoker.commands_loader.load_command_table(None) invoker.commands_loader.command_name = '' - # cli_ctx.raise_event(EVENT_INVOKER_PRE_LOAD_ARGUMENTS, commands_loader=invoker.commands_loader) - # invoker.commands_loader.load_arguments() - # cli_ctx.raise_event(EVENT_INVOKER_POST_LOAD_ARGUMENTS, commands_loader=invoker.commands_loader) + if load_arguments: + from azure.cli.core.commands.events import EVENT_INVOKER_PRE_LOAD_ARGUMENTS, EVENT_INVOKER_POST_LOAD_ARGUMENTS + cli_ctx.raise_event(EVENT_INVOKER_PRE_LOAD_ARGUMENTS, commands_loader=invoker.commands_loader) + invoker.commands_loader.load_arguments() + cli_ctx.raise_event(EVENT_INVOKER_POST_LOAD_ARGUMENTS, commands_loader=invoker.commands_loader) cli_ctx.raise_event(EVENT_INVOKER_POST_CMD_TBL_CREATE, commands_loader=invoker.commands_loader) invoker.parser.cli_ctx = cli_ctx From 5171a0256eee51c38071b9d14a2eeda7dfc5debd Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 30 Aug 2024 17:30:39 +0800 Subject: [PATCH 15/19] Fix style --- azdev/operations/breaking_change/__init__.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index d2076d350..6776832d5 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -4,8 +4,6 @@ # license information. # ----------------------------------------------------------------------------- import re -# pylint: disable=no-else-return - import time from collections import defaultdict from importlib import import_module @@ -15,9 +13,11 @@ from knack.log import get_logger from azdev.operations.statistics import _create_invoker_and_load_cmds # pylint: disable=protected-access -from azdev.utilities import require_azure_cli, get_path_table, display, heading, output, calc_selected_mod_names +from azdev.utilities import require_azure_cli, display, heading, output, calc_selected_mod_names from azure.cli.core.breaking_change import MergedStatusTag, UpcomingBreakingChangeTag, TargetVersion +# pylint: disable=no-else-return + logger = get_logger(__name__) @@ -86,7 +86,7 @@ def _handle_status_tag(module, command, status_tag): for tag in status_tag.tags: yield from _handle_status_tag(module, command, tag) else: - detail = status_tag._get_message(status_tag) + detail = status_tag._get_message(status_tag) # pylint: disable=protected-access version = None if isinstance(status_tag, Deprecated): version = status_tag.expiration @@ -107,7 +107,6 @@ def _handle_command_deprecation(module, command, deprecate_info): def _calc_target_of_arg_deprecation(arg_name, arg_settings): - from knack.deprecation import Deprecated option_str_list = [] depr = arg_settings.get('deprecate_info') for option in arg_settings.get('option_list', []): @@ -129,7 +128,6 @@ def _handle_arg_deprecation(module, command, target, deprecation_info): def _handle_options_deprecation(module, command, options): - from knack.deprecation import Deprecated deprecate_option_map = defaultdict(lambda: []) for option in options: if isinstance(option, Deprecated): @@ -206,12 +204,10 @@ def _iter_and_prepare_module_loader(command_loader, selected_mod_names): yield module_name, loader -def _handle_module(module, loader, main_loader, source): +def _handle_module(module, loader, source): start = time.time() for command, command_info in loader.command_table.items(): - # main_loader.load_arguments(command) - yield from _handle_command_breaking_changes(module, command, command_info, source) for command_group_name, command_group in loader.command_group_table.items(): @@ -246,7 +242,7 @@ def _handle_upcoming_breaking_changes(selected_mod_names, source): yield from _handle_core(source) for module, loader in _iter_and_prepare_module_loader(command_loader, selected_mod_names): - yield from _handle_module(module, loader, command_loader, source) + yield from _handle_module(module, loader, source) def _filter_breaking_changes(iterator, max_version=None): From 397877f03fe6cdc5bc78cdbff444f31f32041bee Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 3 Sep 2024 10:20:59 +0800 Subject: [PATCH 16/19] Fix style --- azdev/operations/breaking_change/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azdev/operations/breaking_change/__init__.py b/azdev/operations/breaking_change/__init__.py index 6776832d5..d93a6c7a1 100644 --- a/azdev/operations/breaking_change/__init__.py +++ b/azdev/operations/breaking_change/__init__.py @@ -9,12 +9,12 @@ from importlib import import_module import packaging.version +from azure.cli.core.breaking_change import MergedStatusTag, UpcomingBreakingChangeTag, TargetVersion from knack.deprecation import Deprecated from knack.log import get_logger from azdev.operations.statistics import _create_invoker_and_load_cmds # pylint: disable=protected-access from azdev.utilities import require_azure_cli, display, heading, output, calc_selected_mod_names -from azure.cli.core.breaking_change import MergedStatusTag, UpcomingBreakingChangeTag, TargetVersion # pylint: disable=no-else-return From cbf59b1db3fbc17c8cc0e35886f454a08d921644 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 3 Sep 2024 15:25:27 +0800 Subject: [PATCH 17/19] Add Help Message --- azdev/help.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/azdev/help.py b/azdev/help.py index 44f37ff83..4a3eb84ef 100644 --- a/azdev/help.py +++ b/azdev/help.py @@ -377,3 +377,14 @@ - name: Check CLI modules command test coverage in argument level. text: azdev cmdcov CLI --level argument """ + +helps['generate-breaking-change-report'] = """ + short-summary: Collect pre-announced breaking changes items and generate the report. + examples: + - name: Collect all pre-announced breaking changes, including any that did not specify a target version and group them by target version. + text: azdev generate-breaking-change-report CLI --group-by-version --target-version None + - name: Collect all pre-announced breaking changes target before next breaking change window, and display them in markdown. + text: azdev generate-breaking-change-report CLI --output-format markdown + - name: Collect all pre-announced breaking changes in vm, including those failed to specify a target version, and display them in json + text: azdev generate-breaking-change-report vm --target-version None +""" From 4a27214da0266f1c33822dd124e65507677516d9 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 23 Sep 2024 14:43:57 +0800 Subject: [PATCH 18/19] Fix style --- azdev/utilities/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azdev/utilities/__init__.py b/azdev/utilities/__init__.py index 6fd3da6c8..8a38ccce3 100644 --- a/azdev/utilities/__init__.py +++ b/azdev/utilities/__init__.py @@ -47,7 +47,8 @@ get_cli_repo_path, get_ext_repo_paths, get_path_table, - get_name_index, calc_selected_mod_names + get_name_index, + calc_selected_mod_names ) from .testing import test_cmd from .tools import ( From c322435db60d60658e10c2e50ae02c41ab5ab028 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 23 Sep 2024 16:44:20 +0800 Subject: [PATCH 19/19] Update Version in init --- azdev/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azdev/__init__.py b/azdev/__init__.py index 6f1312f1c..270fce503 100644 --- a/azdev/__init__.py +++ b/azdev/__init__.py @@ -4,4 +4,4 @@ # license information. # ----------------------------------------------------------------------------- -__VERSION__ = '0.1.81' +__VERSION__ = '0.1.82'