From cf48f968b4bc6091feb68f8d37fea1df0a41ea69 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Thu, 25 Apr 2024 16:47:08 +0800 Subject: [PATCH 01/42] Upcoming Breaking Change Pre-announce --- doc/how_to_introduce_breaking_changes.md | 198 ++++++++++++ .../azure/cli/core/breaking_change.py | 285 ++++++++++++++++++ .../azure/cli/core/commands/__init__.py | 26 ++ 3 files changed, 509 insertions(+) create mode 100644 doc/how_to_introduce_breaking_changes.md create mode 100644 src/azure-cli-core/azure/cli/core/breaking_change.py diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md new file mode 100644 index 00000000000..5359f7079be --- /dev/null +++ b/doc/how_to_introduce_breaking_changes.md @@ -0,0 +1,198 @@ +# How to introduce Breaking Changes in service command + +Azure CLI has bi-annual breaking change releases coinciding with Microsoft **Build** and **Ignite**. Limiting breaking changes to twice a year provides a stable experience for customers while being able to keep up to date with the latest versions of Azure CLI and plan accordingly for announced breaking changes. + +## Breaking Changes in Azure CLI + +A breaking change refers to a modification that disrupts backward compatibility with previous versions. The breaking changes could cause a customer's script or automation written in a previous version to fail. + +The common examples of breaking changes include: +* Modifying the names of parameters/commands. +* Modifying the input logic of parameters. +* Modifying the format or properties of result output. +* Modifying the current behavior model. +* Adding additional verification that changes CLI behavior. + +To mitigate the impact of breaking changes, Azure CLI delays breaking changes and coordinates half-yearly **Breaking Change Releases** that bundle multiple breaking changes together. This approach helps users plan ahead and adapt to the modifications effectively. + +### Breaking Change Window + +The breaking change window *allows* for service command breaking changes. When a Pull Request is merged during this sprint, it will be included in the next Breaking Change Release. + +The timing of the breaking change window in Azure CLI aligns with [Microsoft Build](https://build.microsoft.com/) and [Microsoft Ignite](https://ignite.microsoft.com/). You could find the next Breaking Change Release plan in our [milestones](https://github.com/Azure/azure-cli/milestones). + +> If you would like to release ad-hoc breaking changes, reach out to the CLI team to provide an explanation for the necessity of these changes. The exceptions can be provide in the following cases: +> * The critical bugs need hotfix +> * The security patch +> * If server side has produced a breaking change which is inevitable for users, then CLI side has to adapt it +> +> The above situation makes it necessary to introduce breaking change as soon as possible. + +### Pre-announce Breaking Changes + +All breaking changes **must** be pre-announced several sprints ahead Release. There are two approaches to inform both interactive users and automatic users about the breaking changes. + +1. (**Mandatory**) Breaking Changes must be pre-announced through Warning Log while executing. +2. (*Automatic*) Breaking Changes would be collected automatically and listed in [Upcoming Breaking Change](https://learn.microsoft.com/en-us/cli/azure/upcoming-breaking-changes). + +## Workflow + +### Overview + +* CLI Owned Module + * Service Team should create an Issue that requests CLI Team to create the pre-announcement several sprints ahead Breaking Change Window. The CLI team will look at the issue and evaluate if it will be accepted in the next breaking change release. + * Please ensure sufficient time for CLI Team to finish the pre-announcement. + * The pre-announcement should be released ahead of Breaking Change Window. +* Service Owned Module + * Service Team should create a Pull Request that create the pre-announcement several sprints ahead Breaking Change Window. + * The pre-announcement should be released ahead of Breaking Change Window. +* After releasing the pre-announcement, a pipeline would be triggered, and the Upcoming Breaking Change Documentation would be updated. +* At the start of Breaking Change window, emails would be sent to notify Service Teams to adopt Breaking Changes. +* Breaking Changes should be adopted within Breaking Change Window. + +### Pre-announce Breaking Changes + +We recommend different approaches for different types of Breaking Changes. + +#### Deprecation + +If you would like to deprecate command groups, commands, arguments or options, please following the [deprecation guide](authoring_command_modules/authoring_commands.md#deprecating-commands-and-arguments) to add a pre-announcement. + +```Python +from azure.cli.core.breaking_change import NEXT_BREAKING_CHANGE_RELEASE + +with self.command_group('test', test_sdk) as g: + g.command('show-parameters', 'get_params', deprecate_info=g.deprecate(redirect='test show', expiration=NEXT_BREAKING_CHANGE_RELEASE)) +``` + +A warning message would be produced when executing the deprecated command. + +```This command has been deprecated and will be removed in version 2.1.0. Use `test show` instead.``` + +If you would like to break the deprecated usage automatically in a future version, set the `expiration` in deprecation information. The `expiration` should be the breaking change release version in our [milestones](https://github.com/Azure/azure-cli/milestones) if set. + +#### Others + +To pre-announce custom breaking changes, such as modifications to default argument values, please add an entry to the `_breaking_change.py` file within the relevant module. If this file does not exist, create `_breaking_change.py` and insert the following lines. + +```python +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from azure.cli.core.breaking_change import upcoming_breaking_changes +``` + +Then you could pre-announce breaking changes for different command groups or command, both list and `BreakingChange` object are accepted. + +```python +from azure.cli.core.breaking_change import upcoming_breaking_changes, BeRequired, DefaultChange, OtherChange + +upcoming_breaking_changes['bar foo'] = BeRequired('--name') +upcoming_breaking_changes['bar foo baz'] = [DefaultChange('--foobar', 'A', 'B'), OtherChange('During May 2024, another Breaking Change would happen in Build Event.')] +``` + +All related breaking changes would be displayed while executing the command. For example, in the above + +```shell +# The azure command +az bar foo baz + +# =====Warning output===== +# The argument `--name` will become required in next breaking change release(2.61.0). +# The default value of `--foobar` will be changed to `B` from `A` in next breaking change release(2.61.0). +# During May 2024, another Breaking Change would happen in Build Event. +``` + +There are several types of breaking changes defined in `breaking_change`. You should use any of them to declare breaking changes in `_breaking_change.py`. + +**Remove** +```python +from azure.cli.core.breaking_change import upcoming_breaking_changes, Remove, NextBreakingChangeWindow + +# Remove the command groups, commands or arguments in a future release. +# **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Removal.** +upcoming_breaking_changes['bar foo'] = Remove('az bar foo', target_version=NextBreakingChangeWindow(), redirect='`az barfoo`') +# `az bar foo` will be removed in next breaking change release(2.61.0). Please use `az barfoo` instead. +``` + +**Rename** +```python +from azure.cli.core.breaking_change import upcoming_breaking_changes, Rename, NextBreakingChangeWindow + +# Rename the command groups, commands or arguments to a new name in a future release. +# **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Renaming.** +# It is recommended that the old name and the new name should be reserved in few releases. +upcoming_breaking_changes['bar foo'] = Rename('az bar foo', 'az bar baz', target_version=NextBreakingChangeWindow()) +# `az bar foo` will be renamed to `az bar baz` in next breaking change release(2.61.0). +``` + +**OutputChange** +```python +from azure.cli.core.breaking_change import upcoming_breaking_changes, OutputChange, NextBreakingChangeWindow + +# The output of the command will be changed in a future release. +upcoming_breaking_changes['bar foo'] = OutputChange('Reduce the output field `baz`', target_version=NextBreakingChangeWindow()) +# The output will be changed in next breaking change release(2.61.0). Reduce the output field `baz`. +``` + +**LogicChange** +```python +from azure.cli.core.breaking_change import upcoming_breaking_changes, LogicChange, NextBreakingChangeWindow + +# There would be a breaking change in the logic of the command in future release. +upcoming_breaking_changes['bar foo'] = LogicChange('Update the validator', target_version=NextBreakingChangeWindow(), detail='The xxx will not be accepted.') +# Update the validator in next breaking change release(2.61.0). The xxx will not be accepted. +``` + +**DefaultChange** +```python +from azure.cli.core.breaking_change import upcoming_breaking_changes, DefaultChange, NextBreakingChangeWindow + +# The default value of an argument would be changed in a future release. +upcoming_breaking_changes['bar foo'] = DefaultChange('--type', 'TypeA', 'TypeB', target_version=NextBreakingChangeWindow()) +# The default value of `--type` will be changed to `TypeB` from `TypeA` in next breaking change release(2.61.0). +``` + +**BeRequired** +```python +from azure.cli.core.breaking_change import upcoming_breaking_changes, BeRequired, NextBreakingChangeWindow + +# The argument would become required in a future release. +upcoming_breaking_changes['bar foo'] = BeRequired('--type', target_version=NextBreakingChangeWindow()) +# The argument `--type` will become required in next breaking change release(2.61.0). +``` + +**OtherChange** +```python +from azure.cli.core.breaking_change import upcoming_breaking_changes, OtherChange, NextBreakingChangeWindow +# Other custom breaking changes. +upcoming_breaking_changes['bar foo'] = OtherChange('During May 2024, another Breaking Change would happen in Build Event.', target_version=NextBreakingChangeWindow()) +# During May 2024, another Breaking Change would happen in Build Event. +``` + +To enhance flexibility in using the Breaking Change Pre-announcement, instead of the default automatic warning display prior to command execution, you may opt to specify the pre-announcement using a designated key in the format `{Command}.{NAME}`. + +```python +# src/azure-cli/azure/cli/command_modules/vm/_breaking_change.py +from azure.cli.core.breaking_change import upcoming_breaking_changes, BeRequired +upcoming_breaking_changes['bar foo.TYPE_REQUIRED'] = BeRequired('--type') + +# src/azure-cli/azure/cli/command_modules/vm/custom.py +# Use the pre-announcement. Replace `vm` with your module +import azure.cli.command_modules.vm._breaking_change # pylint: disable=unused-import +from azure.cli.core.breaking_change import upcoming_breaking_changes + +if not_use_type: + logger.warn(upcoming_breaking_changes['bar foo.TYPE_REQUIRED'].message) +``` + +This way, the pre-announcement wouldn't be display unless running into the branch, but still could be collected into the upcoming breaking change documentation. + +## Upcoming Breaking Change Documentation + +The Upcoming Breaking Change Documentation is released every sprint. This document lists the expected breaking changes for the next Breaking Change Release. However, due to the implementation’s dependency on the Service Team, not all the listed Breaking Changes may be adopted. + +The Upcoming Breaking Change Documentation includes: +* The deprecation targeted at the next Breaking Change Release; +* The pre-announcement declared in `_breaking_change.py`. diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py new file mode 100644 index 00000000000..08de647a35c --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -0,0 +1,285 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import abc +import enum +import re + +import packaging.version + +import requests + +from azure.cli.core import __version__ + +NEXT_BREAKING_CHANGE_RELEASE = '2.61.0' + + +def _next_breaking_change_version_from_milestone(cur_version): + owner = "Azure" + repo = "azure-cli" + # The GitHub API v3 URL for milestones + url = f"https://api.github.com/repos/{owner}/{repo}/milestones" + try: + response = requests.get(url) + response.raise_for_status() + milestones = response.json() + except requests.RequestException as e: + return None + for milestone in milestones: + try: + if 'breaking change' in milestone['title'].lower(): + pattern = re.compile(r'Azure CLI version: *(?P[\d.]+) *$', re.MULTILINE | re.IGNORECASE) + match = re.search(pattern, milestone['description']) + if match: + version = match.group('version') + parsed_version = packaging.version.parse(version) + if parsed_version > cur_version: + return version + except (IndexError, KeyError) as e: + pass + return None + + +__bc_version = None + + +def _next_breaking_change_version(): + global __bc_version + if __bc_version: + return __bc_version + cur_version = packaging.version.parse(__version__) + next_bc_version = packaging.version.parse(NEXT_BREAKING_CHANGE_RELEASE) + if cur_version >= next_bc_version: + fetched_bc_version = _next_breaking_change_version_from_milestone(cur_version) + if fetched_bc_version: + __bc_version = fetched_bc_version + return fetched_bc_version + __bc_version = NEXT_BREAKING_CHANGE_RELEASE + return NEXT_BREAKING_CHANGE_RELEASE + + +class TargetVersion(abc.ABC): + @abc.abstractmethod + def __str__(self): + raise NotImplementedError() + + +class NextBreakingChangeWindow(TargetVersion): + def __str__(self): + return f'in next breaking change release({_next_breaking_change_version()})' + + +class ExactVersion(TargetVersion): + def __init__(self, version): + self.version = version + + def __str__(self): + return f'in {self.version}' + + +class UnspecificVersion(TargetVersion): + def __str__(self): + return 'in future' + + +class BreakingChange(abc.ABC): + @property + @abc.abstractmethod + def message(self): + pass + + @property + @abc.abstractmethod + def target_version(self): + pass + + +class Remove(BreakingChange): + """ + Remove the command groups, commands or arguments in a future release. + + **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Removal.** + :param target: name of the removed command group, command or argument + :param target_version: version where the breaking change is expected to happen. + :type target_version: TargetVersion + :param redirect: alternative way to replace the old behavior + :param doc_link: link of the related document + """ + def __init__(self, target, target_version=NextBreakingChangeWindow(), redirect=None, doc_link=None): + self.target = target + self._target_version = target_version + self.alter = redirect + self.doc_link = doc_link + + @property + def message(self): + alter = f' Please use {self.alter} instead.' if self.alter else '' + doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + return f'`{self.target}` will be removed {str(self._target_version)}.{alter}{doc}' + + @property + def target_version(self): + return self._target_version + + +class Rename(BreakingChange): + """ + Rename the command groups, commands or arguments to a new name in a future release. + + **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Renaming.** + It is recommended that the old name and the new name should be reserved in few releases. + :param target: name of the renamed command group, command or argument + :param new_name: new name + :param target_version: version where the breaking change is expected to happen. + :type target_version: TargetVersion + :param doc_link: link of the related document + """ + def __init__(self, target, new_name, target_version=NextBreakingChangeWindow(), doc_link=None): + self.target = target + self.new_name = new_name + self._target_version = target_version + self.doc_link = doc_link + + @property + def message(self): + doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + return f'`{self.target}` will be renamed to `{self.new_name}` {str(self._target_version)}.{doc}' + + @property + def target_version(self): + return self._target_version + + +class OutputChange(BreakingChange): + """ + The output of the command will be changed in a future release. + :param description: describe the changes in output + :param target_version: version where the breaking change is expected to happen. + :type target_version: TargetVersion + :param guide: how to adapt to the change + :param doc_link: link of the related document + """ + def __init__(self, description: str, target_version=NextBreakingChangeWindow(), guide=None, doc_link=None): + self.desc = description + self._target_version = target_version + self.guide = guide + self.doc_link = doc_link + + @property + def message(self): + desc = self.desc.rstrip() + if desc and desc[-1] not in ',.;?!': + desc = desc + '.' + if self.guide: + guide = self.guide.rstrip() + if guide and guide[-1] not in ',.;?!': + guide = guide + '.' + else: + guide = '' + doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + return f'The output will be changed {str(self._target_version)}. {desc} {guide}{doc}' + + @property + def target_version(self): + return self._target_version + + +class LogicChange(BreakingChange): + """ + There would be a breaking change in the logic of the command in future release. + :param summary: a short summary about the breaking change + :param target_version: version where the breaking change is expected to happen. + :type target_version: TargetVersion + :param detail: detailed information + :param doc_link: link of the related document + """ + def __init__(self, summary, target_version=NextBreakingChangeWindow(), detail=None, doc_link=None): + self.summary = summary + self._target_version = target_version + self.detail = detail + self.doc_link = doc_link + + @property + def message(self): + detail = f' {self.detail}' if self.detail else '' + doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + return f'{self.summary} {str(self._target_version)}.{detail}{doc}' + + @property + def target_version(self): + return self._target_version + + +class DefaultChange(BreakingChange): + """ + The default value of an argument would be changed in a future release. + :param target: name of the related argument + :param current_default: current default value of the argument + :param new_default: new default value of the argument + :param target_version: version where the breaking change is expected to happen. + :type target_version: TargetVersion + :param doc_link: link of the related document + """ + def __init__(self, target, current_default, new_default, target_version=NextBreakingChangeWindow(), doc_link=None): + self.target = target + self.current_default = current_default + self.new_default = new_default + self._target_version = target_version + self.doc_link = doc_link + + @property + def message(self): + doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + return (f'The default value of `{self.target}` will be changed to `{self.new_default}` from ' + f'`{self.current_default}` {str(self._target_version)}.{doc}') + + @property + def target_version(self): + return self._target_version + + +class BeRequired(BreakingChange): + """ + The argument would become required in a future release. + :param target: name of the related argument + :param target_version: version where the breaking change is expected to happen. + :type target_version: TargetVersion + :param doc_link: link of the related document + """ + def __init__(self, target, target_version=NextBreakingChangeWindow(), doc_link=None): + self.target = target + self._target_version = target_version + self.doc_link = doc_link + + @property + def message(self): + doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + return f'The argument `{self.target}` will become required {str(self._target_version)}.{doc}' + + @property + def target_version(self): + return self._target_version + + +class OtherChange(BreakingChange): + """ + Other custom breaking changes. + :param message: A description of the breaking change, including the version number where it is expected to occur. + :param target_version: version where the breaking change is expected to happen. + :type target_version: TargetVersion + """ + def __init__(self, message, target_version=NextBreakingChangeWindow()): + self._message = message + self._target_version = target_version + + @property + def message(self): + return self._message + + @property + def target_version(self): + return self._target_version + + +upcoming_breaking_changes = {} diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 26bb7237db8..a7e24c7b118 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -749,6 +749,7 @@ def _run_jobs_concurrently(self, jobs, ids): def resolve_warnings(self, cmd, parsed_args): self._resolve_preview_and_deprecation_warnings(cmd, parsed_args) self._resolve_extension_override_warning(cmd) + self._resolve_upcoming_breaking_change_warning(cmd) def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): deprecations = [] + getattr(parsed_args, '_argument_deprecations', []) @@ -817,6 +818,31 @@ def _resolve_extension_override_warning(self, cmd): # pylint: disable=no-self-u if isinstance(cmd.command_source, ExtensionCommandSource) and cmd.command_source.overrides_command: logger.warning(cmd.command_source.get_command_warn_msg()) + def _resolve_upcoming_breaking_change_warning(self, cmd): + breaking_changes = [] + try: + import_module(cmd.loader.__class__.__module__ + '._breaking_change') + except ImportError: + pass + + from ..breaking_change import upcoming_breaking_changes, BreakingChange + cmd_parts = cmd.name.split() + if cmd_parts and cmd_parts[0] == 'az': + cmd_parts = cmd_parts[1:] + for parts_end in range(0, len(cmd_parts) + 1): + bc = upcoming_breaking_changes.get(' '.join(cmd_parts[:parts_end])) + if isinstance(bc, list): + breaking_changes.extend(bc) + elif bc: + breaking_changes.append(bc) + + if not self.cli_ctx.only_show_errors: + for bc in breaking_changes: + if isinstance(bc, str): + print(bc, file=sys.stderr) + elif isinstance(bc, BreakingChange): + print(bc.message, file=sys.stderr) + def _resolve_output_sensitive_data_warning(self, cmd, result): if not cmd.cli_ctx.config.getboolean('clients', 'show_secrets_warning', False): return From ff7e5e3e7a1a83c93650022954a9e2cb9371f5cf Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 26 Apr 2024 14:25:42 +0800 Subject: [PATCH 02/42] Fix style --- .../azure/cli/core/breaking_change.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 08de647a35c..9f28dfae9e6 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -3,8 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import abc -import enum import re +from functools import lru_cache import packaging.version @@ -24,7 +24,7 @@ def _next_breaking_change_version_from_milestone(cur_version): response = requests.get(url) response.raise_for_status() milestones = response.json() - except requests.RequestException as e: + except requests.RequestException: return None for milestone in milestones: try: @@ -36,40 +36,36 @@ def _next_breaking_change_version_from_milestone(cur_version): parsed_version = packaging.version.parse(version) if parsed_version > cur_version: return version - except (IndexError, KeyError) as e: + except (IndexError, KeyError): pass return None -__bc_version = None - - +@lru_cache() def _next_breaking_change_version(): - global __bc_version - if __bc_version: - return __bc_version cur_version = packaging.version.parse(__version__) next_bc_version = packaging.version.parse(NEXT_BREAKING_CHANGE_RELEASE) if cur_version >= next_bc_version: fetched_bc_version = _next_breaking_change_version_from_milestone(cur_version) if fetched_bc_version: - __bc_version = fetched_bc_version return fetched_bc_version - __bc_version = NEXT_BREAKING_CHANGE_RELEASE return NEXT_BREAKING_CHANGE_RELEASE +# pylint: disable=too-few-public-methods class TargetVersion(abc.ABC): @abc.abstractmethod def __str__(self): raise NotImplementedError() +# pylint: disable=too-few-public-methods class NextBreakingChangeWindow(TargetVersion): def __str__(self): return f'in next breaking change release({_next_breaking_change_version()})' +# pylint: disable=too-few-public-methods class ExactVersion(TargetVersion): def __init__(self, version): self.version = version @@ -78,6 +74,7 @@ def __str__(self): return f'in {self.version}' +# pylint: disable=too-few-public-methods class UnspecificVersion(TargetVersion): def __str__(self): return 'in future' From 7b6989c9129e438739860f6e5487d128f8959e35 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Sun, 28 Apr 2024 10:11:07 +0800 Subject: [PATCH 03/42] Remove next breaking change version fallback logic --- .../azure/cli/core/breaking_change.py | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 9f28dfae9e6..02cc6d65ffe 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -3,52 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import abc -import re -from functools import lru_cache - -import packaging.version - -import requests - -from azure.cli.core import __version__ NEXT_BREAKING_CHANGE_RELEASE = '2.61.0' -def _next_breaking_change_version_from_milestone(cur_version): - owner = "Azure" - repo = "azure-cli" - # The GitHub API v3 URL for milestones - url = f"https://api.github.com/repos/{owner}/{repo}/milestones" - try: - response = requests.get(url) - response.raise_for_status() - milestones = response.json() - except requests.RequestException: - return None - for milestone in milestones: - try: - if 'breaking change' in milestone['title'].lower(): - pattern = re.compile(r'Azure CLI version: *(?P[\d.]+) *$', re.MULTILINE | re.IGNORECASE) - match = re.search(pattern, milestone['description']) - if match: - version = match.group('version') - parsed_version = packaging.version.parse(version) - if parsed_version > cur_version: - return version - except (IndexError, KeyError): - pass - return None - - -@lru_cache() def _next_breaking_change_version(): - cur_version = packaging.version.parse(__version__) - next_bc_version = packaging.version.parse(NEXT_BREAKING_CHANGE_RELEASE) - if cur_version >= next_bc_version: - fetched_bc_version = _next_breaking_change_version_from_milestone(cur_version) - if fetched_bc_version: - return fetched_bc_version return NEXT_BREAKING_CHANGE_RELEASE From ed3dc38775715177e0b726ccdf49d5a832bf9ef1 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Sun, 28 Apr 2024 14:42:35 +0800 Subject: [PATCH 04/42] Add method to get version number from `TargetVersion` --- .../azure/cli/core/breaking_change.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 02cc6d65ffe..69448c14fbc 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -17,12 +17,19 @@ class TargetVersion(abc.ABC): def __str__(self): raise NotImplementedError() + @abc.abstractmethod + def version(self): + raise NotImplementedError() + # pylint: disable=too-few-public-methods class NextBreakingChangeWindow(TargetVersion): def __str__(self): return f'in next breaking change release({_next_breaking_change_version()})' + def version(self): + return _next_breaking_change_version() + # pylint: disable=too-few-public-methods class ExactVersion(TargetVersion): @@ -32,12 +39,18 @@ def __init__(self, version): def __str__(self): return f'in {self.version}' + def version(self): + return self.version() + # pylint: disable=too-few-public-methods class UnspecificVersion(TargetVersion): def __str__(self): return 'in future' + def version(self): + return None + class BreakingChange(abc.ABC): @property From 1c43766125242059163c15f73f9ce21664eb4041 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 14:45:35 +0800 Subject: [PATCH 05/42] Update doc --- doc/how_to_introduce_breaking_changes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 5359f7079be..396348c1db4 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -17,7 +17,7 @@ To mitigate the impact of breaking changes, Azure CLI delays breaking changes an ### Breaking Change Window -The breaking change window *allows* for service command breaking changes. When a Pull Request is merged during this sprint, it will be included in the next Breaking Change Release. +The breaking change window is a designated sprint that **permits** the merging of service command breaking changes. Any Pull Request merged during this sprint will be included in the subsequent Breaking Change Release. The timing of the breaking change window in Azure CLI aligns with [Microsoft Build](https://build.microsoft.com/) and [Microsoft Ignite](https://ignite.microsoft.com/). You could find the next Breaking Change Release plan in our [milestones](https://github.com/Azure/azure-cli/milestones). @@ -40,7 +40,7 @@ All breaking changes **must** be pre-announced several sprints ahead Release. Th ### Overview * CLI Owned Module - * Service Team should create an Issue that requests CLI Team to create the pre-announcement several sprints ahead Breaking Change Window. The CLI team will look at the issue and evaluate if it will be accepted in the next breaking change release. + * Service Team should create an Issue that requests CLI Team to create the pre-announcement several sprints ahead Breaking Change Window. The issue should include the label `Breaking Change`. The CLI team will look at the issue and evaluate if it will be accepted in the next breaking change release. * Please ensure sufficient time for CLI Team to finish the pre-announcement. * The pre-announcement should be released ahead of Breaking Change Window. * Service Owned Module From 959b2372a4805c2e2b66059b2791a2aced6bf064 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 17:09:38 +0800 Subject: [PATCH 06/42] Rephrase the part about ad-hoc breaking changes. --- doc/how_to_introduce_breaking_changes.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 396348c1db4..d878886cd06 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -21,12 +21,11 @@ The breaking change window is a designated sprint that **permits** the merging o The timing of the breaking change window in Azure CLI aligns with [Microsoft Build](https://build.microsoft.com/) and [Microsoft Ignite](https://ignite.microsoft.com/). You could find the next Breaking Change Release plan in our [milestones](https://github.com/Azure/azure-cli/milestones). -> If you would like to release ad-hoc breaking changes, reach out to the CLI team to provide an explanation for the necessity of these changes. The exceptions can be provide in the following cases: -> * The critical bugs need hotfix -> * The security patch -> * If server side has produced a breaking change which is inevitable for users, then CLI side has to adapt it +> It's highlighted that the introduction of breaking changes is typically prohibited within non-breaking-change window, based on what we stated above for consistency and stable tooling experience. > -> The above situation makes it necessary to introduce breaking change as soon as possible. +> Exceptions to this policy may be considered under extraordinary circumstances. We understand and would like to help out. There would be high-graded justifications required to provide the info Azure CLI can access. +> +> Please note that providing the required info for assessment does not mean it will be assured to be green-lighted for breaking changes. Team will still make the decision based on the overall impact. ### Pre-announce Breaking Changes From c9de6a46523fa4b9e160e18f22aeb74a96284493 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 29 Apr 2024 17:56:55 +0800 Subject: [PATCH 07/42] Add parts about azdev tool. --- doc/how_to_introduce_breaking_changes.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index d878886cd06..a7169e82a50 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -188,6 +188,22 @@ if not_use_type: This way, the pre-announcement wouldn't be display unless running into the branch, but still could be collected into the upcoming breaking change documentation. +### Check Your Breaking Change Pre-Announcement + +Before you publish the breaking changes, you need to make sure that the announcement is ready for the Upcoming Breaking Change Documentation. To do that, run this command: + +```commandline +azdev breaking-change collect +``` + +If your breaking change is not for the next breaking change window, you can see all the announcements by using `--target-version None` like this: + +```commandline +azdev breaking-change collect --target-version None +``` + +The output should be a json object including the pre-announcement you made. + ## Upcoming Breaking Change Documentation The Upcoming Breaking Change Documentation is released every sprint. This document lists the expected breaking changes for the next Breaking Change Release. However, due to the implementation’s dependency on the Service Team, not all the listed Breaking Changes may be adopted. @@ -195,3 +211,9 @@ The Upcoming Breaking Change Documentation is released every sprint. This docume The Upcoming Breaking Change Documentation includes: * The deprecation targeted at the next Breaking Change Release; * The pre-announcement declared in `_breaking_change.py`. + +The documentation is generated through `azdev` tool. You can preview the documentation locally through the following command. + +```commandline +azdev breaking-change collect CLI --output-format markdown +``` From 3acf9c92e5ca4dcb36f85588f583da085cc1f041 Mon Sep 17 00:00:00 2001 From: Qinkai Wu <32201005+ReaNAiveD@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:00:49 +0800 Subject: [PATCH 08/42] Update doc/how_to_introduce_breaking_changes.md Co-authored-by: Xing Zhou --- doc/how_to_introduce_breaking_changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index a7169e82a50..413cb5ba257 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -21,7 +21,7 @@ The breaking change window is a designated sprint that **permits** the merging o The timing of the breaking change window in Azure CLI aligns with [Microsoft Build](https://build.microsoft.com/) and [Microsoft Ignite](https://ignite.microsoft.com/). You could find the next Breaking Change Release plan in our [milestones](https://github.com/Azure/azure-cli/milestones). -> It's highlighted that the introduction of breaking changes is typically prohibited within non-breaking-change window, based on what we stated above for consistency and stable tooling experience. +> It's highlighted that the introduction of breaking changes is typically prohibited within non-breaking-change window, based on what we stated above for consistency and stable tooling experience. > > Exceptions to this policy may be considered under extraordinary circumstances. We understand and would like to help out. There would be high-graded justifications required to provide the info Azure CLI can access. > From 20cf425146a9467854dbd50ded17560f0bf4cd41 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Thu, 13 Jun 2024 11:19:16 +0800 Subject: [PATCH 09/42] Rename breaking change related classes --- doc/how_to_introduce_breaking_changes.md | 56 ++++++++++++------- .../azure/cli/core/breaking_change.py | 14 ++--- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 413cb5ba257..846ed0730c4 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -85,10 +85,12 @@ from azure.cli.core.breaking_change import upcoming_breaking_changes Then you could pre-announce breaking changes for different command groups or command, both list and `BreakingChange` object are accepted. ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, BeRequired, DefaultChange, OtherChange +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIBeRequired, AzCLIDefaultChange, + AzCLIOtherChange -upcoming_breaking_changes['bar foo'] = BeRequired('--name') -upcoming_breaking_changes['bar foo baz'] = [DefaultChange('--foobar', 'A', 'B'), OtherChange('During May 2024, another Breaking Change would happen in Build Event.')] +upcoming_breaking_changes['bar foo'] = AzCLIBeRequired('--name') +upcoming_breaking_changes['bar foo baz'] = [AzCLIDefaultChange('--foobar', 'A', 'B'), AzCLIOtherChange( + 'During May 2024, another Breaking Change would happen in Build Event.')] ``` All related breaking changes would be displayed while executing the command. For example, in the above @@ -106,67 +108,82 @@ az bar foo baz There are several types of breaking changes defined in `breaking_change`. You should use any of them to declare breaking changes in `_breaking_change.py`. **Remove** + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, Remove, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIRemove, NextBreakingChangeWindow # Remove the command groups, commands or arguments in a future release. # **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Removal.** -upcoming_breaking_changes['bar foo'] = Remove('az bar foo', target_version=NextBreakingChangeWindow(), redirect='`az barfoo`') +upcoming_breaking_changes['bar foo'] = AzCLIRemove('az bar foo', target_version=NextBreakingChangeWindow(), + redirect='`az barfoo`') # `az bar foo` will be removed in next breaking change release(2.61.0). Please use `az barfoo` instead. ``` **Rename** + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, Rename, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIRename, NextBreakingChangeWindow # Rename the command groups, commands or arguments to a new name in a future release. # **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Renaming.** # It is recommended that the old name and the new name should be reserved in few releases. -upcoming_breaking_changes['bar foo'] = Rename('az bar foo', 'az bar baz', target_version=NextBreakingChangeWindow()) +upcoming_breaking_changes['bar foo'] = AzCLIRename('az bar foo', 'az bar baz', + target_version=NextBreakingChangeWindow()) # `az bar foo` will be renamed to `az bar baz` in next breaking change release(2.61.0). ``` **OutputChange** + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, OutputChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIOutputChange, NextBreakingChangeWindow # The output of the command will be changed in a future release. -upcoming_breaking_changes['bar foo'] = OutputChange('Reduce the output field `baz`', target_version=NextBreakingChangeWindow()) +upcoming_breaking_changes['bar foo'] = AzCLIOutputChange('Reduce the output field `baz`', + target_version=NextBreakingChangeWindow()) # The output will be changed in next breaking change release(2.61.0). Reduce the output field `baz`. ``` **LogicChange** + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, LogicChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLILogicChange, NextBreakingChangeWindow # There would be a breaking change in the logic of the command in future release. -upcoming_breaking_changes['bar foo'] = LogicChange('Update the validator', target_version=NextBreakingChangeWindow(), detail='The xxx will not be accepted.') +upcoming_breaking_changes['bar foo'] = AzCLILogicChange('Update the validator', + target_version=NextBreakingChangeWindow(), + detail='The xxx will not be accepted.') # Update the validator in next breaking change release(2.61.0). The xxx will not be accepted. ``` **DefaultChange** + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, DefaultChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIDefaultChange, NextBreakingChangeWindow # The default value of an argument would be changed in a future release. -upcoming_breaking_changes['bar foo'] = DefaultChange('--type', 'TypeA', 'TypeB', target_version=NextBreakingChangeWindow()) +upcoming_breaking_changes['bar foo'] = AzCLIDefaultChange('--type', 'TypeA', 'TypeB', + target_version=NextBreakingChangeWindow()) # The default value of `--type` will be changed to `TypeB` from `TypeA` in next breaking change release(2.61.0). ``` **BeRequired** + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, BeRequired, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIBeRequired, NextBreakingChangeWindow # The argument would become required in a future release. -upcoming_breaking_changes['bar foo'] = BeRequired('--type', target_version=NextBreakingChangeWindow()) +upcoming_breaking_changes['bar foo'] = AzCLIBeRequired('--type', target_version=NextBreakingChangeWindow()) # The argument `--type` will become required in next breaking change release(2.61.0). ``` **OtherChange** + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, OtherChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIOtherChange, NextBreakingChangeWindow + # Other custom breaking changes. -upcoming_breaking_changes['bar foo'] = OtherChange('During May 2024, another Breaking Change would happen in Build Event.', target_version=NextBreakingChangeWindow()) +upcoming_breaking_changes['bar foo'] = AzCLIOtherChange( + 'During May 2024, another Breaking Change would happen in Build Event.', target_version=NextBreakingChangeWindow()) # During May 2024, another Breaking Change would happen in Build Event. ``` @@ -174,8 +191,9 @@ To enhance flexibility in using the Breaking Change Pre-announcement, instead of ```python # src/azure-cli/azure/cli/command_modules/vm/_breaking_change.py -from azure.cli.core.breaking_change import upcoming_breaking_changes, BeRequired -upcoming_breaking_changes['bar foo.TYPE_REQUIRED'] = BeRequired('--type') +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIBeRequired + +upcoming_breaking_changes['bar foo.TYPE_REQUIRED'] = AzCLIBeRequired('--type') # src/azure-cli/azure/cli/command_modules/vm/custom.py # Use the pre-announcement. Replace `vm` with your module diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 69448c14fbc..129456a0fc2 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -64,7 +64,7 @@ def target_version(self): pass -class Remove(BreakingChange): +class AzCLIRemove(BreakingChange): """ Remove the command groups, commands or arguments in a future release. @@ -92,7 +92,7 @@ def target_version(self): return self._target_version -class Rename(BreakingChange): +class AzCLIRename(BreakingChange): """ Rename the command groups, commands or arguments to a new name in a future release. @@ -120,7 +120,7 @@ def target_version(self): return self._target_version -class OutputChange(BreakingChange): +class AzCLIOutputChange(BreakingChange): """ The output of the command will be changed in a future release. :param description: describe the changes in output @@ -154,7 +154,7 @@ def target_version(self): return self._target_version -class LogicChange(BreakingChange): +class AzCLILogicChange(BreakingChange): """ There would be a breaking change in the logic of the command in future release. :param summary: a short summary about the breaking change @@ -180,7 +180,7 @@ def target_version(self): return self._target_version -class DefaultChange(BreakingChange): +class AzCLIDefaultChange(BreakingChange): """ The default value of an argument would be changed in a future release. :param target: name of the related argument @@ -208,7 +208,7 @@ def target_version(self): return self._target_version -class BeRequired(BreakingChange): +class AzCLIBeRequired(BreakingChange): """ The argument would become required in a future release. :param target: name of the related argument @@ -231,7 +231,7 @@ def target_version(self): return self._target_version -class OtherChange(BreakingChange): +class AzCLIOtherChange(BreakingChange): """ Other custom breaking changes. :param message: A description of the breaking change, including the version number where it is expected to occur. From 74cf298fa5950ca8b244e26083e19650f9c434b8 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Wed, 19 Jun 2024 16:09:54 +0800 Subject: [PATCH 10/42] Rename Breaking Change related classes --- doc/how_to_introduce_breaking_changes.md | 12 ++++++------ src/azure-cli-core/azure/cli/core/breaking_change.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 846ed0730c4..0817b4072f8 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -110,25 +110,25 @@ There are several types of breaking changes defined in `breaking_change`. You sh **Remove** ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIRemove, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIRemoveChange, NextBreakingChangeWindow # Remove the command groups, commands or arguments in a future release. # **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Removal.** -upcoming_breaking_changes['bar foo'] = AzCLIRemove('az bar foo', target_version=NextBreakingChangeWindow(), - redirect='`az barfoo`') +upcoming_breaking_changes['bar foo'] = AzCLIRemoveChange('az bar foo', target_version=NextBreakingChangeWindow(), + redirect='`az barfoo`') # `az bar foo` will be removed in next breaking change release(2.61.0). Please use `az barfoo` instead. ``` **Rename** ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIRename, NextBreakingChangeWindow +from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIRenameChange, NextBreakingChangeWindow # Rename the command groups, commands or arguments to a new name in a future release. # **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Renaming.** # It is recommended that the old name and the new name should be reserved in few releases. -upcoming_breaking_changes['bar foo'] = AzCLIRename('az bar foo', 'az bar baz', - target_version=NextBreakingChangeWindow()) +upcoming_breaking_changes['bar foo'] = AzCLIRenameChange('az bar foo', 'az bar baz', + target_version=NextBreakingChangeWindow()) # `az bar foo` will be renamed to `az bar baz` in next breaking change release(2.61.0). ``` diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 129456a0fc2..c874a71d321 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -64,7 +64,7 @@ def target_version(self): pass -class AzCLIRemove(BreakingChange): +class AzCLIRemoveChange(BreakingChange): """ Remove the command groups, commands or arguments in a future release. @@ -92,7 +92,7 @@ def target_version(self): return self._target_version -class AzCLIRename(BreakingChange): +class AzCLIRenameChange(BreakingChange): """ Rename the command groups, commands or arguments to a new name in a future release. From 983322fc18c653b2bd09d48b64cb145718bf9574 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 5 Jul 2024 16:22:01 +0800 Subject: [PATCH 11/42] Support Breaking Change warning in `--help` --- src/azure-cli-core/azure/cli/core/__init__.py | 3 ++ src/azure-cli-core/azure/cli/core/_help.py | 11 ++++++++ .../azure/cli/core/breaking_change.py | 28 +++++++++++++++++++ .../azure/cli/core/commands/__init__.py | 21 ++------------ 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 38ba5baeb01..515f5d7a3ea 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -218,6 +218,7 @@ def load_command_table(self, args): _load_module_command_loader, _load_extension_command_loader, BLOCKED_MODS, ExtensionCommandSource) from azure.cli.core.extension import ( get_extensions, get_extension_path, get_extension_modname) + from azure.cli.core.breaking_change import (import_module_breaking_changes, import_extension_breaking_changes) def _update_command_table_from_modules(args, command_modules=None): """Loads command tables from modules and merge into the main command table. @@ -254,6 +255,7 @@ def _update_command_table_from_modules(args, command_modules=None): try: start_time = timeit.default_timer() module_command_table, module_group_table = _load_module_command_loader(self, args, mod) + import_module_breaking_changes(mod) for cmd in module_command_table.values(): cmd.command_source = mod self.command_table.update(module_command_table) @@ -349,6 +351,7 @@ def _filter_modname(extensions): start_time = timeit.default_timer() extension_command_table, extension_group_table = \ _load_extension_command_loader(self, args, ext_mod) + import_extension_breaking_changes(ext_mod) for cmd_name, cmd in extension_command_table.items(): cmd.command_source = ExtensionCommandSource( diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 62d2ae975ca..6e17f012a7d 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -61,6 +61,7 @@ def _print_header(self, cli_name, help_file): def _print_detailed_help(self, cli_name, help_file): CLIPrintMixin._print_extensions_msg(help_file) + CLIPrintMixin._print_break_change_msg(help_file) super(CLIPrintMixin, self)._print_detailed_help(cli_name, help_file) self._print_az_find_message(help_file.command) @@ -127,6 +128,16 @@ def _print_extensions_msg(help_file): # elif help_file.command_source.preview: # logger.warning(help_file.command_source.get_preview_warn_msg()) + @staticmethod + def _print_break_change_msg(help_file): + from azure.cli.core.breaking_change import iter_command_breaking_changes, BreakingChange + + for bc in iter_command_breaking_changes(help_file.command): + if isinstance(bc, str): + logger.warning(bc) + elif isinstance(bc, BreakingChange): + logger.warning(bc.message) + class AzCliHelp(CLIPrintMixin, CLIHelp): diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index c874a71d321..6eb2eb7d5a9 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -252,3 +252,31 @@ def target_version(self): upcoming_breaking_changes = {} + + +def import_module_breaking_changes(mod): + try: + from importlib import import_module + import_module('azure.cli.command_modules.' + mod + '._breaking_change') + except ImportError: + pass + + +def import_extension_breaking_changes(ext_mod): + try: + from importlib import import_module + import_module(ext_mod + '._breaking_change') + except ImportError: + pass + + +def iter_command_breaking_changes(cmd): + cmd_parts = cmd.split() + if cmd_parts and cmd_parts[0] == 'az': + cmd_parts = cmd_parts[1:] + for parts_end in range(0, len(cmd_parts) + 1): + bc = upcoming_breaking_changes.get(' '.join(cmd_parts[:parts_end])) + if isinstance(bc, list): + yield from bc + elif bc: + yield bc diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index a7e24c7b118..3740d92836c 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -819,25 +819,10 @@ def _resolve_extension_override_warning(self, cmd): # pylint: disable=no-self-u logger.warning(cmd.command_source.get_command_warn_msg()) def _resolve_upcoming_breaking_change_warning(self, cmd): - breaking_changes = [] - try: - import_module(cmd.loader.__class__.__module__ + '._breaking_change') - except ImportError: - pass - - from ..breaking_change import upcoming_breaking_changes, BreakingChange - cmd_parts = cmd.name.split() - if cmd_parts and cmd_parts[0] == 'az': - cmd_parts = cmd_parts[1:] - for parts_end in range(0, len(cmd_parts) + 1): - bc = upcoming_breaking_changes.get(' '.join(cmd_parts[:parts_end])) - if isinstance(bc, list): - breaking_changes.extend(bc) - elif bc: - breaking_changes.append(bc) - if not self.cli_ctx.only_show_errors: - for bc in breaking_changes: + from ..breaking_change import iter_command_breaking_changes, BreakingChange + + for bc in iter_command_breaking_changes(cmd.name): if isinstance(bc, str): print(bc, file=sys.stderr) elif isinstance(bc, BreakingChange): From d2668ccb82a1e8f4ad67080cfca98a177eba07ab Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 8 Jul 2024 14:44:22 +0800 Subject: [PATCH 12/42] Update Next Breaking Change Release Version --- src/azure-cli-core/azure/cli/core/breaking_change.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 6eb2eb7d5a9..55c576255ba 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -4,7 +4,7 @@ # -------------------------------------------------------------------------------------------- import abc -NEXT_BREAKING_CHANGE_RELEASE = '2.61.0' +NEXT_BREAKING_CHANGE_RELEASE = '2.67.0' def _next_breaking_change_version(): From 9b405b4cf4ff2f860ff48808d22a91531fba6f7c Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 16 Jul 2024 13:55:57 +0800 Subject: [PATCH 13/42] Use logger.warning to print breaking change message --- src/azure-cli-core/azure/cli/core/commands/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 3740d92836c..0bd47503c8e 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -824,9 +824,9 @@ def _resolve_upcoming_breaking_change_warning(self, cmd): for bc in iter_command_breaking_changes(cmd.name): if isinstance(bc, str): - print(bc, file=sys.stderr) + logger.warning(bc) elif isinstance(bc, BreakingChange): - print(bc.message, file=sys.stderr) + logger.warning(bc.message) def _resolve_output_sensitive_data_warning(self, cmd, result): if not cmd.cli_ctx.config.getboolean('clients', 'show_secrets_warning', False): From 88501bfd5ea9e62a8fd72f047fa7f5ee50b97b89 Mon Sep 17 00:00:00 2001 From: Qinkai Wu <32201005+ReaNAiveD@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:59:53 +0800 Subject: [PATCH 14/42] Update doc/how_to_introduce_breaking_changes.md Co-authored-by: Xing Zhou --- doc/how_to_introduce_breaking_changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 0817b4072f8..89dca1c77fc 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -132,7 +132,7 @@ upcoming_breaking_changes['bar foo'] = AzCLIRenameChange('az bar foo', 'az bar b # `az bar foo` will be renamed to `az bar baz` in next breaking change release(2.61.0). ``` -**OutputChange** +**Output Change** ```python from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIOutputChange, NextBreakingChangeWindow From a94b3e02111e259b141784796f0da810c8897164 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 16 Jul 2024 14:34:25 +0800 Subject: [PATCH 15/42] Abstract doc_link message into a reusable constant. --- .../azure/cli/core/breaking_change.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 55c576255ba..eaabd56a95e 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -63,6 +63,10 @@ def message(self): def target_version(self): pass + @staticmethod + def format_doc_link(doc_link): + return f' To know more about the Breaking Change, please visit {doc_link}.' if doc_link else '' + class AzCLIRemoveChange(BreakingChange): """ @@ -84,7 +88,7 @@ def __init__(self, target, target_version=NextBreakingChangeWindow(), redirect=N @property def message(self): alter = f' Please use {self.alter} instead.' if self.alter else '' - doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + doc = self.format_doc_link(self.doc_link) return f'`{self.target}` will be removed {str(self._target_version)}.{alter}{doc}' @property @@ -112,7 +116,7 @@ def __init__(self, target, new_name, target_version=NextBreakingChangeWindow(), @property def message(self): - doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + doc = self.format_doc_link(self.doc_link) return f'`{self.target}` will be renamed to `{self.new_name}` {str(self._target_version)}.{doc}' @property @@ -146,7 +150,7 @@ def message(self): guide = guide + '.' else: guide = '' - doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + doc = self.format_doc_link(self.doc_link) return f'The output will be changed {str(self._target_version)}. {desc} {guide}{doc}' @property @@ -172,8 +176,7 @@ def __init__(self, summary, target_version=NextBreakingChangeWindow(), detail=No @property def message(self): detail = f' {self.detail}' if self.detail else '' - doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' - return f'{self.summary} {str(self._target_version)}.{detail}{doc}' + return f'{self.summary} {str(self._target_version)}.{detail}{self.format_doc_link(self.doc_link)}' @property def target_version(self): @@ -199,7 +202,7 @@ def __init__(self, target, current_default, new_default, target_version=NextBrea @property def message(self): - doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + doc = self.format_doc_link(self.doc_link) return (f'The default value of `{self.target}` will be changed to `{self.new_default}` from ' f'`{self.current_default}` {str(self._target_version)}.{doc}') @@ -223,7 +226,7 @@ def __init__(self, target, target_version=NextBreakingChangeWindow(), doc_link=N @property def message(self): - doc = f' To know more about the Breaking Change, please visit {self.doc_link}.' if self.doc_link else '' + doc = self.format_doc_link(self.doc_link) return f'The argument `{self.target}` will become required {str(self._target_version)}.{doc}' @property From 2ffdb1f82cdc5828fcef00e2bb941ddf0ea6e815 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 16 Jul 2024 17:07:23 +0800 Subject: [PATCH 16/42] Update title --- doc/how_to_introduce_breaking_changes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 89dca1c77fc..c3970e70eee 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -143,7 +143,7 @@ upcoming_breaking_changes['bar foo'] = AzCLIOutputChange('Reduce the output fiel # The output will be changed in next breaking change release(2.61.0). Reduce the output field `baz`. ``` -**LogicChange** +**Logic Change** ```python from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLILogicChange, NextBreakingChangeWindow @@ -155,7 +155,7 @@ upcoming_breaking_changes['bar foo'] = AzCLILogicChange('Update the validator', # Update the validator in next breaking change release(2.61.0). The xxx will not be accepted. ``` -**DefaultChange** +**Default Change** ```python from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIDefaultChange, NextBreakingChangeWindow @@ -166,7 +166,7 @@ upcoming_breaking_changes['bar foo'] = AzCLIDefaultChange('--type', 'TypeA', 'Ty # The default value of `--type` will be changed to `TypeB` from `TypeA` in next breaking change release(2.61.0). ``` -**BeRequired** +**Be Required** ```python from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIBeRequired, NextBreakingChangeWindow @@ -176,7 +176,7 @@ upcoming_breaking_changes['bar foo'] = AzCLIBeRequired('--type', target_version= # The argument `--type` will become required in next breaking change release(2.61.0). ``` -**OtherChange** +**Other Changes** ```python from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIOtherChange, NextBreakingChangeWindow From e9553c2645e5d214e6e5a3844d790367858f6501 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Wed, 31 Jul 2024 15:13:48 +0800 Subject: [PATCH 17/42] Insert Upcoming Breaking Change in deprecate_info field --- src/azure-cli-core/azure/cli/core/__init__.py | 2 + src/azure-cli-core/azure/cli/core/_help.py | 49 +++- .../azure/cli/core/breaking_change.py | 223 ++++++++++++++++-- .../azure/cli/core/commands/__init__.py | 22 +- 4 files changed, 257 insertions(+), 39 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 6ca67ce4f80..434f06e1d4b 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -58,6 +58,7 @@ class AzCli(CLI): def __init__(self, **kwargs): super(AzCli, self).__init__(**kwargs) + from azure.cli.core.breaking_change import register_upcoming_breaking_change_info from azure.cli.core.commands import register_cache_arguments from azure.cli.core.commands.arm import ( register_ids_argument, register_global_subscription_argument) @@ -90,6 +91,7 @@ def __init__(self, **kwargs): register_global_subscription_argument(self) register_ids_argument(self) # global subscription must be registered first! register_cache_arguments(self) + register_upcoming_breaking_change_info(self) self.progress_controller = None diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 6e17f012a7d..ffa47e95887 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -61,7 +61,6 @@ def _print_header(self, cli_name, help_file): def _print_detailed_help(self, cli_name, help_file): CLIPrintMixin._print_extensions_msg(help_file) - CLIPrintMixin._print_break_change_msg(help_file) super(CLIPrintMixin, self)._print_detailed_help(cli_name, help_file) self._print_az_find_message(help_file.command) @@ -128,16 +127,6 @@ def _print_extensions_msg(help_file): # elif help_file.command_source.preview: # logger.warning(help_file.command_source.get_preview_warn_msg()) - @staticmethod - def _print_break_change_msg(help_file): - from azure.cli.core.breaking_change import iter_command_breaking_changes, BreakingChange - - for bc in iter_command_breaking_changes(help_file.command): - if isinstance(bc, str): - logger.warning(bc) - elif isinstance(bc, BreakingChange): - logger.warning(bc.message) - class AzCliHelp(CLIPrintMixin, CLIHelp): @@ -261,6 +250,44 @@ def __init__(self, help_ctx, delimiters): super(CliHelpFile, self).__init__(help_ctx, delimiters) self.links = [] + from knack.deprecation import resolve_deprecate_info, ImplicitDeprecated, Deprecated + from azure.cli.core.breaking_change import UpcomingBreakingChangeTag, MergedTag + direct_deprecate_info = None + breaking_changes = [] + deprecate_info = resolve_deprecate_info(help_ctx.cli_ctx, delimiters) + if isinstance(deprecate_info, Deprecated): + direct_deprecate_info = deprecate_info + elif isinstance(deprecate_info, UpcomingBreakingChangeTag): + breaking_changes.append(deprecate_info) + + # search for implicit deprecation + path_comps = delimiters.split()[:-1] + implicit_deprecate_info = None + while path_comps: + deprecate_info = resolve_deprecate_info(help_ctx.cli_ctx, ' '.join(path_comps)) + if isinstance(deprecate_info, Deprecated) and implicit_deprecate_info is None: + implicit_deprecate_info = deprecate_info + elif isinstance(deprecate_info, UpcomingBreakingChangeTag): + breaking_changes.append(deprecate_info) + del path_comps[-1] + + if implicit_deprecate_info: + deprecate_kwargs = implicit_deprecate_info.__dict__.copy() + deprecate_kwargs['object_type'] = 'command' if delimiters in \ + help_ctx.cli_ctx.invocation.commands_loader.command_table else 'command group' + del deprecate_kwargs['_get_tag'] + del deprecate_kwargs['_get_message'] + self.deprecate_info = ImplicitDeprecated(cli_ctx=help_ctx.cli_ctx, **deprecate_kwargs) + else: + self.deprecate_info = direct_deprecate_info + + all_deprecate_info = [self.deprecate_info] if self.deprecate_info else [] + all_deprecate_info.extend(breaking_changes) + if len(all_deprecate_info) > 1: + self.deprecate_info = MergedTag(help_ctx.cli_ctx, *all_deprecate_info) + elif all_deprecate_info: + self.deprecate_info = all_deprecate_info[0] + def _should_include_example(self, ex): supported_profiles = ex.get('supported-profiles') unsupported_profiles = ex.get('unsupported-profiles') diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index eaabd56a95e..9b200ddd8e3 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -4,7 +4,73 @@ # -------------------------------------------------------------------------------------------- import abc +from knack.util import StatusTag, color_map + NEXT_BREAKING_CHANGE_RELEASE = '2.67.0' +DEFAULT_BREAKING_CHANGE_TAG = '[BrkChange]' + + +class UpcomingBreakingChangeTag(StatusTag): + def __init__(self, cli_ctx, object_type='', target=None, target_version=None, tag_func=None, message_func=None): + def _default_get_message(bc): + msg = f"A breaking change may occur to this {bc.object_type} " + if isinstance(target_version, TargetVersion): + msg += str(target_version) + '.' + elif isinstance(target_version, str): + msg += 'in ' + target_version + '.' + else: + msg += 'in future release.' + return msg + + if isinstance(message_func, str): + message_func = lambda _: message_func + + self.target_version = target_version + super().__init__( + cli_ctx=cli_ctx, + object_type=object_type, + target=target, + color=color_map['deprecation'], + tag_func=tag_func or (lambda _: DEFAULT_BREAKING_CHANGE_TAG), + message_func=message_func or _default_get_message + ) + + def expired(self): + return False + + +class MergedTag(StatusTag): + + def __init__(self, cli_ctx, *tags): + assert len(tags) > 0 + tag = tags[0] + + def _get_merged_tag(self): + return ''.join(set([tag._get_tag(self) for tag in tags])) + + def _get_merged_msg(self): + return '\n'.join(set([tag._get_message(self) for tag in tags])) + + super().__init__(cli_ctx, tag.object_type, tag.target, tag_func=_get_merged_tag, + message_func=_get_merged_msg, color=tag._color) + self.tags = tags + + def hidden(self): + return any([tag.hidden() for tag in self.tags]) + + def show_in_help(self): + return any([tag.show_in_help() for tag in self.tags]) + + def expired(self): + return any([tag.expired() for tag in self.tags]) + + @property + def tag(self): + return ''.join(set([str(tag.tag) for tag in self.tags])) + + @property + def message(self): + return '\n'.join(set([str(tag.message) for tag in self.tags])) def _next_breaking_change_version(): @@ -53,20 +119,123 @@ def version(self): class BreakingChange(abc.ABC): + def __init__(self, cmd, arg=None, target=None): + self.cmd = cmd + if isinstance(arg, str): + self.args = [arg] + elif isinstance(arg, list): + self.args = arg + else: + self.args = [] + self.target = target if target else '/'.join(self.args) if self.args else self.cmd + @property - @abc.abstractmethod def message(self): - pass + return '' @property - @abc.abstractmethod def target_version(self): - pass + return UnspecificVersion() @staticmethod def format_doc_link(doc_link): return f' To know more about the Breaking Change, please visit {doc_link}.' if doc_link else '' + @property + def command_name(self): + if self.cmd.startswith('az '): + return self.cmd[3:].strip() + else: + return self.cmd + + def is_command_group(self, cli_ctx): + return self.command_name in cli_ctx.invocation.commands_loader.command_group_table + + def to_tag(self, cli_ctx): + if self.args: + object_type = 'argument' + elif self.is_command_group(cli_ctx): + object_type = 'command group' + else: + object_type = 'command' + return UpcomingBreakingChangeTag(cli_ctx, object_type, target=self.target, target_version=self.target_version, + message_func=lambda _: self.message) + + def apply(self, cli_ctx): + def _handle_argument_deprecation(deprecate_info, parent_class): + + class DeprecatedArgumentAction(parent_class): + + def __call__(self, parser, namespace, values, option_string=None): + if not hasattr(namespace, '_argument_deprecations'): + setattr(namespace, '_argument_deprecations', [deprecate_info]) + else: + namespace._argument_deprecations.append(deprecate_info) # pylint: disable=protected-access + try: + super().__call__(parser, namespace, values, option_string) + except NotImplementedError: + setattr(namespace, self.dest, values) + + return DeprecatedArgumentAction + + def find_arg(arg_name, arguments): + if arg_name in arguments: + return arg_name, arguments[arg_name] + for key, argument in arguments.items(): + for option in argument.options_list or []: + if arg_name == option: + return key, argument + trimmed_arg_name = arg_name.strip('-').replace('-', '_') + if trimmed_arg_name in arguments: + return trimmed_arg_name, arguments[trimmed_arg_name] + return None, None + + def iter_direct_sub_cg(cg_name): + for key, command_group in cli_ctx.invocation.commands_loader.command_group_table.items(): + if key.rsplit(maxsplit=1)[0] == cg_name: + from azure.cli.core.commands import AzCommandGroup + if isinstance(command_group, AzCommandGroup): + yield command_group + else: + yield from iter_direct_sub_cg(key) + + def upsert_breaking_change(command_group, tag): + command_group.group_kwargs['deprecate_info'] = tag + + if self.args: + for arg_name in self.args: + # arg_name, arg_type = find_arg(arg) + # # if arg_type and 'deprecate_info' in arg_type.settings: + # # arg_type.settings['deprecate_info'] = self.to_tag(cli_ctx) + # # else: + # # arg_type.settings['upcoming_breaking_change'] = [self.to_tag(cli_ctx)] + # if not arg_type: + # continue + # for loader in cli_ctx.invocation.commands_loader.cmd_to_loader_map[self.command_name]: + # with loader.argument_context(self.command_name) as c: + # c.argument(arg_name, arg_type=arg_type, deprecate_info=self.to_tag(cli_ctx)) + # cli_ctx.invocation.commands_loader._update_command_definitions() + command = cli_ctx.invocation.commands_loader.command_table.get(self.cmd) + if not command: + return + arg_name, arg = find_arg(arg_name, command.arguments) + if not arg: + continue + arg.deprecate_info = self.to_tag(cli_ctx) + arg.action = _handle_argument_deprecation(arg.deprecate_info, arg.options['action']) + elif self.is_command_group(cli_ctx): + command_group = cli_ctx.invocation.commands_loader.command_group_table[self.command_name] + if not command_group: + for command_group in iter_direct_sub_cg(self.command_name): + upsert_breaking_change(command_group, self.to_tag(cli_ctx)) + else: + upsert_breaking_change(command_group, self.to_tag(cli_ctx)) + else: + command = cli_ctx.invocation.commands_loader.command_table.get(self.cmd) + if not command: + return + command.deprecate_info = self.to_tag(cli_ctx) + class AzCLIRemoveChange(BreakingChange): """ @@ -79,8 +248,9 @@ class AzCLIRemoveChange(BreakingChange): :param redirect: alternative way to replace the old behavior :param doc_link: link of the related document """ - def __init__(self, target, target_version=NextBreakingChangeWindow(), redirect=None, doc_link=None): - self.target = target + + def __init__(self, cmd, arg=None, target_version=NextBreakingChangeWindow(), target=None, redirect=None, doc_link=None): + super().__init__(cmd, arg, target) self._target_version = target_version self.alter = redirect self.doc_link = doc_link @@ -108,8 +278,9 @@ class AzCLIRenameChange(BreakingChange): :type target_version: TargetVersion :param doc_link: link of the related document """ - def __init__(self, target, new_name, target_version=NextBreakingChangeWindow(), doc_link=None): - self.target = target + + def __init__(self, cmd, new_name, arg=None, target=None, target_version=NextBreakingChangeWindow(), doc_link=None): + super().__init__(cmd, arg, target) self.new_name = new_name self._target_version = target_version self.doc_link = doc_link @@ -133,7 +304,9 @@ class AzCLIOutputChange(BreakingChange): :param guide: how to adapt to the change :param doc_link: link of the related document """ - def __init__(self, description: str, target_version=NextBreakingChangeWindow(), guide=None, doc_link=None): + + def __init__(self, cmd, description: str, target_version=NextBreakingChangeWindow(), guide=None, doc_link=None): + super().__init__(cmd, None, None) self.desc = description self._target_version = target_version self.guide = guide @@ -167,7 +340,9 @@ class AzCLILogicChange(BreakingChange): :param detail: detailed information :param doc_link: link of the related document """ - def __init__(self, summary, target_version=NextBreakingChangeWindow(), detail=None, doc_link=None): + + def __init__(self, cmd, summary, target_version=NextBreakingChangeWindow(), detail=None, doc_link=None): + super().__init__(cmd, None, None) self.summary = summary self._target_version = target_version self.detail = detail @@ -193,7 +368,10 @@ class AzCLIDefaultChange(BreakingChange): :type target_version: TargetVersion :param doc_link: link of the related document """ - def __init__(self, target, current_default, new_default, target_version=NextBreakingChangeWindow(), doc_link=None): + + def __init__(self, cmd, arg, current_default, new_default, target_version=NextBreakingChangeWindow(), + target=None, doc_link=None): + super().__init__(cmd, arg, target) self.target = target self.current_default = current_default self.new_default = new_default @@ -219,8 +397,9 @@ class AzCLIBeRequired(BreakingChange): :type target_version: TargetVersion :param doc_link: link of the related document """ - def __init__(self, target, target_version=NextBreakingChangeWindow(), doc_link=None): - self.target = target + + def __init__(self, cmd, arg, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): + super().__init__(cmd, arg, target) self._target_version = target_version self.doc_link = doc_link @@ -241,7 +420,9 @@ class AzCLIOtherChange(BreakingChange): :param target_version: version where the breaking change is expected to happen. :type target_version: TargetVersion """ - def __init__(self, message, target_version=NextBreakingChangeWindow()): + + def __init__(self, cmd, message, arg=None, target=None, target_version=NextBreakingChangeWindow()): + super().__init__(cmd, arg, target) self._message = message self._target_version = target_version @@ -273,6 +454,20 @@ def import_extension_breaking_changes(ext_mod): pass +def register_upcoming_breaking_change_info(cli_ctx): + from knack import events + + def update_breaking_change_info(cli_ctx, **kwargs): + for bc in upcoming_breaking_changes.values(): + if isinstance(bc, list): + for bc in bc: + bc.apply(cli_ctx) + else: + bc.apply(cli_ctx) + + cli_ctx.register_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, update_breaking_change_info) + + def iter_command_breaking_changes(cmd): cmd_parts = cmd.split() if cmd_parts and cmd_parts[0] == 'az': diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 0bd47503c8e..6a143bd5f19 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -16,6 +16,7 @@ import copy from importlib import import_module +from azure.cli.core.breaking_change import UpcomingBreakingChangeTag # pylint: disable=unused-import from azure.cli.core.commands.constants import ( BLOCKED_MODS, DEFAULT_QUERY_TIME_RANGE, CLI_COMMON_KWARGS, CLI_COMMAND_KWARGS, CLI_PARAM_KWARGS, @@ -31,7 +32,7 @@ from knack.arguments import CLICommandArgument from knack.commands import CLICommand, CommandGroup, PREVIEW_EXPERIMENTAL_CONFLICT_ERROR -from knack.deprecation import ImplicitDeprecated, resolve_deprecate_info +from knack.deprecation import ImplicitDeprecated, resolve_deprecate_info, Deprecated from knack.invocation import CommandInvoker from knack.preview import ImplicitPreviewItem, PreviewItem, resolve_preview_info from knack.experimental import ImplicitExperimentalItem, ExperimentalItem, resolve_experimental_info @@ -749,7 +750,6 @@ def _run_jobs_concurrently(self, jobs, ids): def resolve_warnings(self, cmd, parsed_args): self._resolve_preview_and_deprecation_warnings(cmd, parsed_args) self._resolve_extension_override_warning(cmd) - self._resolve_upcoming_breaking_change_warning(cmd) def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): deprecations = [] + getattr(parsed_args, '_argument_deprecations', []) @@ -759,8 +759,12 @@ def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): # search for implicit deprecation path_comps = cmd.name.split()[:-1] implicit_deprecate_info = None - while path_comps and not implicit_deprecate_info: - implicit_deprecate_info = resolve_deprecate_info(self.cli_ctx, ' '.join(path_comps)) + while path_comps: + deprecate_info = resolve_deprecate_info(self.cli_ctx, ' '.join(path_comps)) + if isinstance(deprecate_info, Deprecated) and implicit_deprecate_info is None: + implicit_deprecate_info = deprecate_info + elif isinstance(deprecate_info, UpcomingBreakingChangeTag): + deprecations.append(deprecate_info) del path_comps[-1] if implicit_deprecate_info: @@ -818,16 +822,6 @@ def _resolve_extension_override_warning(self, cmd): # pylint: disable=no-self-u if isinstance(cmd.command_source, ExtensionCommandSource) and cmd.command_source.overrides_command: logger.warning(cmd.command_source.get_command_warn_msg()) - def _resolve_upcoming_breaking_change_warning(self, cmd): - if not self.cli_ctx.only_show_errors: - from ..breaking_change import iter_command_breaking_changes, BreakingChange - - for bc in iter_command_breaking_changes(cmd.name): - if isinstance(bc, str): - logger.warning(bc) - elif isinstance(bc, BreakingChange): - logger.warning(bc.message) - def _resolve_output_sensitive_data_warning(self, cmd, result): if not cmd.cli_ctx.config.getboolean('clients', 'show_secrets_warning', False): return From fc55121e5b228cc345e0d2553463a19a57f3ea25 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Thu, 1 Aug 2024 15:38:10 +0800 Subject: [PATCH 18/42] Support option deprecation declare in upcoming breaking change --- .../azure/cli/core/breaking_change.py | 78 ++++++++++++++----- .../azure/cli/core/commands/__init__.py | 7 +- 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 9b200ddd8e3..301d58be654 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import abc +from knack.deprecation import Deprecated from knack.util import StatusTag, color_map NEXT_BREAKING_CHANGE_RELEASE = '2.67.0' @@ -44,16 +45,19 @@ class MergedTag(StatusTag): def __init__(self, cli_ctx, *tags): assert len(tags) > 0 tag = tags[0] + self.tags = list(tags) def _get_merged_tag(self): - return ''.join(set([tag._get_tag(self) for tag in tags])) + return ''.join(set([tag._get_tag(self) for tag in self.tags])) def _get_merged_msg(self): - return '\n'.join(set([tag._get_message(self) for tag in tags])) + return '\n'.join(set([tag._get_message(self) for tag in self.tags])) super().__init__(cli_ctx, tag.object_type, tag.target, tag_func=_get_merged_tag, message_func=_get_merged_msg, color=tag._color) - self.tags = tags + + def merge(self, other): + self.tags.append(other) def hidden(self): return any([tag.hidden() for tag in self.tags]) @@ -162,15 +166,19 @@ def to_tag(self, cli_ctx): message_func=lambda _: self.message) def apply(self, cli_ctx): - def _handle_argument_deprecation(deprecate_info, parent_class): + def _argument_breaking_change_action(status_tag, parent_class): class DeprecatedArgumentAction(parent_class): def __call__(self, parser, namespace, values, option_string=None): if not hasattr(namespace, '_argument_deprecations'): - setattr(namespace, '_argument_deprecations', [deprecate_info]) + setattr(namespace, '_argument_deprecations', []) + if isinstance(status_tag, MergedTag): + for tag in status_tag.tags: + namespace._argument_deprecations.append(tag) else: - namespace._argument_deprecations.append(deprecate_info) # pylint: disable=protected-access + if status_tag not in namespace._argument_deprecations: + namespace._argument_deprecations.append(status_tag) try: super().__call__(parser, namespace, values, option_string) except NotImplementedError: @@ -178,12 +186,22 @@ def __call__(self, parser, namespace, values, option_string=None): return DeprecatedArgumentAction + def update_option(option_name, arguments): + for key, argument in arguments.items(): + if argument.options_list and len(argument.options_list) > 1: + for idx, option in enumerate(argument.options_list): + if isinstance(option, str) and option_name == option and isinstance(self, AzCLIDeprecate): + argument.options_list[idx] = self.to_tag(cli_ctx) + argument.options_list[idx].target = option + return True + return False + def find_arg(arg_name, arguments): if arg_name in arguments: return arg_name, arguments[arg_name] for key, argument in arguments.items(): for option in argument.options_list or []: - if arg_name == option: + if arg_name == option or (isinstance(option, Deprecated) and arg_name == option.target): return key, argument trimmed_arg_name = arg_name.strip('-').replace('-', '_') if trimmed_arg_name in arguments: @@ -204,25 +222,21 @@ def upsert_breaking_change(command_group, tag): if self.args: for arg_name in self.args: - # arg_name, arg_type = find_arg(arg) - # # if arg_type and 'deprecate_info' in arg_type.settings: - # # arg_type.settings['deprecate_info'] = self.to_tag(cli_ctx) - # # else: - # # arg_type.settings['upcoming_breaking_change'] = [self.to_tag(cli_ctx)] - # if not arg_type: - # continue - # for loader in cli_ctx.invocation.commands_loader.cmd_to_loader_map[self.command_name]: - # with loader.argument_context(self.command_name) as c: - # c.argument(arg_name, arg_type=arg_type, deprecate_info=self.to_tag(cli_ctx)) - # cli_ctx.invocation.commands_loader._update_command_definitions() command = cli_ctx.invocation.commands_loader.command_table.get(self.cmd) if not command: return + if update_option(arg_name, command.arguments): + continue arg_name, arg = find_arg(arg_name, command.arguments) if not arg: continue - arg.deprecate_info = self.to_tag(cli_ctx) - arg.action = _handle_argument_deprecation(arg.deprecate_info, arg.options['action']) + if isinstance(arg.deprecate_info, Deprecated) or isinstance(arg.deprecate_info, UpcomingBreakingChangeTag): + arg.deprecate_info = MergedTag(cli_ctx, arg.deprecate_info, self.to_tag(cli_ctx)) + elif isinstance(arg.deprecate_info, MergedTag): + arg.deprecate_info.merge(self.to_tag(cli_ctx)) + else: + arg.deprecate_info = self.to_tag(cli_ctx) + arg.action = _argument_breaking_change_action(arg.deprecate_info, arg.options['action']) elif self.is_command_group(cli_ctx): command_group = cli_ctx.invocation.commands_loader.command_group_table[self.command_name] if not command_group: @@ -237,6 +251,30 @@ def upsert_breaking_change(command_group, tag): command.deprecate_info = self.to_tag(cli_ctx) +class AzCLIDeprecate(BreakingChange): + def __init__(self, cmd, arg=None, **kwargs): + super().__init__(cmd, arg, None) + self.kwargs = kwargs + + def message(self): + msg = "`{}` has been deprecated and will be removed ".format(self.target) + if self.kwargs.get('expiration'): + msg += "in version '{}'.".format(self.kwargs.get('expiration')) + else: + msg += 'in a future release.' + if self.kwargs.get('redirect'): + msg += " Use '{}' instead.".format(self.kwargs.get('redirect')) + + def target_version(self): + if self.kwargs.get('expiration'): + return ExactVersion(self.kwargs.get('expiration')) + else: + return UnspecificVersion() + + def to_tag(self, cli_ctx): + return Deprecated(cli_ctx, **self.kwargs) + + class AzCLIRemoveChange(BreakingChange): """ Remove the command groups, commands or arguments in a future release. diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 6a143bd5f19..741a2975375 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -14,6 +14,7 @@ import sys import time import copy +from collections import OrderedDict from importlib import import_module from azure.cli.core.breaking_change import UpcomingBreakingChangeTag @@ -752,7 +753,11 @@ def resolve_warnings(self, cmd, parsed_args): self._resolve_extension_override_warning(cmd) def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): - deprecations = [] + getattr(parsed_args, '_argument_deprecations', []) + deprecations = getattr(parsed_args, '_argument_deprecations', []) + # Dedup the deprecations + # If an argument has multiple breaking changes or deprecations, + # duplicated deprecations would be produced due to the inherent logic of action + deprecations = list(OrderedDict.fromkeys(deprecations)) if cmd.deprecate_info: deprecations.append(cmd.deprecate_info) From 3b81a0c20202a4eba6e97cf3d041e67e033c648b Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 2 Aug 2024 14:58:17 +0800 Subject: [PATCH 19/42] Add `always_display=True` for BeRequired and DefaultChange breaking changes --- src/azure-cli-core/azure/cli/core/_help.py | 4 +- .../azure/cli/core/breaking_change.py | 42 ++++++++++++++----- .../azure/cli/core/commands/__init__.py | 10 ++++- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index ffa47e95887..df250431b28 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -251,7 +251,7 @@ def __init__(self, help_ctx, delimiters): self.links = [] from knack.deprecation import resolve_deprecate_info, ImplicitDeprecated, Deprecated - from azure.cli.core.breaking_change import UpcomingBreakingChangeTag, MergedTag + from azure.cli.core.breaking_change import UpcomingBreakingChangeTag, MergedStatusTag direct_deprecate_info = None breaking_changes = [] deprecate_info = resolve_deprecate_info(help_ctx.cli_ctx, delimiters) @@ -284,7 +284,7 @@ def __init__(self, help_ctx, delimiters): all_deprecate_info = [self.deprecate_info] if self.deprecate_info else [] all_deprecate_info.extend(breaking_changes) if len(all_deprecate_info) > 1: - self.deprecate_info = MergedTag(help_ctx.cli_ctx, *all_deprecate_info) + self.deprecate_info = MergedStatusTag(help_ctx.cli_ctx, *all_deprecate_info) elif all_deprecate_info: self.deprecate_info = all_deprecate_info[0] diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 301d58be654..c41dbe16b7a 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -12,7 +12,8 @@ class UpcomingBreakingChangeTag(StatusTag): - def __init__(self, cli_ctx, object_type='', target=None, target_version=None, tag_func=None, message_func=None): + def __init__(self, cli_ctx, object_type='', target=None, target_version=None, tag_func=None, message_func=None, + always_display=False): def _default_get_message(bc): msg = f"A breaking change may occur to this {bc.object_type} " if isinstance(target_version, TargetVersion): @@ -26,6 +27,7 @@ def _default_get_message(bc): if isinstance(message_func, str): message_func = lambda _: message_func + self.always_display = always_display self.target_version = target_version super().__init__( cli_ctx=cli_ctx, @@ -40,7 +42,7 @@ def expired(self): return False -class MergedTag(StatusTag): +class MergedStatusTag(StatusTag): def __init__(self, cli_ctx, *tags): assert len(tags) > 0 @@ -155,15 +157,21 @@ def command_name(self): def is_command_group(self, cli_ctx): return self.command_name in cli_ctx.invocation.commands_loader.command_group_table - def to_tag(self, cli_ctx): + def to_tag(self, cli_ctx, **kwargs): if self.args: object_type = 'argument' elif self.is_command_group(cli_ctx): object_type = 'command group' else: object_type = 'command' - return UpcomingBreakingChangeTag(cli_ctx, object_type, target=self.target, target_version=self.target_version, - message_func=lambda _: self.message) + tag_kwargs = { + 'object_type': object_type, + 'target': self.target, + 'target_version': self.target_version, + 'message_func': lambda _: self.message, + } + tag_kwargs.update(kwargs) + return UpcomingBreakingChangeTag(cli_ctx, **tag_kwargs) def apply(self, cli_ctx): def _argument_breaking_change_action(status_tag, parent_class): @@ -173,7 +181,7 @@ class DeprecatedArgumentAction(parent_class): def __call__(self, parser, namespace, values, option_string=None): if not hasattr(namespace, '_argument_deprecations'): setattr(namespace, '_argument_deprecations', []) - if isinstance(status_tag, MergedTag): + if isinstance(status_tag, MergedStatusTag): for tag in status_tag.tags: namespace._argument_deprecations.append(tag) else: @@ -231,8 +239,8 @@ def upsert_breaking_change(command_group, tag): if not arg: continue if isinstance(arg.deprecate_info, Deprecated) or isinstance(arg.deprecate_info, UpcomingBreakingChangeTag): - arg.deprecate_info = MergedTag(cli_ctx, arg.deprecate_info, self.to_tag(cli_ctx)) - elif isinstance(arg.deprecate_info, MergedTag): + arg.deprecate_info = MergedStatusTag(cli_ctx, arg.deprecate_info, self.to_tag(cli_ctx)) + elif isinstance(arg.deprecate_info, MergedStatusTag): arg.deprecate_info.merge(self.to_tag(cli_ctx)) else: arg.deprecate_info = self.to_tag(cli_ctx) @@ -271,8 +279,10 @@ def target_version(self): else: return UnspecificVersion() - def to_tag(self, cli_ctx): - return Deprecated(cli_ctx, **self.kwargs) + def to_tag(self, cli_ctx, **kwargs): + tag_kwargs = dict(self.kwargs) + tag_kwargs.update(kwargs) + return Deprecated(cli_ctx, **tag_kwargs) class AzCLIRemoveChange(BreakingChange): @@ -295,7 +305,7 @@ def __init__(self, cmd, arg=None, target_version=NextBreakingChangeWindow(), tar @property def message(self): - alter = f' Please use {self.alter} instead.' if self.alter else '' + alter = f' Please use `{self.alter}` instead.' if self.alter else '' doc = self.format_doc_link(self.doc_link) return f'`{self.target}` will be removed {str(self._target_version)}.{alter}{doc}' @@ -426,6 +436,11 @@ def message(self): def target_version(self): return self._target_version + def to_tag(self, cli_ctx, **kwargs): + if 'always_display' not in kwargs: + kwargs['always_display'] = True + return super().to_tag(cli_ctx, **kwargs) + class AzCLIBeRequired(BreakingChange): """ @@ -450,6 +465,11 @@ def message(self): def target_version(self): return self._target_version + def to_tag(self, cli_ctx, **kwargs): + if 'always_display' not in kwargs: + kwargs['always_display'] = True + return super().to_tag(cli_ctx, **kwargs) + class AzCLIOtherChange(BreakingChange): """ diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 741a2975375..22443886f18 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -17,7 +17,7 @@ from collections import OrderedDict from importlib import import_module -from azure.cli.core.breaking_change import UpcomingBreakingChangeTag +from azure.cli.core.breaking_change import UpcomingBreakingChangeTag, MergedStatusTag # pylint: disable=unused-import from azure.cli.core.commands.constants import ( BLOCKED_MODS, DEFAULT_QUERY_TIME_RANGE, CLI_COMMON_KWARGS, CLI_COMMAND_KWARGS, CLI_PARAM_KWARGS, @@ -754,6 +754,14 @@ def resolve_warnings(self, cmd, parsed_args): def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): deprecations = getattr(parsed_args, '_argument_deprecations', []) + for arg_name, argument in parsed_args.func.arguments.items(): + if isinstance(argument.deprecate_info, UpcomingBreakingChangeTag): + if argument.deprecate_info.always_display: + deprecations.append(argument.deprecate_info) + elif isinstance(argument.deprecate_info, MergedStatusTag): + for deprecation in argument.deprecate_info.tags: + if isinstance(deprecation, UpcomingBreakingChangeTag) and deprecation.always_display: + deprecations.append(deprecation) # Dedup the deprecations # If an argument has multiple breaking changes or deprecations, # duplicated deprecations would be produced due to the inherent logic of action From 226688714f442fd01f8458c0415b49cb752b19b0 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 5 Aug 2024 10:40:44 +0800 Subject: [PATCH 20/42] Change interface to call method instead of defining dict --- .../azure/cli/core/breaking_change.py | 196 +++++++++++------- 1 file changed, 125 insertions(+), 71 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index c41dbe16b7a..85c2bc96cbb 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -11,6 +11,45 @@ DEFAULT_BREAKING_CHANGE_TAG = '[BrkChange]' +def _argument_breaking_change_action(status_tag, parent_class): + + class ArgumentBreakingChangeAction(parent_class): + + def __call__(self, parser, namespace, values, option_string=None): + if not hasattr(namespace, '_argument_deprecations'): + setattr(namespace, '_argument_deprecations', []) + if isinstance(status_tag, MergedStatusTag): + for tag in status_tag.tags: + namespace._argument_deprecations.append(tag) + else: + if status_tag not in namespace._argument_deprecations: + namespace._argument_deprecations.append(status_tag) + try: + super().__call__(parser, namespace, values, option_string) + except NotImplementedError: + setattr(namespace, self.dest, values) + + return ArgumentBreakingChangeAction + + +def _find_arg(arg_name, arguments): + if arg_name in arguments: + return arg_name, arguments[arg_name] + for key, argument in arguments.items(): + for option in argument.options_list or []: + if arg_name == option or (isinstance(option, Deprecated) and arg_name == option.target): + return key, argument + trimmed_arg_name = arg_name.strip('-').replace('-', '_') + if trimmed_arg_name in arguments: + return trimmed_arg_name, arguments[trimmed_arg_name] + return None, None + + +class NonExpirationDeprecated(Deprecated): + def expired(self): + return False + + class UpcomingBreakingChangeTag(StatusTag): def __init__(self, cli_ctx, object_type='', target=None, target_version=None, tag_func=None, message_func=None, always_display=False): @@ -174,48 +213,16 @@ def to_tag(self, cli_ctx, **kwargs): return UpcomingBreakingChangeTag(cli_ctx, **tag_kwargs) def apply(self, cli_ctx): - def _argument_breaking_change_action(status_tag, parent_class): - - class DeprecatedArgumentAction(parent_class): - - def __call__(self, parser, namespace, values, option_string=None): - if not hasattr(namespace, '_argument_deprecations'): - setattr(namespace, '_argument_deprecations', []) - if isinstance(status_tag, MergedStatusTag): - for tag in status_tag.tags: - namespace._argument_deprecations.append(tag) - else: - if status_tag not in namespace._argument_deprecations: - namespace._argument_deprecations.append(status_tag) - try: - super().__call__(parser, namespace, values, option_string) - except NotImplementedError: - setattr(namespace, self.dest, values) - - return DeprecatedArgumentAction - - def update_option(option_name, arguments): + def apply_option_deprecate(option_name, arguments): for key, argument in arguments.items(): if argument.options_list and len(argument.options_list) > 1: for idx, option in enumerate(argument.options_list): if isinstance(option, str) and option_name == option and isinstance(self, AzCLIDeprecate): - argument.options_list[idx] = self.to_tag(cli_ctx) + argument.options_list[idx] = self.to_tag(cli_ctx, object_type='option') argument.options_list[idx].target = option return True return False - def find_arg(arg_name, arguments): - if arg_name in arguments: - return arg_name, arguments[arg_name] - for key, argument in arguments.items(): - for option in argument.options_list or []: - if arg_name == option or (isinstance(option, Deprecated) and arg_name == option.target): - return key, argument - trimmed_arg_name = arg_name.strip('-').replace('-', '_') - if trimmed_arg_name in arguments: - return trimmed_arg_name, arguments[trimmed_arg_name] - return None, None - def iter_direct_sub_cg(cg_name): for key, command_group in cli_ctx.invocation.commands_loader.command_group_table.items(): if key.rsplit(maxsplit=1)[0] == cg_name: @@ -225,17 +232,14 @@ def iter_direct_sub_cg(cg_name): else: yield from iter_direct_sub_cg(key) - def upsert_breaking_change(command_group, tag): - command_group.group_kwargs['deprecate_info'] = tag - if self.args: + command = cli_ctx.invocation.commands_loader.command_table.get(self.command_name) + if not command: + return for arg_name in self.args: - command = cli_ctx.invocation.commands_loader.command_table.get(self.cmd) - if not command: - return - if update_option(arg_name, command.arguments): + if apply_option_deprecate(arg_name, command.arguments): continue - arg_name, arg = find_arg(arg_name, command.arguments) + arg_name, arg = _find_arg(arg_name, command.arguments) if not arg: continue if isinstance(arg.deprecate_info, Deprecated) or isinstance(arg.deprecate_info, UpcomingBreakingChangeTag): @@ -249,9 +253,9 @@ def upsert_breaking_change(command_group, tag): command_group = cli_ctx.invocation.commands_loader.command_group_table[self.command_name] if not command_group: for command_group in iter_direct_sub_cg(self.command_name): - upsert_breaking_change(command_group, self.to_tag(cli_ctx)) + command_group.group_kwargs['deprecate_info'] = self.to_tag(cli_ctx) else: - upsert_breaking_change(command_group, self.to_tag(cli_ctx)) + command_group.group_kwargs['deprecate_info'] = self.to_tag(cli_ctx) else: command = cli_ctx.invocation.commands_loader.command_table.get(self.cmd) if not command: @@ -264,14 +268,25 @@ def __init__(self, cmd, arg=None, **kwargs): super().__init__(cmd, arg, None) self.kwargs = kwargs - def message(self): - msg = "`{}` has been deprecated and will be removed ".format(self.target) - if self.kwargs.get('expiration'): - msg += "in version '{}'.".format(self.kwargs.get('expiration')) + @staticmethod + def _build_message(object_type, target, expiration, redirect): + if object_type in ['argument', 'option']: + msg = "{} '{}' has been deprecated and will be removed ".format(object_type, target).capitalize() + elif object_type: + msg = "This {} has been deprecated and will be removed ".format(object_type) + else: + msg = "`{}` has been deprecated and will be removed ".format(target) + if expiration: + msg += "in version '{}'.".format(expiration) else: msg += 'in a future release.' - if self.kwargs.get('redirect'): - msg += " Use '{}' instead.".format(self.kwargs.get('redirect')) + if redirect: + msg += " Use '{}' instead.".format(redirect) + return msg + + def message(self): + return self._build_message(self.kwargs.get('object_type'), self.target, self.kwargs.get('expiration'), + self.kwargs.get('redirect')) def target_version(self): if self.kwargs.get('expiration'): @@ -280,9 +295,21 @@ def target_version(self): return UnspecificVersion() def to_tag(self, cli_ctx, **kwargs): - tag_kwargs = dict(self.kwargs) + if self.args: + object_type = 'argument' + elif self.is_command_group(cli_ctx): + object_type = 'command group' + else: + object_type = 'command' + tag_kwargs = { + 'object_type': object_type, + 'message_func': lambda depr: self._build_message( + depr.object_type, depr.target, depr.expiration, depr.redirect), + 'target': self.target + } + tag_kwargs.update(self.kwargs) tag_kwargs.update(kwargs) - return Deprecated(cli_ctx, **tag_kwargs) + return NonExpirationDeprecated(cli_ctx, **tag_kwargs) class AzCLIRemoveChange(BreakingChange): @@ -479,8 +506,8 @@ class AzCLIOtherChange(BreakingChange): :type target_version: TargetVersion """ - def __init__(self, cmd, message, arg=None, target=None, target_version=NextBreakingChangeWindow()): - super().__init__(cmd, arg, target) + def __init__(self, cmd, message, arg=None, target_version=NextBreakingChangeWindow()): + super().__init__(cmd, arg, None) self._message = message self._target_version = target_version @@ -493,7 +520,7 @@ def target_version(self): return self._target_version -upcoming_breaking_changes = {} +upcoming_breaking_changes = [] def import_module_breaking_changes(mod): @@ -516,23 +543,50 @@ def register_upcoming_breaking_change_info(cli_ctx): from knack import events def update_breaking_change_info(cli_ctx, **kwargs): - for bc in upcoming_breaking_changes.values(): - if isinstance(bc, list): - for bc in bc: - bc.apply(cli_ctx) - else: - bc.apply(cli_ctx) + for bc in upcoming_breaking_changes: + bc.apply(cli_ctx) cli_ctx.register_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, update_breaking_change_info) -def iter_command_breaking_changes(cmd): - cmd_parts = cmd.split() - if cmd_parts and cmd_parts[0] == 'az': - cmd_parts = cmd_parts[1:] - for parts_end in range(0, len(cmd_parts) + 1): - bc = upcoming_breaking_changes.get(' '.join(cmd_parts[:parts_end])) - if isinstance(bc, list): - yield from bc - elif bc: - yield bc +def announce_deprecate_info(command_name, arg=None, **kwargs): + upcoming_breaking_changes.append(AzCLIDeprecate(command_name, arg=arg, **kwargs)) + + +def announce_output_breaking_change(command_name, description, target_version=NextBreakingChangeWindow(), guide=None, + doc_link=None): + upcoming_breaking_changes.append(AzCLIOutputChange(command_name, description, target_version, guide, doc_link)) + + +def announce_logic_breaking_change(command_name, summary, target_version=NextBreakingChangeWindow(), detail=None, + doc_link=None): + upcoming_breaking_changes.append(AzCLILogicChange(command_name, summary, target_version, detail, doc_link)) + + +def announce_default_value_breaking_change(command_name, summary, target_version=NextBreakingChangeWindow(), + detail=None, doc_link=None): + upcoming_breaking_changes.append(AzCLIDefaultChange(command_name, summary, target_version, detail, doc_link)) + + +def announce_required_flag_breaking_change(command_name, arg, target_version=NextBreakingChangeWindow(), target=None, + doc_link=None): + upcoming_breaking_changes.append(AzCLIBeRequired(command_name, arg, target_version, target, doc_link)) + + +def announce_other_breaking_change(command_name, message, arg=None, target_version=NextBreakingChangeWindow()): + upcoming_breaking_changes.append(AzCLIOtherChange(command_name, message, arg, target_version)) + + +def announce_command_group_deprecate(command_group, redirect=None, hide=None, + target_version=NEXT_BREAKING_CHANGE_RELEASE, **kwargs): + announce_deprecate_info(command_group, redirect=redirect, hide=hide, expiration=target_version, **kwargs) + + +def announce_command_deprecate(command, redirect=None, hide=None, + target_version=NEXT_BREAKING_CHANGE_RELEASE, **kwargs): + announce_deprecate_info(command, redirect=redirect, hide=hide, expiration=target_version, **kwargs) + + +def announce_argument_deprecate(command, argument, redirect=None, hide=None, + target_version=NEXT_BREAKING_CHANGE_RELEASE, **kwargs): + announce_deprecate_info(command, argument, redirect=redirect, hide=hide, expiration=target_version, **kwargs) From cf3eb6d9b82250efbaa386a23f1d945516bc9e7b Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 5 Aug 2024 11:14:13 +0800 Subject: [PATCH 21/42] Fix None and string action. --- .../azure/cli/core/breaking_change.py | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 85c2bc96cbb..f67471e67ee 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- import abc +import argparse from knack.deprecation import Deprecated from knack.util import StatusTag, color_map @@ -11,9 +12,23 @@ DEFAULT_BREAKING_CHANGE_TAG = '[BrkChange]' -def _argument_breaking_change_action(status_tag, parent_class): +def _get_action_class(cli_ctx, action): + action_class = argparse.Action - class ArgumentBreakingChangeAction(parent_class): + # action is either a user-defined Action class or a string referring a library-defined Action + if isinstance(action, type) and issubclass(action, argparse.Action): + action_class = action + elif isinstance(action, str): + action_class = cli_ctx.invocation.command_loader.cli_ctx.invocation.parser._registries['action'][ + action] # pylint: disable=protected-access + return action_class + + +def _argument_breaking_change_action(cli_ctx, status_tag, action): + + action_class = _get_action_class(cli_ctx, action) + + class ArgumentBreakingChangeAction(action_class): def __call__(self, parser, namespace, values, option_string=None): if not hasattr(namespace, '_argument_deprecations'): @@ -248,7 +263,7 @@ def iter_direct_sub_cg(cg_name): arg.deprecate_info.merge(self.to_tag(cli_ctx)) else: arg.deprecate_info = self.to_tag(cli_ctx) - arg.action = _argument_breaking_change_action(arg.deprecate_info, arg.options['action']) + arg.action = _argument_breaking_change_action(cli_ctx, arg.deprecate_info, arg.options['action']) elif self.is_command_group(cli_ctx): command_group = cli_ctx.invocation.commands_loader.command_group_table[self.command_name] if not command_group: @@ -332,9 +347,9 @@ def __init__(self, cmd, arg=None, target_version=NextBreakingChangeWindow(), tar @property def message(self): - alter = f' Please use `{self.alter}` instead.' if self.alter else '' + alter = f" Please use '{self.alter}' instead." if self.alter else '' doc = self.format_doc_link(self.doc_link) - return f'`{self.target}` will be removed {str(self._target_version)}.{alter}{doc}' + return f"'{self.target}' will be removed {str(self._target_version)}.{alter}{doc}" @property def target_version(self): @@ -363,7 +378,7 @@ def __init__(self, cmd, new_name, arg=None, target=None, target_version=NextBrea @property def message(self): doc = self.format_doc_link(self.doc_link) - return f'`{self.target}` will be renamed to `{self.new_name}` {str(self._target_version)}.{doc}' + return f"'{self.target}' will be renamed to '{self.new_name}' {str(self._target_version)}.{doc}" @property def target_version(self): @@ -456,8 +471,8 @@ def __init__(self, cmd, arg, current_default, new_default, target_version=NextBr @property def message(self): doc = self.format_doc_link(self.doc_link) - return (f'The default value of `{self.target}` will be changed to `{self.new_default}` from ' - f'`{self.current_default}` {str(self._target_version)}.{doc}') + return (f"The default value of '{self.target}' will be changed to '{self.new_default}' from " + f"'{self.current_default}' {str(self._target_version)}.{doc}") @property def target_version(self): From ef36bdfb6ba2636f0431741eaddb47a9d030bccf Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 5 Aug 2024 11:15:12 +0800 Subject: [PATCH 22/42] Change ` to ' --- src/azure-cli-core/azure/cli/core/breaking_change.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index f67471e67ee..c86cd125d96 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -501,7 +501,7 @@ def __init__(self, cmd, arg, target_version=NextBreakingChangeWindow(), target=N @property def message(self): doc = self.format_doc_link(self.doc_link) - return f'The argument `{self.target}` will become required {str(self._target_version)}.{doc}' + return f"The argument '{self.target}' will become required {str(self._target_version)}.{doc}" @property def target_version(self): From 4f8c22341f27380a309f3790e9614059f9668382 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 5 Aug 2024 11:49:58 +0800 Subject: [PATCH 23/42] Support option deprecate execution message --- src/azure-cli-core/azure/cli/core/breaking_change.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index c86cd125d96..82f48177c0e 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -235,6 +235,8 @@ def apply_option_deprecate(option_name, arguments): if isinstance(option, str) and option_name == option and isinstance(self, AzCLIDeprecate): argument.options_list[idx] = self.to_tag(cli_ctx, object_type='option') argument.options_list[idx].target = option + argument.action = _argument_breaking_change_action(cli_ctx, argument.options_list[idx], + argument.options['action']) return True return False From dc9b85097a5e472214f01312f17f3609dd60ed9b Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 5 Aug 2024 13:48:13 +0800 Subject: [PATCH 24/42] Update target_version field --- .../azure/cli/core/breaking_change.py | 109 ++++++------------ 1 file changed, 33 insertions(+), 76 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 82f48177c0e..da97efe3ec5 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -60,11 +60,6 @@ def _find_arg(arg_name, arguments): return None, None -class NonExpirationDeprecated(Deprecated): - def expired(self): - return False - - class UpcomingBreakingChangeTag(StatusTag): def __init__(self, cli_ctx, object_type='', target=None, target_version=None, tag_func=None, message_func=None, always_display=False): @@ -172,14 +167,14 @@ def version(self): # pylint: disable=too-few-public-methods class UnspecificVersion(TargetVersion): def __str__(self): - return 'in future' + return 'in a future release' def version(self): return None class BreakingChange(abc.ABC): - def __init__(self, cmd, arg=None, target=None): + def __init__(self, cmd, arg=None, target=None, target_version=None): self.cmd = cmd if isinstance(arg, str): self.args = [arg] @@ -188,6 +183,12 @@ def __init__(self, cmd, arg=None, target=None): else: self.args = [] self.target = target if target else '/'.join(self.args) if self.args else self.cmd + if isinstance(target_version, TargetVersion): + self._target_version = target_version + elif isinstance(target_version, str): + self._target_version = ExactVersion(target_version) + else: + self._target_version = UnspecificVersion() @property def message(self): @@ -195,7 +196,7 @@ def message(self): @property def target_version(self): - return UnspecificVersion() + return self._target_version @staticmethod def format_doc_link(doc_link): @@ -281,35 +282,26 @@ def iter_direct_sub_cg(cg_name): class AzCLIDeprecate(BreakingChange): - def __init__(self, cmd, arg=None, **kwargs): - super().__init__(cmd, arg, None) + def __init__(self, cmd, arg=None, target_version=NextBreakingChangeWindow(), **kwargs): + super().__init__(cmd, arg, None, target_version) self.kwargs = kwargs @staticmethod - def _build_message(object_type, target, expiration, redirect): + def _build_message(object_type, target, target_version, redirect): if object_type in ['argument', 'option']: msg = "{} '{}' has been deprecated and will be removed ".format(object_type, target).capitalize() elif object_type: msg = "This {} has been deprecated and will be removed ".format(object_type) else: msg = "`{}` has been deprecated and will be removed ".format(target) - if expiration: - msg += "in version '{}'.".format(expiration) - else: - msg += 'in a future release.' + msg += str(target_version) + '.' if redirect: msg += " Use '{}' instead.".format(redirect) return msg def message(self): return self._build_message(self.kwargs.get('object_type'), self.target, self.kwargs.get('expiration'), - self.kwargs.get('redirect')) - - def target_version(self): - if self.kwargs.get('expiration'): - return ExactVersion(self.kwargs.get('expiration')) - else: - return UnspecificVersion() + self.kwargs.get('redirect')) def to_tag(self, cli_ctx, **kwargs): if self.args: @@ -321,12 +313,12 @@ def to_tag(self, cli_ctx, **kwargs): tag_kwargs = { 'object_type': object_type, 'message_func': lambda depr: self._build_message( - depr.object_type, depr.target, depr.expiration, depr.redirect), + depr.object_type, depr.target, self.target_version, depr.redirect), 'target': self.target } tag_kwargs.update(self.kwargs) tag_kwargs.update(kwargs) - return NonExpirationDeprecated(cli_ctx, **tag_kwargs) + return Deprecated(cli_ctx, **tag_kwargs) class AzCLIRemoveChange(BreakingChange): @@ -342,8 +334,7 @@ class AzCLIRemoveChange(BreakingChange): """ def __init__(self, cmd, arg=None, target_version=NextBreakingChangeWindow(), target=None, redirect=None, doc_link=None): - super().__init__(cmd, arg, target) - self._target_version = target_version + super().__init__(cmd, arg, target, target_version) self.alter = redirect self.doc_link = doc_link @@ -353,10 +344,6 @@ def message(self): doc = self.format_doc_link(self.doc_link) return f"'{self.target}' will be removed {str(self._target_version)}.{alter}{doc}" - @property - def target_version(self): - return self._target_version - class AzCLIRenameChange(BreakingChange): """ @@ -372,9 +359,8 @@ class AzCLIRenameChange(BreakingChange): """ def __init__(self, cmd, new_name, arg=None, target=None, target_version=NextBreakingChangeWindow(), doc_link=None): - super().__init__(cmd, arg, target) + super().__init__(cmd, arg, target, target_version) self.new_name = new_name - self._target_version = target_version self.doc_link = doc_link @property @@ -382,10 +368,6 @@ def message(self): doc = self.format_doc_link(self.doc_link) return f"'{self.target}' will be renamed to '{self.new_name}' {str(self._target_version)}.{doc}" - @property - def target_version(self): - return self._target_version - class AzCLIOutputChange(BreakingChange): """ @@ -398,9 +380,8 @@ class AzCLIOutputChange(BreakingChange): """ def __init__(self, cmd, description: str, target_version=NextBreakingChangeWindow(), guide=None, doc_link=None): - super().__init__(cmd, None, None) + super().__init__(cmd, None, None, target_version) self.desc = description - self._target_version = target_version self.guide = guide self.doc_link = doc_link @@ -416,11 +397,7 @@ def message(self): else: guide = '' doc = self.format_doc_link(self.doc_link) - return f'The output will be changed {str(self._target_version)}. {desc} {guide}{doc}' - - @property - def target_version(self): - return self._target_version + return f'The output will be changed {str(self.target_version)}. {desc} {guide}{doc}' class AzCLILogicChange(BreakingChange): @@ -434,20 +411,15 @@ class AzCLILogicChange(BreakingChange): """ def __init__(self, cmd, summary, target_version=NextBreakingChangeWindow(), detail=None, doc_link=None): - super().__init__(cmd, None, None) + super().__init__(cmd, None, None, target_version) self.summary = summary - self._target_version = target_version self.detail = detail self.doc_link = doc_link @property def message(self): detail = f' {self.detail}' if self.detail else '' - return f'{self.summary} {str(self._target_version)}.{detail}{self.format_doc_link(self.doc_link)}' - - @property - def target_version(self): - return self._target_version + return f'{self.summary} {str(self.target_version)}.{detail}{self.format_doc_link(self.doc_link)}' class AzCLIDefaultChange(BreakingChange): @@ -463,11 +435,10 @@ class AzCLIDefaultChange(BreakingChange): def __init__(self, cmd, arg, current_default, new_default, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): - super().__init__(cmd, arg, target) + super().__init__(cmd, arg, target, target_version) self.target = target self.current_default = current_default self.new_default = new_default - self._target_version = target_version self.doc_link = doc_link @property @@ -476,10 +447,6 @@ def message(self): return (f"The default value of '{self.target}' will be changed to '{self.new_default}' from " f"'{self.current_default}' {str(self._target_version)}.{doc}") - @property - def target_version(self): - return self._target_version - def to_tag(self, cli_ctx, **kwargs): if 'always_display' not in kwargs: kwargs['always_display'] = True @@ -496,8 +463,7 @@ class AzCLIBeRequired(BreakingChange): """ def __init__(self, cmd, arg, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): - super().__init__(cmd, arg, target) - self._target_version = target_version + super().__init__(cmd, arg, target, target_version) self.doc_link = doc_link @property @@ -505,10 +471,6 @@ def message(self): doc = self.format_doc_link(self.doc_link) return f"The argument '{self.target}' will become required {str(self._target_version)}.{doc}" - @property - def target_version(self): - return self._target_version - def to_tag(self, cli_ctx, **kwargs): if 'always_display' not in kwargs: kwargs['always_display'] = True @@ -524,18 +486,13 @@ class AzCLIOtherChange(BreakingChange): """ def __init__(self, cmd, message, arg=None, target_version=NextBreakingChangeWindow()): - super().__init__(cmd, arg, None) + super().__init__(cmd, arg, None, target_version) self._message = message - self._target_version = target_version @property def message(self): return self._message - @property - def target_version(self): - return self._target_version - upcoming_breaking_changes = [] @@ -566,8 +523,8 @@ def update_breaking_change_info(cli_ctx, **kwargs): cli_ctx.register_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, update_breaking_change_info) -def announce_deprecate_info(command_name, arg=None, **kwargs): - upcoming_breaking_changes.append(AzCLIDeprecate(command_name, arg=arg, **kwargs)) +def announce_deprecate_info(command_name, arg=None, target_version=NextBreakingChangeWindow(), **kwargs): + upcoming_breaking_changes.append(AzCLIDeprecate(command_name, arg, target_version, **kwargs)) def announce_output_breaking_change(command_name, description, target_version=NextBreakingChangeWindow(), guide=None, @@ -595,15 +552,15 @@ def announce_other_breaking_change(command_name, message, arg=None, target_versi def announce_command_group_deprecate(command_group, redirect=None, hide=None, - target_version=NEXT_BREAKING_CHANGE_RELEASE, **kwargs): - announce_deprecate_info(command_group, redirect=redirect, hide=hide, expiration=target_version, **kwargs) + target_version=NextBreakingChangeWindow(), **kwargs): + announce_deprecate_info(command_group, redirect=redirect, hide=hide, target_version=target_version, **kwargs) def announce_command_deprecate(command, redirect=None, hide=None, - target_version=NEXT_BREAKING_CHANGE_RELEASE, **kwargs): - announce_deprecate_info(command, redirect=redirect, hide=hide, expiration=target_version, **kwargs) + target_version=NextBreakingChangeWindow(), **kwargs): + announce_deprecate_info(command, redirect=redirect, hide=hide, target_version=target_version, **kwargs) def announce_argument_deprecate(command, argument, redirect=None, hide=None, - target_version=NEXT_BREAKING_CHANGE_RELEASE, **kwargs): - announce_deprecate_info(command, argument, redirect=redirect, hide=hide, expiration=target_version, **kwargs) + target_version=NextBreakingChangeWindow(), **kwargs): + announce_deprecate_info(command, argument, redirect=redirect, hide=hide, target_version=target_version, **kwargs) From 140401ed9c088bb34f3f244c9c594b9e50156ba1 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 5 Aug 2024 15:03:40 +0800 Subject: [PATCH 25/42] Fix default value breaking change display --- src/azure-cli-core/azure/cli/core/breaking_change.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index da97efe3ec5..c3d2269e166 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -436,7 +436,6 @@ class AzCLIDefaultChange(BreakingChange): def __init__(self, cmd, arg, current_default, new_default, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): super().__init__(cmd, arg, target, target_version) - self.target = target self.current_default = current_default self.new_default = new_default self.doc_link = doc_link @@ -537,9 +536,10 @@ def announce_logic_breaking_change(command_name, summary, target_version=NextBre upcoming_breaking_changes.append(AzCLILogicChange(command_name, summary, target_version, detail, doc_link)) -def announce_default_value_breaking_change(command_name, summary, target_version=NextBreakingChangeWindow(), - detail=None, doc_link=None): - upcoming_breaking_changes.append(AzCLIDefaultChange(command_name, summary, target_version, detail, doc_link)) +def announce_default_value_breaking_change(command_name, arg, current_default, new_default, + target_version=NextBreakingChangeWindow(), target=None, doc_link=None): + upcoming_breaking_changes.append(AzCLIDefaultChange(command_name, arg, current_default, new_default, + target_version, target, doc_link)) def announce_required_flag_breaking_change(command_name, arg, target_version=NextBreakingChangeWindow(), target=None, From b63aaf10b869cb5a5469a6ba77ae3d080f2e6102 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 5 Aug 2024 17:58:43 +0800 Subject: [PATCH 26/42] Add support for multiple breaking changes in one command/command group. Add support for manual breaking change. --- src/azure-cli-core/azure/cli/core/_help.py | 27 +++++++ .../azure/cli/core/breaking_change.py | 73 +++++++++++++------ .../azure/cli/core/commands/__init__.py | 2 + 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index df250431b28..e2e822db712 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -259,6 +259,10 @@ def __init__(self, help_ctx, delimiters): direct_deprecate_info = deprecate_info elif isinstance(deprecate_info, UpcomingBreakingChangeTag): breaking_changes.append(deprecate_info) + elif isinstance(deprecate_info, MergedStatusTag): + depr, bcs = CliHelpFile.classify_merged_status_tag(deprecate_info) + direct_deprecate_info = depr[0] if depr else None + breaking_changes.extend(bcs) # search for implicit deprecation path_comps = delimiters.split()[:-1] @@ -269,6 +273,11 @@ def __init__(self, help_ctx, delimiters): implicit_deprecate_info = deprecate_info elif isinstance(deprecate_info, UpcomingBreakingChangeTag): breaking_changes.append(deprecate_info) + elif isinstance(deprecate_info, MergedStatusTag): + depr, bcs = CliHelpFile.classify_merged_status_tag(deprecate_info) + if depr and implicit_deprecate_info is None: + implicit_deprecate_info = depr[0] + breaking_changes.extend(bcs) del path_comps[-1] if implicit_deprecate_info: @@ -288,6 +297,24 @@ def __init__(self, help_ctx, delimiters): elif all_deprecate_info: self.deprecate_info = all_deprecate_info[0] + @staticmethod + def classify_merged_status_tag(merged_status_tag): + from knack.deprecation import resolve_deprecate_info, ImplicitDeprecated, Deprecated + from azure.cli.core.breaking_change import UpcomingBreakingChangeTag, MergedStatusTag + + deprecate_info = [] + breaking_changes = [] + for tag in merged_status_tag.tags: + if isinstance(tag, Deprecated): + deprecate_info.append(tag) + elif isinstance(tag, UpcomingBreakingChangeTag): + breaking_changes.append(tag) + elif isinstance(tag, MergedStatusTag): + depr, bcs = CliHelpFile.classify_merged_status_tag(tag) + deprecate_info.extend(depr) + breaking_changes.extend(bcs) + return deprecate_info, breaking_changes + def _should_include_example(self, ex): supported_profiles = ex.get('supported-profiles') unsupported_profiles = ex.get('unsupported-profiles') diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index c3d2269e166..3803c028d17 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -4,10 +4,15 @@ # -------------------------------------------------------------------------------------------- import abc import argparse +from collections import defaultdict +from knack.log import get_logger from knack.deprecation import Deprecated from knack.util import StatusTag, color_map + +logger = get_logger() + NEXT_BREAKING_CHANGE_RELEASE = '2.67.0' DEFAULT_BREAKING_CHANGE_TAG = '[BrkChange]' @@ -241,14 +246,27 @@ def apply_option_deprecate(option_name, arguments): return True return False - def iter_direct_sub_cg(cg_name): + def appended_status_tag(old_status_tag, new_status_tag): + if isinstance(old_status_tag, Deprecated) or isinstance(old_status_tag, UpcomingBreakingChangeTag): + return MergedStatusTag(cli_ctx, old_status_tag, new_status_tag) + elif isinstance(old_status_tag, MergedStatusTag): + old_status_tag.merge(new_status_tag) + return old_status_tag + else: + return new_status_tag + + def apply_to_direct_sub_cg_or_command(cg_name, status_tag): for key, command_group in cli_ctx.invocation.commands_loader.command_group_table.items(): if key.rsplit(maxsplit=1)[0] == cg_name: from azure.cli.core.commands import AzCommandGroup if isinstance(command_group, AzCommandGroup): - yield command_group + command_group.group_kwargs['deprecate_info'] = \ + appended_status_tag(command_group.group_kwargs.get('deprecate_info'), status_tag) else: - yield from iter_direct_sub_cg(key) + apply_to_direct_sub_cg_or_command(key, status_tag) + for key, command in cli_ctx.invocation.commands_loader.command_table.items(): + if key.rsplit(maxsplit=1)[0] == cg_name: + command.deprecate_info = appended_status_tag(command.deprecate_info, self.to_tag(cli_ctx)) if self.args: command = cli_ctx.invocation.commands_loader.command_table.get(self.command_name) @@ -260,25 +278,20 @@ def iter_direct_sub_cg(cg_name): arg_name, arg = _find_arg(arg_name, command.arguments) if not arg: continue - if isinstance(arg.deprecate_info, Deprecated) or isinstance(arg.deprecate_info, UpcomingBreakingChangeTag): - arg.deprecate_info = MergedStatusTag(cli_ctx, arg.deprecate_info, self.to_tag(cli_ctx)) - elif isinstance(arg.deprecate_info, MergedStatusTag): - arg.deprecate_info.merge(self.to_tag(cli_ctx)) - else: - arg.deprecate_info = self.to_tag(cli_ctx) + arg.deprecate_info = appended_status_tag(arg.deprecate_info, self.to_tag(cli_ctx)) arg.action = _argument_breaking_change_action(cli_ctx, arg.deprecate_info, arg.options['action']) elif self.is_command_group(cli_ctx): command_group = cli_ctx.invocation.commands_loader.command_group_table[self.command_name] if not command_group: - for command_group in iter_direct_sub_cg(self.command_name): - command_group.group_kwargs['deprecate_info'] = self.to_tag(cli_ctx) + apply_to_direct_sub_cg_or_command(self.command_name, self.to_tag(cli_ctx)) else: - command_group.group_kwargs['deprecate_info'] = self.to_tag(cli_ctx) + command_group.group_kwargs['deprecate_info'] = \ + appended_status_tag(command_group.group_kwargs.get('deprecate_info'), self.to_tag(cli_ctx)) else: command = cli_ctx.invocation.commands_loader.command_table.get(self.cmd) if not command: return - command.deprecate_info = self.to_tag(cli_ctx) + command.deprecate_info = appended_status_tag(command.deprecate_info, self.to_tag(cli_ctx)) class AzCLIDeprecate(BreakingChange): @@ -493,7 +506,7 @@ def message(self): return self._message -upcoming_breaking_changes = [] +upcoming_breaking_changes = defaultdict(lambda: []) def import_module_breaking_changes(mod): @@ -516,39 +529,40 @@ def register_upcoming_breaking_change_info(cli_ctx): from knack import events def update_breaking_change_info(cli_ctx, **kwargs): - for bc in upcoming_breaking_changes: - bc.apply(cli_ctx) + for breaking_changes in upcoming_breaking_changes.values(): + for breaking_change in breaking_changes: + breaking_change.apply(cli_ctx) cli_ctx.register_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, update_breaking_change_info) def announce_deprecate_info(command_name, arg=None, target_version=NextBreakingChangeWindow(), **kwargs): - upcoming_breaking_changes.append(AzCLIDeprecate(command_name, arg, target_version, **kwargs)) + upcoming_breaking_changes[command_name].append(AzCLIDeprecate(command_name, arg, target_version, **kwargs)) def announce_output_breaking_change(command_name, description, target_version=NextBreakingChangeWindow(), guide=None, doc_link=None): - upcoming_breaking_changes.append(AzCLIOutputChange(command_name, description, target_version, guide, doc_link)) + upcoming_breaking_changes[command_name].append(AzCLIOutputChange(command_name, description, target_version, guide, doc_link)) def announce_logic_breaking_change(command_name, summary, target_version=NextBreakingChangeWindow(), detail=None, doc_link=None): - upcoming_breaking_changes.append(AzCLILogicChange(command_name, summary, target_version, detail, doc_link)) + upcoming_breaking_changes[command_name].append(AzCLILogicChange(command_name, summary, target_version, detail, doc_link)) def announce_default_value_breaking_change(command_name, arg, current_default, new_default, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): - upcoming_breaking_changes.append(AzCLIDefaultChange(command_name, arg, current_default, new_default, + upcoming_breaking_changes[command_name].append(AzCLIDefaultChange(command_name, arg, current_default, new_default, target_version, target, doc_link)) def announce_required_flag_breaking_change(command_name, arg, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): - upcoming_breaking_changes.append(AzCLIBeRequired(command_name, arg, target_version, target, doc_link)) + upcoming_breaking_changes[command_name].append(AzCLIBeRequired(command_name, arg, target_version, target, doc_link)) def announce_other_breaking_change(command_name, message, arg=None, target_version=NextBreakingChangeWindow()): - upcoming_breaking_changes.append(AzCLIOtherChange(command_name, message, arg, target_version)) + upcoming_breaking_changes[command_name].append(AzCLIOtherChange(command_name, message, arg, target_version)) def announce_command_group_deprecate(command_group, redirect=None, hide=None, @@ -564,3 +578,18 @@ def announce_command_deprecate(command, redirect=None, hide=None, def announce_argument_deprecate(command, argument, redirect=None, hide=None, target_version=NextBreakingChangeWindow(), **kwargs): announce_deprecate_info(command, argument, redirect=redirect, hide=hide, target_version=target_version, **kwargs) + + +def announce_manual_breaking_change(tag, breaking_change): + upcoming_breaking_changes[breaking_change.command_name + ':' + tag].append(breaking_change) + + +def print_manual_breaking_change(cli_ctx, tag, custom_logger=None): + command = cli_ctx.invocation.command_name + custom_logger = custom_logger or logger + + command_comps = command.split() + while command_comps: + for breaking_change in upcoming_breaking_changes[' '.join(command_comps) + ':' + tag]: + custom_logger.warning(breaking_change) + del command_comps[-1] diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 22443886f18..eff5d34f1b2 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -778,6 +778,8 @@ def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): implicit_deprecate_info = deprecate_info elif isinstance(deprecate_info, UpcomingBreakingChangeTag): deprecations.append(deprecate_info) + elif isinstance(deprecate_info, MergedStatusTag): + deprecations.extend(deprecate_info.tags) del path_comps[-1] if implicit_deprecate_info: From 61df4f2be4453deefe902efa191b2dfb06283143 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 6 Aug 2024 10:47:11 +0800 Subject: [PATCH 27/42] Refactor function name and custom bc key --- .../azure/cli/core/breaking_change.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 3803c028d17..037a2671663 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -233,8 +233,8 @@ def to_tag(self, cli_ctx, **kwargs): tag_kwargs.update(kwargs) return UpcomingBreakingChangeTag(cli_ctx, **tag_kwargs) - def apply(self, cli_ctx): - def apply_option_deprecate(option_name, arguments): + def register(self, cli_ctx): + def register_option_deprecate(option_name, arguments): for key, argument in arguments.items(): if argument.options_list and len(argument.options_list) > 1: for idx, option in enumerate(argument.options_list): @@ -255,7 +255,7 @@ def appended_status_tag(old_status_tag, new_status_tag): else: return new_status_tag - def apply_to_direct_sub_cg_or_command(cg_name, status_tag): + def register_to_direct_sub_cg_or_command(cg_name, status_tag): for key, command_group in cli_ctx.invocation.commands_loader.command_group_table.items(): if key.rsplit(maxsplit=1)[0] == cg_name: from azure.cli.core.commands import AzCommandGroup @@ -263,7 +263,7 @@ def apply_to_direct_sub_cg_or_command(cg_name, status_tag): command_group.group_kwargs['deprecate_info'] = \ appended_status_tag(command_group.group_kwargs.get('deprecate_info'), status_tag) else: - apply_to_direct_sub_cg_or_command(key, status_tag) + register_to_direct_sub_cg_or_command(key, status_tag) for key, command in cli_ctx.invocation.commands_loader.command_table.items(): if key.rsplit(maxsplit=1)[0] == cg_name: command.deprecate_info = appended_status_tag(command.deprecate_info, self.to_tag(cli_ctx)) @@ -273,7 +273,7 @@ def apply_to_direct_sub_cg_or_command(cg_name, status_tag): if not command: return for arg_name in self.args: - if apply_option_deprecate(arg_name, command.arguments): + if register_option_deprecate(arg_name, command.arguments): continue arg_name, arg = _find_arg(arg_name, command.arguments) if not arg: @@ -283,7 +283,7 @@ def apply_to_direct_sub_cg_or_command(cg_name, status_tag): elif self.is_command_group(cli_ctx): command_group = cli_ctx.invocation.commands_loader.command_group_table[self.command_name] if not command_group: - apply_to_direct_sub_cg_or_command(self.command_name, self.to_tag(cli_ctx)) + register_to_direct_sub_cg_or_command(self.command_name, self.to_tag(cli_ctx)) else: command_group.group_kwargs['deprecate_info'] = \ appended_status_tag(command_group.group_kwargs.get('deprecate_info'), self.to_tag(cli_ctx)) @@ -531,7 +531,7 @@ def register_upcoming_breaking_change_info(cli_ctx): def update_breaking_change_info(cli_ctx, **kwargs): for breaking_changes in upcoming_breaking_changes.values(): for breaking_change in breaking_changes: - breaking_change.apply(cli_ctx) + breaking_change.register(cli_ctx) cli_ctx.register_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, update_breaking_change_info) @@ -581,7 +581,7 @@ def announce_argument_deprecate(command, argument, redirect=None, hide=None, def announce_manual_breaking_change(tag, breaking_change): - upcoming_breaking_changes[breaking_change.command_name + ':' + tag].append(breaking_change) + upcoming_breaking_changes[breaking_change.command_name + '.' + tag].append(breaking_change) def print_manual_breaking_change(cli_ctx, tag, custom_logger=None): @@ -590,6 +590,6 @@ def print_manual_breaking_change(cli_ctx, tag, custom_logger=None): command_comps = command.split() while command_comps: - for breaking_change in upcoming_breaking_changes[' '.join(command_comps) + ':' + tag]: + for breaking_change in upcoming_breaking_changes.get(' '.join(command_comps) + '.' + tag, []): custom_logger.warning(breaking_change) del command_comps[-1] From 5fff1d46f8a1a98aa73f86d268e419f47ee8ca09 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 6 Aug 2024 11:09:53 +0800 Subject: [PATCH 28/42] Fix some style problems --- src/azure-cli-core/azure/cli/core/_help.py | 2 +- .../azure/cli/core/breaking_change.py | 43 +++++++++---------- .../azure/cli/core/commands/__init__.py | 2 +- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index e2e822db712..681b6925b35 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -299,7 +299,7 @@ def __init__(self, help_ctx, delimiters): @staticmethod def classify_merged_status_tag(merged_status_tag): - from knack.deprecation import resolve_deprecate_info, ImplicitDeprecated, Deprecated + from knack.deprecation import Deprecated from azure.cli.core.breaking_change import UpcomingBreakingChangeTag, MergedStatusTag deprecate_info = [] diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 037a2671663..0f0ceec5402 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -24,8 +24,7 @@ def _get_action_class(cli_ctx, action): if isinstance(action, type) and issubclass(action, argparse.Action): action_class = action elif isinstance(action, str): - action_class = cli_ctx.invocation.command_loader.cli_ctx.invocation.parser._registries['action'][ - action] # pylint: disable=protected-access + action_class = cli_ctx.invocation.command_loader.cli_ctx.invocation.parser._registries['action'][action] # pylint: disable=protected-access return action_class @@ -78,9 +77,6 @@ def _default_get_message(bc): msg += 'in future release.' return msg - if isinstance(message_func, str): - message_func = lambda _: message_func - self.always_display = always_display self.target_version = target_version super().__init__( @@ -104,10 +100,10 @@ def __init__(self, cli_ctx, *tags): self.tags = list(tags) def _get_merged_tag(self): - return ''.join(set([tag._get_tag(self) for tag in self.tags])) + return ''.join({tag._get_tag(self) for tag in self.tags}) # pylint: disable=protected-access def _get_merged_msg(self): - return '\n'.join(set([tag._get_message(self) for tag in self.tags])) + return '\n'.join({tag._get_message(self) for tag in self.tags}) # pylint: disable=protected-access super().__init__(cli_ctx, tag.object_type, tag.target, tag_func=_get_merged_tag, message_func=_get_merged_msg, color=tag._color) @@ -116,21 +112,21 @@ def merge(self, other): self.tags.append(other) def hidden(self): - return any([tag.hidden() for tag in self.tags]) + return any(tag.hidden() for tag in self.tags) def show_in_help(self): - return any([tag.show_in_help() for tag in self.tags]) + return any(tag.show_in_help() for tag in self.tags) def expired(self): - return any([tag.expired() for tag in self.tags]) + return any(tag.expired() for tag in self.tags) @property def tag(self): - return ''.join(set([str(tag.tag) for tag in self.tags])) + return ''.join({str(tag.tag) for tag in self.tags}) @property def message(self): - return '\n'.join(set([str(tag.message) for tag in self.tags])) + return '\n'.join({str(tag.message) for tag in self.tags}) def _next_breaking_change_version(): @@ -211,8 +207,7 @@ def format_doc_link(doc_link): def command_name(self): if self.cmd.startswith('az '): return self.cmd[3:].strip() - else: - return self.cmd + return self.cmd def is_command_group(self, cli_ctx): return self.command_name in cli_ctx.invocation.commands_loader.command_group_table @@ -235,7 +230,7 @@ def to_tag(self, cli_ctx, **kwargs): def register(self, cli_ctx): def register_option_deprecate(option_name, arguments): - for key, argument in arguments.items(): + for _, argument in arguments.items(): if argument.options_list and len(argument.options_list) > 1: for idx, option in enumerate(argument.options_list): if isinstance(option, str) and option_name == option and isinstance(self, AzCLIDeprecate): @@ -247,7 +242,7 @@ def register_option_deprecate(option_name, arguments): return False def appended_status_tag(old_status_tag, new_status_tag): - if isinstance(old_status_tag, Deprecated) or isinstance(old_status_tag, UpcomingBreakingChangeTag): + if isinstance(old_status_tag, (Deprecated, UpcomingBreakingChangeTag)): return MergedStatusTag(cli_ctx, old_status_tag, new_status_tag) elif isinstance(old_status_tag, MergedStatusTag): old_status_tag.merge(new_status_tag) @@ -312,6 +307,7 @@ def _build_message(object_type, target, target_version, redirect): msg += " Use '{}' instead.".format(redirect) return msg + @property def message(self): return self._build_message(self.kwargs.get('object_type'), self.target, self.kwargs.get('expiration'), self.kwargs.get('redirect')) @@ -346,7 +342,8 @@ class AzCLIRemoveChange(BreakingChange): :param doc_link: link of the related document """ - def __init__(self, cmd, arg=None, target_version=NextBreakingChangeWindow(), target=None, redirect=None, doc_link=None): + def __init__(self, cmd, arg=None, target_version=NextBreakingChangeWindow(), target=None, redirect=None, + doc_link=None): super().__init__(cmd, arg, target, target_version) self.alter = redirect self.doc_link = doc_link @@ -528,7 +525,7 @@ def import_extension_breaking_changes(ext_mod): def register_upcoming_breaking_change_info(cli_ctx): from knack import events - def update_breaking_change_info(cli_ctx, **kwargs): + def update_breaking_change_info(cli_ctx, **kwargs): # pylint: disable=unused-argument for breaking_changes in upcoming_breaking_changes.values(): for breaking_change in breaking_changes: breaking_change.register(cli_ctx) @@ -542,18 +539,20 @@ def announce_deprecate_info(command_name, arg=None, target_version=NextBreakingC def announce_output_breaking_change(command_name, description, target_version=NextBreakingChangeWindow(), guide=None, doc_link=None): - upcoming_breaking_changes[command_name].append(AzCLIOutputChange(command_name, description, target_version, guide, doc_link)) + upcoming_breaking_changes[command_name].append( + AzCLIOutputChange(command_name, description, target_version, guide, doc_link)) def announce_logic_breaking_change(command_name, summary, target_version=NextBreakingChangeWindow(), detail=None, doc_link=None): - upcoming_breaking_changes[command_name].append(AzCLILogicChange(command_name, summary, target_version, detail, doc_link)) + upcoming_breaking_changes[command_name].append( + AzCLILogicChange(command_name, summary, target_version, detail, doc_link)) def announce_default_value_breaking_change(command_name, arg, current_default, new_default, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): - upcoming_breaking_changes[command_name].append(AzCLIDefaultChange(command_name, arg, current_default, new_default, - target_version, target, doc_link)) + upcoming_breaking_changes[command_name].append( + AzCLIDefaultChange(command_name, arg, current_default, new_default, target_version, target, doc_link)) def announce_required_flag_breaking_change(command_name, arg, target_version=NextBreakingChangeWindow(), target=None, diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index eff5d34f1b2..3534ca28fb1 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -754,7 +754,7 @@ def resolve_warnings(self, cmd, parsed_args): def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): deprecations = getattr(parsed_args, '_argument_deprecations', []) - for arg_name, argument in parsed_args.func.arguments.items(): + for _, argument in parsed_args.func.arguments.items(): if isinstance(argument.deprecate_info, UpcomingBreakingChangeTag): if argument.deprecate_info.always_display: deprecations.append(argument.deprecate_info) From 303294fc241ff233b3c68df247dfc48dc06adfd5 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 6 Aug 2024 15:31:06 +0800 Subject: [PATCH 29/42] Fix style --- src/azure-cli-core/azure/cli/core/breaking_change.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 0f0ceec5402..01020700822 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -247,8 +247,7 @@ def appended_status_tag(old_status_tag, new_status_tag): elif isinstance(old_status_tag, MergedStatusTag): old_status_tag.merge(new_status_tag) return old_status_tag - else: - return new_status_tag + return new_status_tag def register_to_direct_sub_cg_or_command(cg_name, status_tag): for key, command_group in cli_ctx.invocation.commands_loader.command_group_table.items(): From 31b53c8ddde61da1fe1d1e762071263c42226580 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 6 Aug 2024 17:16:13 +0800 Subject: [PATCH 30/42] Fix style --- src/azure-cli-core/azure/cli/core/breaking_change.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 01020700822..f9536549d0f 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -244,7 +244,7 @@ def register_option_deprecate(option_name, arguments): def appended_status_tag(old_status_tag, new_status_tag): if isinstance(old_status_tag, (Deprecated, UpcomingBreakingChangeTag)): return MergedStatusTag(cli_ctx, old_status_tag, new_status_tag) - elif isinstance(old_status_tag, MergedStatusTag): + if isinstance(old_status_tag, MergedStatusTag): old_status_tag.merge(new_status_tag) return old_status_tag return new_status_tag From 316439fd62efc5b1b5225f6180bfd10e836c421b Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Wed, 7 Aug 2024 19:10:31 +0800 Subject: [PATCH 31/42] Update upcoming breaking change document --- doc/how_to_introduce_breaking_changes.md | 210 +++++++++++------- .../azure/cli/core/breaking_change.py | 8 +- 2 files changed, 138 insertions(+), 80 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index c3970e70eee..837066f899f 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -29,7 +29,7 @@ The timing of the breaking change window in Azure CLI aligns with [Microsoft Bui ### Pre-announce Breaking Changes -All breaking changes **must** be pre-announced several sprints ahead Release. There are two approaches to inform both interactive users and automatic users about the breaking changes. +All breaking changes **must** be pre-announced two sprints ahead Release. There are two approaches to inform both interactive users and automatic users about the breaking changes. 1. (**Mandatory**) Breaking Changes must be pre-announced through Warning Log while executing. 2. (*Automatic*) Breaking Changes would be collected automatically and listed in [Upcoming Breaking Change](https://learn.microsoft.com/en-us/cli/azure/upcoming-breaking-changes). @@ -46,162 +46,210 @@ All breaking changes **must** be pre-announced several sprints ahead Release. Th * Service Team should create a Pull Request that create the pre-announcement several sprints ahead Breaking Change Window. * The pre-announcement should be released ahead of Breaking Change Window. * After releasing the pre-announcement, a pipeline would be triggered, and the Upcoming Breaking Change Documentation would be updated. -* At the start of Breaking Change window, emails would be sent to notify Service Teams to adopt Breaking Changes. +* At the start of Breaking Change window, the CLI team would notify Service Teams to adopt Breaking Changes. * Breaking Changes should be adopted within Breaking Change Window. ### Pre-announce Breaking Changes -We recommend different approaches for different types of Breaking Changes. +The breaking change pre-announcement must be released at least two sprints before the breaking change itself. It is strongly recommended to follow the best practice of providing the new behavior along with the pre-announcement. This allows customers to take action as soon as they discover the pre-announcement. -#### Deprecation +We provide several interfaces to pre-announce different types of breaking changes. -If you would like to deprecate command groups, commands, arguments or options, please following the [deprecation guide](authoring_command_modules/authoring_commands.md#deprecating-commands-and-arguments) to add a pre-announcement. - -```Python -from azure.cli.core.breaking_change import NEXT_BREAKING_CHANGE_RELEASE - -with self.command_group('test', test_sdk) as g: - g.command('show-parameters', 'get_params', deprecate_info=g.deprecate(redirect='test show', expiration=NEXT_BREAKING_CHANGE_RELEASE)) -``` - -A warning message would be produced when executing the deprecated command. - -```This command has been deprecated and will be removed in version 2.1.0. Use `test show` instead.``` - -If you would like to break the deprecated usage automatically in a future version, set the `expiration` in deprecation information. The `expiration` should be the breaking change release version in our [milestones](https://github.com/Azure/azure-cli/milestones) if set. - -#### Others - -To pre-announce custom breaking changes, such as modifications to default argument values, please add an entry to the `_breaking_change.py` file within the relevant module. If this file does not exist, create `_breaking_change.py` and insert the following lines. +To pre-announce breaking changes, such as modifications to default argument values, please add an entry to the `_breaking_change.py` file within the relevant module. If this file does not exist, create `_breaking_change.py` and insert the following lines. ```python # -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azure.cli.core.breaking_change import upcoming_breaking_changes ``` -Then you could pre-announce breaking changes for different command groups or command, both list and `BreakingChange` object are accepted. +You can then pre-announce breaking changes for different command groups or commands. Multiple breaking changes on the same command are accepted. ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIBeRequired, AzCLIDefaultChange, - AzCLIOtherChange +from azure.cli.core.breaking_change import announce_required_flag_breaking_change, \ + announce_default_value_breaking_change, announce_other_breaking_change -upcoming_breaking_changes['bar foo'] = AzCLIBeRequired('--name') -upcoming_breaking_changes['bar foo baz'] = [AzCLIDefaultChange('--foobar', 'A', 'B'), AzCLIOtherChange( - 'During May 2024, another Breaking Change would happen in Build Event.')] +announce_required_flag_breaking_change('bar foo', '--name') +announce_default_value_breaking_change('bar foo baz', '--foobar', 'A', 'B') +announce_other_breaking_change('bar foo baz', 'During May 2024, another Breaking Change would happen in Build Event.') ``` -All related breaking changes would be displayed while executing the command. For example, in the above +All related breaking changes will be displayed while executing the command. For example, in the above declarations, the following warnings will be output when executing the command: ```shell # The azure command az bar foo baz # =====Warning output===== -# The argument `--name` will become required in next breaking change release(2.61.0). -# The default value of `--foobar` will be changed to `B` from `A` in next breaking change release(2.61.0). +# The argument '--name' will become required in next breaking change release(2.61.0). +# The default value of '--foobar' will be changed to 'B' from 'A' in next breaking change release(2.61.0). # During May 2024, another Breaking Change would happen in Build Event. ``` -There are several types of breaking changes defined in `breaking_change`. You should use any of them to declare breaking changes in `_breaking_change.py`. +There are several types of breaking changes defined in `breaking_change.py`. You should use any of them to declare breaking changes in `_breaking_change.py`. + +**Deprecate** + +Declaring deprecation in `_breaking_change.py` is similar to deprecation when authoring commands. You can use the following method to declare deprecation: + +* `announce_command_group_deprecate`: Deprecating a command group. +* `announce_command_deprecate`: Deprecating a command. +* `announce_argument_deprecate`: Deprecating an argument or option. + +They share similar arguments: + +* `command_group/command`: The name of the command group or command. +* `argument`: The name of the argument or option to be deprecated. If it is one of the options in `options_list`, the declaration will deprecate the option instead of the entire argument. +* `redirect`: This is the alternative that should be used in lieu of the deprecated thing. If not provided, the item is expected to be removed in the future with no replacement. +* `hide`: Hide the deprecated item from the help system, reducing discoverability, but still allow it to be used. Accepts either the boolean `True` to immediately hide the item or a core CLI version. If a version is supplied, the item will appear until the core CLI rolls to the specified value, after which it will be hidden. +* `target_version`: The version when the deprecated item should be removed. This version will be communicated in all warning messages. The `target_version` is the next breaking change window by default. The deprecated item will still function at the input version. + +```python +from azure.cli.core.breaking_change import announce_command_group_deprecate, announce_command_deprecate, announce_argument_deprecate + +announce_command_group_deprecate('bar', redirect='baz') +# Warning Message: This command group has been deprecated and will be removed in next breaking change release(2.67.0). Use `baz` instead. + +announce_command_deprecate('bar foo', redirect='baz foo', hide=True) +# Warning Message: This command has been deprecated and will be removed in next breaking change release(2.67.0). Use `baz foo` instead. + +announce_argument_deprecate('bar foo', '--name', target_version='2.70.0') +# Warning Message: Option `--name` has been deprecated and will be removed in 2.70.0. +``` + +> Note: The declared deprecation would be transformed into `knack.deprecation.Deprecated` item during runtime. The `tag_func` and `message_func` will remain effective. However, due to the timing of the transformation, the `expiration` will not take effect. **Remove** +To declare the removal of an item, use the deprecation method instead. + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIRemoveChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import announce_argument_deprecate -# Remove the command groups, commands or arguments in a future release. -# **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Removal.** -upcoming_breaking_changes['bar foo'] = AzCLIRemoveChange('az bar foo', target_version=NextBreakingChangeWindow(), - redirect='`az barfoo`') -# `az bar foo` will be removed in next breaking change release(2.61.0). Please use `az barfoo` instead. +announce_argument_deprecate('bar foo', '--name', target_version='2.70.0') +# Warning Message: Option `--name` has been deprecated and will be removed in 2.70.0. ``` **Rename** +To declare the renaming of an item, use the deprecation method. + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIRenameChange, NextBreakingChangeWindow - -# Rename the command groups, commands or arguments to a new name in a future release. -# **It is recommended to utilize `deprecate_info` instead of this class to pre-announce Breaking Change of Renaming.** -# It is recommended that the old name and the new name should be reserved in few releases. -upcoming_breaking_changes['bar foo'] = AzCLIRenameChange('az bar foo', 'az bar baz', - target_version=NextBreakingChangeWindow()) -# `az bar foo` will be renamed to `az bar baz` in next breaking change release(2.61.0). +from azure.cli.core.breaking_change import announce_argument_deprecate + +announce_argument_deprecate('bar foo', '--name', '--new-name') +# Warning Message: Option `--name` has been deprecated and will be removed in next breaking change release(2.67.0). Use `--new-name` instead. ``` **Output Change** +Announce breaking changes that affect the output of a command. This ensures users are aware of modifications to the command’s output format or content. + +* `command`: The name of the command group or command. If it is a command group, the warning would show in the execution of all commands in the group. +* `description`: The description of the breaking change. The description will display in warning messages. +* `target_version`: The version when the deprecated item should be removed. The `target_version` is the next breaking change window by default. +* `guide`: The guide that customers could take action to prepare for the future breaking change. +* `doc_link`: A link to related documentation, which will be displayed in warning messages. + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIOutputChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import announce_output_breaking_change -# The output of the command will be changed in a future release. -upcoming_breaking_changes['bar foo'] = AzCLIOutputChange('Reduce the output field `baz`', - target_version=NextBreakingChangeWindow()) -# The output will be changed in next breaking change release(2.61.0). Reduce the output field `baz`. +announce_output_breaking_change('bar foo', description='Reduce the output field `baz`', guide='You could retrieve this field through `az another command`.') +# The output will be changed in next breaking change release(2.61.0). Reduce the output field `baz`. You could retrieve this field through `az another command`. ``` **Logic Change** +Announce breaking changes in the logic of the command. + +* `command`: The name of the command. +* `summary`: Summary of the breaking change, which will be displayed in warning messages. +* `target_version`: The version when the breaking change should happen. The `target_version` is the next breaking change window by default. +* `detail`: A detailed description of the breaking change, including the actions customers should take. +* `doc_link`: A link to related documentation, which will be displayed in warning messages. + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLILogicChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import announce_logic_breaking_change -# There would be a breaking change in the logic of the command in future release. -upcoming_breaking_changes['bar foo'] = AzCLILogicChange('Update the validator', - target_version=NextBreakingChangeWindow(), - detail='The xxx will not be accepted.') +announce_logic_breaking_change('bar foo', 'Update the validator', detail='The xxx will not be accepted.') # Update the validator in next breaking change release(2.61.0). The xxx will not be accepted. ``` **Default Change** +Announce breaking changes caused by changes in default values. This ensures users are aware of modifications to default values. + +* `command`: The name of the command. +* `arg`: The name of the argument or one of its options. The default change warning will display whether the argument is used or not. +* `current_default`: The current default value of the argument. +* `new_default`: The new default value of the argument. +* `target_version`: The version in which the breaking change should happen. By default, this is set to the next breaking change window. +* `target`: Use this field to overwrite the argument display in warning messages. +* `doc_link`: A link to related documentation, which will be displayed in warning messages. + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIDefaultChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import announce_default_value_breaking_change -# The default value of an argument would be changed in a future release. -upcoming_breaking_changes['bar foo'] = AzCLIDefaultChange('--type', 'TypeA', 'TypeB', - target_version=NextBreakingChangeWindow()) +announce_default_value_breaking_change('bar foo', '--type', 'TypeA', 'TypeB') # The default value of `--type` will be changed to `TypeB` from `TypeA` in next breaking change release(2.61.0). ``` **Be Required** +Announce breaking changes that will make an argument required in a future release. This ensures users are aware of upcoming mandatory parameters. + +* `command`: The name of the command. +* `arg`: The name of the argument that will become required. +* `target_version`: The version in which the argument will become required. By default, this is set to the next breaking change window. +* `target`: Use this field to overwrite the argument display in warning messages. +* `doc_link`: A link to related documentation, which will be displayed in warning messages. + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIBeRequired, NextBreakingChangeWindow +from azure.cli.core.breaking_change import announce_required_flag_breaking_change -# The argument would become required in a future release. -upcoming_breaking_changes['bar foo'] = AzCLIBeRequired('--type', target_version=NextBreakingChangeWindow()) +announce_required_flag_breaking_change('bar foo', '--type') # The argument `--type` will become required in next breaking change release(2.61.0). ``` **Other Changes** +Announce other custom breaking changes that do not fall into the predefined categories. This allows for flexibility in communicating various types of breaking changes to users. + +* `command`: The name of the command. +* `message`: A description of the breaking change. +* `arg`: The name of the argument associated with the breaking change. If arg is not None, the warning message will only be displayed when the argument is used. +* `target_version`: The version in which the breaking change will occur. By default, this is set to the next breaking change window. This value won't display in warning message but is used to generate upcoming breaking change document. + ```python -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIOtherChange, NextBreakingChangeWindow +from azure.cli.core.breaking_change import announce_other_breaking_change -# Other custom breaking changes. -upcoming_breaking_changes['bar foo'] = AzCLIOtherChange( - 'During May 2024, another Breaking Change would happen in Build Event.', target_version=NextBreakingChangeWindow()) +announce_other_breaking_change('bar foo', 'During May 2024, another Breaking Change would happen in Build Event.') # During May 2024, another Breaking Change would happen in Build Event. ``` -To enhance flexibility in using the Breaking Change Pre-announcement, instead of the default automatic warning display prior to command execution, you may opt to specify the pre-announcement using a designated key in the format `{Command}.{NAME}`. +**Conditional Breaking Change** -```python -# src/azure-cli/azure/cli/command_modules/vm/_breaking_change.py -from azure.cli.core.breaking_change import upcoming_breaking_changes, AzCLIBeRequired +To enhance flexibility, the CLI supports using a designated tag to specify a Breaking Change Pre-announcement. This method avoids reliance on the default automatic warning display and allows the warning to be shown whenever `print_manual_breaking_change` is called. -upcoming_breaking_changes['bar foo.TYPE_REQUIRED'] = AzCLIBeRequired('--type') +**Note:** We strongly recommend using this method to display breaking change warnings under specific conditions instead of using `logger.warning` directly. This approach enables centralized documentation of breaking changes and assists in automating customer notifications. +```python # src/azure-cli/azure/cli/command_modules/vm/custom.py -# Use the pre-announcement. Replace `vm` with your module -import azure.cli.command_modules.vm._breaking_change # pylint: disable=unused-import -from azure.cli.core.breaking_change import upcoming_breaking_changes +from azure.cli.core.breaking_change import announce_conditional_breaking_change, AzCLIOtherChange + +announce_conditional_breaking_change(tag='SpecialBreakingChangeA', breaking_change=( +'vm create', 'This is special Breaking Change Warning A. This breaking change is happend in "vm create" command.')) +announce_conditional_breaking_change(tag='SpecialBreakingChangeB', breaking_change=( +'vm', 'This is special Breaking Change Warning B. This breaking change is happend in "vm" command group.')) -if not_use_type: - logger.warn(upcoming_breaking_changes['bar foo.TYPE_REQUIRED'].message) + +# src/azure-cli/azure/cli/command_modules/vm/custom.py +def create_vm(cmd, vm_name, **): + from azure.cli.core.breaking_change import print_manual_breaking_change + if some_condition: + print_manual_breaking_change(cmd.cli_ctx, tag='SpecialBreakingChangeA', custom_logger=logger) + print_manual_breaking_change(cmd.cli_ctx, tag='SpecialBreakingChangeB', custom_logger=logger) ``` This way, the pre-announcement wouldn't be display unless running into the branch, but still could be collected into the upcoming breaking change documentation. @@ -222,6 +270,12 @@ azdev breaking-change collect --target-version None The output should be a json object including the pre-announcement you made. +### Adopt Breaking Changes + +Breaking changes should be released with the announced CLI version, typically during the next breaking change window. The breaking change Pull Request must be reviewed by a CLI team member and merged before the sprint’s code freeze day. + +**Note:** Ensure the breaking change pre-announcement is removed in the same Pull Request. + ## Upcoming Breaking Change Documentation The Upcoming Breaking Change Documentation is released every sprint. This document lists the expected breaking changes for the next Breaking Change Release. However, due to the implementation’s dependency on the Service Team, not all the listed Breaking Changes may be adopted. diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index f9536549d0f..9c46ed9cc54 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -147,7 +147,11 @@ def version(self): # pylint: disable=too-few-public-methods class NextBreakingChangeWindow(TargetVersion): def __str__(self): - return f'in next breaking change release({_next_breaking_change_version()})' + next_breaking_change_version = _next_breaking_change_version() + if next_breaking_change_version: + return f'in next breaking change release({next_breaking_change_version})' + else: + return f'in next breaking change release' def version(self): return _next_breaking_change_version() @@ -578,7 +582,7 @@ def announce_argument_deprecate(command, argument, redirect=None, hide=None, announce_deprecate_info(command, argument, redirect=redirect, hide=hide, target_version=target_version, **kwargs) -def announce_manual_breaking_change(tag, breaking_change): +def announce_conditional_breaking_change(tag, breaking_change): upcoming_breaking_changes[breaking_change.command_name + '.' + tag].append(breaking_change) From a8cba275e50c15f151494ff79bffd30301a1fea7 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Thu, 8 Aug 2024 12:22:27 +0800 Subject: [PATCH 32/42] Rename fn and fix style problem --- src/azure-cli-core/azure/cli/core/breaking_change.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 9c46ed9cc54..e6235006af0 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -150,8 +150,7 @@ def __str__(self): next_breaking_change_version = _next_breaking_change_version() if next_breaking_change_version: return f'in next breaking change release({next_breaking_change_version})' - else: - return f'in next breaking change release' + return 'in next breaking change release' def version(self): return _next_breaking_change_version() @@ -586,7 +585,7 @@ def announce_conditional_breaking_change(tag, breaking_change): upcoming_breaking_changes[breaking_change.command_name + '.' + tag].append(breaking_change) -def print_manual_breaking_change(cli_ctx, tag, custom_logger=None): +def print_conditional_breaking_change(cli_ctx, tag, custom_logger=None): command = cli_ctx.invocation.command_name custom_logger = custom_logger or logger From 58332d8f8c5a7af1b66c0e2ab66baee3e149b18b Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Thu, 8 Aug 2024 14:46:56 +0800 Subject: [PATCH 33/42] Unflatten closure function & fix conditional breaking change --- .../azure/cli/core/breaking_change.py | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index e6235006af0..d5aa728daea 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -232,63 +232,65 @@ def to_tag(self, cli_ctx, **kwargs): return UpcomingBreakingChangeTag(cli_ctx, **tag_kwargs) def register(self, cli_ctx): - def register_option_deprecate(option_name, arguments): - for _, argument in arguments.items(): - if argument.options_list and len(argument.options_list) > 1: - for idx, option in enumerate(argument.options_list): - if isinstance(option, str) and option_name == option and isinstance(self, AzCLIDeprecate): - argument.options_list[idx] = self.to_tag(cli_ctx, object_type='option') - argument.options_list[idx].target = option - argument.action = _argument_breaking_change_action(cli_ctx, argument.options_list[idx], - argument.options['action']) - return True - return False - - def appended_status_tag(old_status_tag, new_status_tag): - if isinstance(old_status_tag, (Deprecated, UpcomingBreakingChangeTag)): - return MergedStatusTag(cli_ctx, old_status_tag, new_status_tag) - if isinstance(old_status_tag, MergedStatusTag): - old_status_tag.merge(new_status_tag) - return old_status_tag - return new_status_tag - - def register_to_direct_sub_cg_or_command(cg_name, status_tag): - for key, command_group in cli_ctx.invocation.commands_loader.command_group_table.items(): - if key.rsplit(maxsplit=1)[0] == cg_name: - from azure.cli.core.commands import AzCommandGroup - if isinstance(command_group, AzCommandGroup): - command_group.group_kwargs['deprecate_info'] = \ - appended_status_tag(command_group.group_kwargs.get('deprecate_info'), status_tag) - else: - register_to_direct_sub_cg_or_command(key, status_tag) - for key, command in cli_ctx.invocation.commands_loader.command_table.items(): - if key.rsplit(maxsplit=1)[0] == cg_name: - command.deprecate_info = appended_status_tag(command.deprecate_info, self.to_tag(cli_ctx)) - if self.args: command = cli_ctx.invocation.commands_loader.command_table.get(self.command_name) if not command: return for arg_name in self.args: - if register_option_deprecate(arg_name, command.arguments): + if self._register_option_deprecate(cli_ctx, command.arguments, arg_name): continue arg_name, arg = _find_arg(arg_name, command.arguments) if not arg: continue - arg.deprecate_info = appended_status_tag(arg.deprecate_info, self.to_tag(cli_ctx)) + arg.deprecate_info = self.appended_status_tag(cli_ctx, arg.deprecate_info, self.to_tag(cli_ctx)) arg.action = _argument_breaking_change_action(cli_ctx, arg.deprecate_info, arg.options['action']) elif self.is_command_group(cli_ctx): command_group = cli_ctx.invocation.commands_loader.command_group_table[self.command_name] if not command_group: - register_to_direct_sub_cg_or_command(self.command_name, self.to_tag(cli_ctx)) + self._register_to_direct_sub_cg_or_command(cli_ctx, self.command_name, self.to_tag(cli_ctx)) else: command_group.group_kwargs['deprecate_info'] = \ - appended_status_tag(command_group.group_kwargs.get('deprecate_info'), self.to_tag(cli_ctx)) + self.appended_status_tag(cli_ctx, command_group.group_kwargs.get('deprecate_info'), + self.to_tag(cli_ctx)) else: command = cli_ctx.invocation.commands_loader.command_table.get(self.cmd) if not command: return - command.deprecate_info = appended_status_tag(command.deprecate_info, self.to_tag(cli_ctx)) + command.deprecate_info = self.appended_status_tag(cli_ctx, command.deprecate_info, self.to_tag(cli_ctx)) + + @staticmethod + def appended_status_tag(cli_ctx, old_status_tag, new_status_tag): + if isinstance(old_status_tag, (Deprecated, UpcomingBreakingChangeTag)): + return MergedStatusTag(cli_ctx, old_status_tag, new_status_tag) + if isinstance(old_status_tag, MergedStatusTag): + old_status_tag.merge(new_status_tag) + return old_status_tag + return new_status_tag + + def _register_to_direct_sub_cg_or_command(self, cli_ctx, cg_name, status_tag): + for key, command_group in cli_ctx.invocation.commands_loader.command_group_table.items(): + if key.rsplit(maxsplit=1)[0] == cg_name: + from azure.cli.core.commands import AzCommandGroup + if isinstance(command_group, AzCommandGroup): + command_group.group_kwargs['deprecate_info'] = \ + self.appended_status_tag(cli_ctx, command_group.group_kwargs.get('deprecate_info'), status_tag) + else: + self._register_to_direct_sub_cg_or_command(cli_ctx, key, status_tag) + for key, command in cli_ctx.invocation.commands_loader.command_table.items(): + if key.rsplit(maxsplit=1)[0] == cg_name: + command.deprecate_info = self.appended_status_tag(cli_ctx, command.deprecate_info, self.to_tag(cli_ctx)) + + def _register_option_deprecate(self, cli_ctx, arguments, option_name): + for _, argument in arguments.items(): + if argument.options_list and len(argument.options_list) > 1: + for idx, option in enumerate(argument.options_list): + if isinstance(option, str) and option_name == option and isinstance(self, AzCLIDeprecate): + argument.options_list[idx] = self.to_tag(cli_ctx, object_type='option') + argument.options_list[idx].target = option + argument.action = _argument_breaking_change_action(cli_ctx, argument.options_list[idx], + argument.options['action']) + return True + return False class AzCLIDeprecate(BreakingChange): @@ -528,7 +530,10 @@ def register_upcoming_breaking_change_info(cli_ctx): from knack import events def update_breaking_change_info(cli_ctx, **kwargs): # pylint: disable=unused-argument - for breaking_changes in upcoming_breaking_changes.values(): + for key, breaking_changes in upcoming_breaking_changes.items(): + # Conditional Breaking Changes are announced with key `CommandName.Tag`. They should not be registered. + if '.' in key: + continue for breaking_change in breaking_changes: breaking_change.register(cli_ctx) @@ -586,11 +591,11 @@ def announce_conditional_breaking_change(tag, breaking_change): def print_conditional_breaking_change(cli_ctx, tag, custom_logger=None): - command = cli_ctx.invocation.command_name + command = cli_ctx.invocation.commands_loader.command_name custom_logger = custom_logger or logger command_comps = command.split() while command_comps: for breaking_change in upcoming_breaking_changes.get(' '.join(command_comps) + '.' + tag, []): - custom_logger.warning(breaking_change) + custom_logger.warning(breaking_change.message) del command_comps[-1] From 0a4105a159935cfab01b95bbc47f1e0120fccbac Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 9 Aug 2024 14:29:45 +0800 Subject: [PATCH 34/42] Add Unit Tests & Fix edge case. --- .../azure/cli/core/breaking_change.py | 15 +- .../azure/cli/core/commands/__init__.py | 1 + .../cli/core/tests/test_breaking_change.py | 357 ++++++++++++++++++ 3 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index d5aa728daea..3d8ca730f7f 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -269,7 +269,10 @@ def appended_status_tag(cli_ctx, old_status_tag, new_status_tag): def _register_to_direct_sub_cg_or_command(self, cli_ctx, cg_name, status_tag): for key, command_group in cli_ctx.invocation.commands_loader.command_group_table.items(): - if key.rsplit(maxsplit=1)[0] == cg_name: + split_key = key.rsplit(maxsplit=1) + # If inpass command group name is empty, all first level command group should be registered to. + # Otherwise, we need to find all direct sub command groups. + if (not cg_name and len(split_key) == 1) or (len(split_key) == 2 and split_key[0] == cg_name): from azure.cli.core.commands import AzCommandGroup if isinstance(command_group, AzCommandGroup): command_group.group_kwargs['deprecate_info'] = \ @@ -277,7 +280,8 @@ def _register_to_direct_sub_cg_or_command(self, cli_ctx, cg_name, status_tag): else: self._register_to_direct_sub_cg_or_command(cli_ctx, key, status_tag) for key, command in cli_ctx.invocation.commands_loader.command_table.items(): - if key.rsplit(maxsplit=1)[0] == cg_name: + # If inpass command group name is empty, all first level command should be registered to. + if (not cg_name and ' ' not in key) or key.rsplit(maxsplit=1)[0] == cg_name: command.deprecate_info = self.appended_status_tag(cli_ctx, command.deprecate_info, self.to_tag(cli_ctx)) def _register_option_deprecate(self, cli_ctx, arguments, option_name): @@ -285,6 +289,9 @@ def _register_option_deprecate(self, cli_ctx, arguments, option_name): if argument.options_list and len(argument.options_list) > 1: for idx, option in enumerate(argument.options_list): if isinstance(option, str) and option_name == option and isinstance(self, AzCLIDeprecate): + if isinstance(argument.options_list, tuple): + # Some of the command would declare options_list as tuple + argument.options_list = list(argument.options_list) argument.options_list[idx] = self.to_tag(cli_ctx, object_type='option') argument.options_list[idx].target = option argument.action = _argument_breaking_change_action(cli_ctx, argument.options_list[idx], @@ -407,11 +414,11 @@ def message(self): if self.guide: guide = self.guide.rstrip() if guide and guide[-1] not in ',.;?!': - guide = guide + '.' + guide = ' ' + guide + '.' else: guide = '' doc = self.format_doc_link(self.doc_link) - return f'The output will be changed {str(self.target_version)}. {desc} {guide}{doc}' + return f'The output will be changed {str(self.target_version)}. {desc}{guide}{doc}' class AzCLILogicChange(BreakingChange): diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 3534ca28fb1..727ccdcee08 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -754,6 +754,7 @@ def resolve_warnings(self, cmd, parsed_args): def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): deprecations = getattr(parsed_args, '_argument_deprecations', []) + # Handle `always_display` argument breaking changes for _, argument in parsed_args.func.arguments.items(): if isinstance(argument.deprecate_info, UpcomingBreakingChangeTag): if argument.deprecate_info.always_display: diff --git a/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py b/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py new file mode 100644 index 00000000000..611eef77b41 --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py @@ -0,0 +1,357 @@ +import io +import logging +import unittest +from unittest import mock + +from azure.cli.core import AzCommandsLoader +from azure.cli.core.mock import DummyCli + + +class TestCommandsLoader(AzCommandsLoader): + def __init__(self, cli_ctx=None, command_group_cls=None, argument_context_cls=None, suppress_extension=None, + **kwargs): + super().__init__(cli_ctx, command_group_cls, argument_context_cls, suppress_extension, **kwargs) + self.cmd_to_loader_map = {} + + def load_command_table(self, args): + super(TestCommandsLoader, self).load_command_table(args) + with self.command_group('test group', operations_tmpl='{}#TestCommandsLoader.{{}}'.format(__name__)) as g: + g.command('cmd', '_test_command') + self.cmd_to_loader_map['test group cmd'] = [self] + + return self.command_table + + def load_arguments(self, command): + super(TestCommandsLoader, self).load_arguments(command) + with self.argument_context('test group cmd') as c: + c.argument('arg1', options_list=('--arg1', '--arg1-alias', '-a')) + c.argument('arg2', options_list=('--arg2', '--arg2-alias', '--arg2-alias-long')) + self._update_command_definitions() + + @staticmethod + def _test_command(arg1=None, arg2=None): + pass + + +class TestBreakingChange(unittest.TestCase): + def setUp(self): + super().setUp() + from azure.cli.core.breaking_change import upcoming_breaking_changes + for key in upcoming_breaking_changes: + upcoming_breaking_changes[key] = [] + + def test_register_and_execute(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_other_breaking_change + + warning_message = 'Test Breaking Change in Test Group' + announce_other_breaking_change('test', warning_message) + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {warning_message}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning_message, captured_output.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', '--help']) + self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn(warning_message, captured_output.getvalue()) + + def test_command_group_deprecate(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_command_group_deprecate + + announce_command_group_deprecate('test group', redirect='test group1', target_version='2.70.0') + implicit_warning = "This command is implicitly deprecated because command group 'test "\ + "group' is deprecated and will be removed in a future release. Use 'test "\ + "group1' instead." + warning = "This command group has been deprecated and will be removed in 2.70.0. Use \'test group1\' instead." + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {implicit_warning}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(implicit_warning, captured_output.getvalue().replace('\n ', ' ')) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', '--help']) + self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', '--help']) + self.assertIn('[Deprecated]', captured_output.getvalue()) + + def test_command_deprecate(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_command_deprecate + + announce_command_deprecate('test group cmd', redirect='test group cmd1', target_version='2.70.0') + warning = "This command has been deprecated and will be removed in 2.70.0. Use \'test group cmd1\' instead." + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', '--help']) + self.assertIn('[Deprecated]', captured_output.getvalue()) + + @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') + def test_argument_deprecate(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_argument_deprecate + + announce_argument_deprecate('test group cmd', argument='arg1', redirect='arg2') + warning = ("Argument 'arg1' has been deprecated and will be removed in next breaking change release(3.0.0). " + "Use 'arg2' instead.") + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) + self.assertIn('[Deprecated]', captured_output.getvalue()) + + @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') + def test_option_deprecate(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_argument_deprecate + + announce_argument_deprecate('test group cmd', argument='--arg1', redirect='--arg1-alias') + warning = ("Option '--arg1' has been deprecated and will be removed in next breaking change release(3.0.0). " + "Use '--arg1-alias' instead.") + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) + print(captured_output.getvalue()) + self.assertIn(' --arg1 [Deprecated]', captured_output.getvalue()) + + @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') + def test_be_required(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_required_flag_breaking_change + + announce_required_flag_breaking_change('test group cmd', arg='--arg1') + warning = "The argument '--arg1' will become required in next breaking change release(3.0.0)." + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) + self.assertIn('[BrkChange]', captured_output.getvalue()) + + @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') + def test_default_change(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_default_value_breaking_change + + announce_default_value_breaking_change('test group cmd', arg='arg1', new_default='Default', + current_default='None') + warning = ("The default value of 'arg1' will be changed to 'Default' from 'None' " + "in next breaking change release(3.0.0).") + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) + self.assertIn('[BrkChange]', captured_output.getvalue()) + + @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') + def test_output_change(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_output_breaking_change + + announce_output_breaking_change('test group cmd', description="The output of 'test group cmd' " + "would be changed.") + warning = ("The output will be changed in next breaking change release(3.0.0). The output of 'test group cmd' " + "would be changed.") + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', '--help']) + self.assertIn('[BrkChange]', captured_output.getvalue()) + + @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') + def test_logic_change(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_logic_breaking_change + + announce_logic_breaking_change('test group cmd', summary="Logic Change Summary") + warning = "Logic Change Summary in next breaking change release(3.0.0)." + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertEqual(f'WARNING: {warning}\n', captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', '--help']) + self.assertIn('[BrkChange]', captured_output.getvalue()) + + @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') + def test_multi_breaking_change(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_logic_breaking_change, announce_argument_deprecate, \ + announce_required_flag_breaking_change + + announce_argument_deprecate('test group cmd', argument='--arg1', redirect='--arg1-alias') + warning1 = ("Option '--arg1' has been deprecated and will be removed in next breaking change release(3.0.0). " + "Use '--arg1-alias' instead.") + announce_required_flag_breaking_change('test group cmd', arg='--arg1') + warning2 = "The argument '--arg1' will become required in next breaking change release(3.0.0)." + announce_logic_breaking_change('test group cmd', summary="Logic Change Summary") + warning3 = "Logic Change Summary in next breaking change release(3.0.0)." + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertIn(warning1, captured_err.getvalue()) + self.assertIn(warning2, captured_err.getvalue()) + self.assertIn(warning3, captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertIn(warning1, captured_output.getvalue().replace('\n ', ' ')) + self.assertIn(warning2, captured_output.getvalue().replace('\n ', ' ')) + self.assertIn(warning3, captured_output.getvalue().replace('\n ', ' ')) + self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn('[Deprecated]', captured_output.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', '--help']) + self.assertIn('[BrkChange]', captured_output.getvalue()) + + @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') + def test_conditional_breaking_change(self): + from contextlib import redirect_stderr, redirect_stdout + + from azure.cli.core.breaking_change import announce_conditional_breaking_change, AzCLIOtherChange, \ + print_conditional_breaking_change + + warning_message = 'Test Breaking Change in Test Group' + announce_conditional_breaking_change('TestConditional', AzCLIOtherChange('test group cmd', warning_message)) + cli = DummyCli(commands_loader_cls=TestCommandsLoader) + + captured_err = io.StringIO() + with redirect_stderr(captured_err): + cli.invoke(['test', 'group', 'cmd', '--arg1', '1', '--arg2', '2']) + self.assertNotIn(warning_message, captured_err.getvalue()) + + captured_output = io.StringIO() + with redirect_stdout(captured_output): + with self.assertRaises(SystemExit): + cli.invoke(['test', 'group', 'cmd', '--help']) + self.assertNotIn(warning_message, captured_err.getvalue().replace('\n ', ' ')) + self.assertNotIn('[BrkChange]', captured_output.getvalue()) + + cli_ctx = mock.MagicMock() + cli_ctx.invocation.commands_loader.command_name = 'test group cmd' + logger = logging.getLogger('TestLogger') + with self.assertLogs(logger=logger, level='WARNING') as cm: + print_conditional_breaking_change(cli_ctx, 'TestConditional', custom_logger=logger) + self.assertListEqual([f'WARNING:TestLogger:{warning_message}'], cm.output) From 6ab41749dd1d05b531a914ad69af55fa6abb8896 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 9 Aug 2024 14:36:59 +0800 Subject: [PATCH 35/42] Add License for new test file --- .../azure/cli/core/tests/test_breaking_change.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py b/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py index 611eef77b41..5b89937b2f0 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py @@ -1,3 +1,7 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- import io import logging import unittest From 3089fca1fd3a0a0143bac17658399cf78f7c09df Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 9 Aug 2024 15:13:41 +0800 Subject: [PATCH 36/42] Add some comments --- src/azure-cli-core/azure/cli/core/_help.py | 5 +++++ src/azure-cli-core/azure/cli/core/breaking_change.py | 3 +++ src/azure-cli-core/azure/cli/core/commands/__init__.py | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index 681b6925b35..04987de97f4 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -259,6 +259,8 @@ def __init__(self, help_ctx, delimiters): direct_deprecate_info = deprecate_info elif isinstance(deprecate_info, UpcomingBreakingChangeTag): breaking_changes.append(deprecate_info) + # If there are more than two `deprecate_info` and/or upcoming breaking changes, + # extract them and store separately from the merged status tag. elif isinstance(deprecate_info, MergedStatusTag): depr, bcs = CliHelpFile.classify_merged_status_tag(deprecate_info) direct_deprecate_info = depr[0] if depr else None @@ -273,6 +275,8 @@ def __init__(self, help_ctx, delimiters): implicit_deprecate_info = deprecate_info elif isinstance(deprecate_info, UpcomingBreakingChangeTag): breaking_changes.append(deprecate_info) + # If there are more than two `deprecate_info` and/or upcoming breaking changes, + # extract them and store separately from the merged status tag. elif isinstance(deprecate_info, MergedStatusTag): depr, bcs = CliHelpFile.classify_merged_status_tag(deprecate_info) if depr and implicit_deprecate_info is None: @@ -293,6 +297,7 @@ def __init__(self, help_ctx, delimiters): all_deprecate_info = [self.deprecate_info] if self.deprecate_info else [] all_deprecate_info.extend(breaking_changes) if len(all_deprecate_info) > 1: + # Merge multiple `deprecate_info` and/or breaking changes so their messages can be displayed together. self.deprecate_info = MergedStatusTag(help_ctx.cli_ctx, *all_deprecate_info) elif all_deprecate_info: self.deprecate_info = all_deprecate_info[0] diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 3d8ca730f7f..5aeedcd3050 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -93,6 +93,9 @@ def expired(self): class MergedStatusTag(StatusTag): + # This class is used to merge multiple status tags into one. + # It is particularly useful when multiple breaking changes and deprecation information need to be recorded + # in a single deprecate_info field. def __init__(self, cli_ctx, *tags): assert len(tags) > 0 diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index 727ccdcee08..ac58b88d4ae 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -756,6 +756,8 @@ def _resolve_preview_and_deprecation_warnings(self, cmd, parsed_args): deprecations = getattr(parsed_args, '_argument_deprecations', []) # Handle `always_display` argument breaking changes for _, argument in parsed_args.func.arguments.items(): + # Some arguments have breaking changes that must always be displayed. + # Iterate through them and show the warnings. if isinstance(argument.deprecate_info, UpcomingBreakingChangeTag): if argument.deprecate_info.always_display: deprecations.append(argument.deprecate_info) From de31c561a0dfeb3c408061eb811e1f7d1f992b94 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Fri, 9 Aug 2024 15:17:15 +0800 Subject: [PATCH 37/42] Add deprecation recommendation --- doc/how_to_introduce_breaking_changes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 837066f899f..ae8b357488c 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -91,7 +91,7 @@ There are several types of breaking changes defined in `breaking_change.py`. You **Deprecate** -Declaring deprecation in `_breaking_change.py` is similar to deprecation when authoring commands. You can use the following method to declare deprecation: +Declaring deprecation in `_breaking_change.py` is similar to deprecation when authoring commands. It is recommended to use this method rather than declaring deprecate_info when defining a command or argument. You can use the following method to declare deprecation: * `announce_command_group_deprecate`: Deprecating a command group. * `announce_command_deprecate`: Deprecating a command. @@ -246,10 +246,10 @@ announce_conditional_breaking_change(tag='SpecialBreakingChangeB', breaking_chan # src/azure-cli/azure/cli/command_modules/vm/custom.py def create_vm(cmd, vm_name, **): - from azure.cli.core.breaking_change import print_manual_breaking_change + from azure.cli.core.breaking_change import print_conditional_breaking_change if some_condition: - print_manual_breaking_change(cmd.cli_ctx, tag='SpecialBreakingChangeA', custom_logger=logger) - print_manual_breaking_change(cmd.cli_ctx, tag='SpecialBreakingChangeB', custom_logger=logger) + print_conditional_breaking_change(cmd.cli_ctx, tag='SpecialBreakingChangeA', custom_logger=logger) + print_conditional_breaking_change(cmd.cli_ctx, tag='SpecialBreakingChangeB', custom_logger=logger) ``` This way, the pre-announcement wouldn't be display unless running into the branch, but still could be collected into the upcoming breaking change documentation. From b28c11f7710741edaea2be1457ebe1990dd77678 Mon Sep 17 00:00:00 2001 From: Qinkai Wu <32201005+ReaNAiveD@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:10:24 +0800 Subject: [PATCH 38/42] Update doc/how_to_introduce_breaking_changes.md Co-authored-by: Xing Zhou --- doc/how_to_introduce_breaking_changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index ae8b357488c..046330035bf 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -91,7 +91,7 @@ There are several types of breaking changes defined in `breaking_change.py`. You **Deprecate** -Declaring deprecation in `_breaking_change.py` is similar to deprecation when authoring commands. It is recommended to use this method rather than declaring deprecate_info when defining a command or argument. You can use the following method to declare deprecation: +Declaring deprecation in `_breaking_change.py` is similar to deprecation when authoring commands. It is recommended to use this method rather than declaring `deprecate_info` when defining a command or argument. You can use the following method to declare deprecation: * `announce_command_group_deprecate`: Deprecating a command group. * `announce_command_deprecate`: Deprecating a command. From eba6aa219634f9e02f81ec9571ede67acd817c6e Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Mon, 12 Aug 2024 10:11:07 +0800 Subject: [PATCH 39/42] Apply Document suggestion from jeremy --- doc/how_to_introduce_breaking_changes.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 046330035bf..54d1893f09a 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -19,7 +19,9 @@ To mitigate the impact of breaking changes, Azure CLI delays breaking changes an The breaking change window is a designated sprint that **permits** the merging of service command breaking changes. Any Pull Request merged during this sprint will be included in the subsequent Breaking Change Release. -The timing of the breaking change window in Azure CLI aligns with [Microsoft Build](https://build.microsoft.com/) and [Microsoft Ignite](https://ignite.microsoft.com/). You could find the next Breaking Change Release plan in our [milestones](https://github.com/Azure/azure-cli/milestones). +The timing of the breaking change window in Azure CLI aligns with [Microsoft Build](https://build.microsoft.com/) and [Microsoft Ignite](https://ignite.microsoft.com/). It normally occurs in May for Build and November for Ignite. So please prepare beforehand to align command breaking changes with Azure CLI team accordingly. + +You could find the next Breaking Change Release plan in our [milestones](https://github.com/Azure/azure-cli/milestones). > It's highlighted that the introduction of breaking changes is typically prohibited within non-breaking-change window, based on what we stated above for consistency and stable tooling experience. > @@ -29,10 +31,10 @@ The timing of the breaking change window in Azure CLI aligns with [Microsoft Bui ### Pre-announce Breaking Changes -All breaking changes **must** be pre-announced two sprints ahead Release. There are two approaches to inform both interactive users and automatic users about the breaking changes. +All breaking changes **must** be pre-announced two sprints ahead Release. It give users the buffer time ahead to mitigate for better command experience. There are two approaches to inform both interactive users and automatic users about the breaking changes. 1. (**Mandatory**) Breaking Changes must be pre-announced through Warning Log while executing. -2. (*Automatic*) Breaking Changes would be collected automatically and listed in [Upcoming Breaking Change](https://learn.microsoft.com/en-us/cli/azure/upcoming-breaking-changes). +2. (*Automatic*) Breaking Changes would be collected automatically and listed in [Upcoming Breaking Change](https://learn.microsoft.com/en-us/cli/azure/upcoming-breaking-changes) Document. ## Workflow From e3c1f887404a410f1ca81fa33a1c11a419364fc7 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Wed, 14 Aug 2024 16:11:34 +0800 Subject: [PATCH 40/42] Apply suggestions from the meeting --- doc/how_to_introduce_breaking_changes.md | 80 ++++++++++--------- .../azure/cli/core/breaking_change.py | 28 +++---- .../cli/core/tests/test_breaking_change.py | 68 ++++++++-------- 3 files changed, 89 insertions(+), 87 deletions(-) diff --git a/doc/how_to_introduce_breaking_changes.md b/doc/how_to_introduce_breaking_changes.md index 54d1893f09a..38689a99bce 100644 --- a/doc/how_to_introduce_breaking_changes.md +++ b/doc/how_to_introduce_breaking_changes.md @@ -49,7 +49,7 @@ All breaking changes **must** be pre-announced two sprints ahead Release. It giv * The pre-announcement should be released ahead of Breaking Change Window. * After releasing the pre-announcement, a pipeline would be triggered, and the Upcoming Breaking Change Documentation would be updated. * At the start of Breaking Change window, the CLI team would notify Service Teams to adopt Breaking Changes. -* Breaking Changes should be adopted within Breaking Change Window. +* Breaking Changes should be adopted within Breaking Change Window. Any unfinished pre-announcements of breaking changes targeting this release will be deleted by the CLI team. ### Pre-announce Breaking Changes @@ -69,12 +69,11 @@ To pre-announce breaking changes, such as modifications to default argument valu You can then pre-announce breaking changes for different command groups or commands. Multiple breaking changes on the same command are accepted. ```python -from azure.cli.core.breaking_change import announce_required_flag_breaking_change, \ - announce_default_value_breaking_change, announce_other_breaking_change +from azure.cli.core.breaking_change import register_required_flag_breaking_change, register_default_value_breaking_change, register_other_breaking_change -announce_required_flag_breaking_change('bar foo', '--name') -announce_default_value_breaking_change('bar foo baz', '--foobar', 'A', 'B') -announce_other_breaking_change('bar foo baz', 'During May 2024, another Breaking Change would happen in Build Event.') +register_required_flag_breaking_change('bar foo', '--name') +register_default_value_breaking_change('bar foo baz', '--foobar', 'A', 'B') +register_other_breaking_change('bar foo baz', 'During May 2024, another Breaking Change would happen in Build Event.') ``` All related breaking changes will be displayed while executing the command. For example, in the above declarations, the following warnings will be output when executing the command: @@ -95,9 +94,11 @@ There are several types of breaking changes defined in `breaking_change.py`. You Declaring deprecation in `_breaking_change.py` is similar to deprecation when authoring commands. It is recommended to use this method rather than declaring `deprecate_info` when defining a command or argument. You can use the following method to declare deprecation: -* `announce_command_group_deprecate`: Deprecating a command group. -* `announce_command_deprecate`: Deprecating a command. -* `announce_argument_deprecate`: Deprecating an argument or option. +* `register_command_group_deprecate`: Deprecating a command group. +* `register_command_deprecate`: Deprecating a command. +* `register_argument_deprecate`: Deprecating an argument or option. + +> **Note:** Avoid marking an item with both a deprecation and another breaking change. A command group, command, or argument cannot be deprecated while also undergoing other breaking changes. They share similar arguments: @@ -108,15 +109,15 @@ They share similar arguments: * `target_version`: The version when the deprecated item should be removed. This version will be communicated in all warning messages. The `target_version` is the next breaking change window by default. The deprecated item will still function at the input version. ```python -from azure.cli.core.breaking_change import announce_command_group_deprecate, announce_command_deprecate, announce_argument_deprecate +from azure.cli.core.breaking_change import register_command_group_deprecate, register_command_deprecate, register_argument_deprecate -announce_command_group_deprecate('bar', redirect='baz') +register_command_group_deprecate('bar', redirect='baz') # Warning Message: This command group has been deprecated and will be removed in next breaking change release(2.67.0). Use `baz` instead. -announce_command_deprecate('bar foo', redirect='baz foo', hide=True) +register_command_deprecate('bar foo', redirect='baz foo', hide=True) # Warning Message: This command has been deprecated and will be removed in next breaking change release(2.67.0). Use `baz foo` instead. -announce_argument_deprecate('bar foo', '--name', target_version='2.70.0') +register_argument_deprecate('bar foo', '--name', target_version='2.70.0') # Warning Message: Option `--name` has been deprecated and will be removed in 2.70.0. ``` @@ -124,29 +125,29 @@ announce_argument_deprecate('bar foo', '--name', target_version='2.70.0') **Remove** -To declare the removal of an item, use the deprecation method instead. +To declare the removal of an item, use the deprecation method instead. ```python -from azure.cli.core.breaking_change import announce_argument_deprecate +from azure.cli.core.breaking_change import register_argument_deprecate -announce_argument_deprecate('bar foo', '--name', target_version='2.70.0') +register_argument_deprecate('bar foo', '--name', target_version='2.70.0') # Warning Message: Option `--name` has been deprecated and will be removed in 2.70.0. ``` **Rename** -To declare the renaming of an item, use the deprecation method. +To declare the renaming of an item, use the deprecation method. ```python -from azure.cli.core.breaking_change import announce_argument_deprecate +from azure.cli.core.breaking_change import register_argument_deprecate -announce_argument_deprecate('bar foo', '--name', '--new-name') +register_argument_deprecate('bar foo', '--name', '--new-name') # Warning Message: Option `--name` has been deprecated and will be removed in next breaking change release(2.67.0). Use `--new-name` instead. ``` **Output Change** -Announce breaking changes that affect the output of a command. This ensures users are aware of modifications to the command’s output format or content. +Declare breaking changes that affect the output of a command. This ensures users are aware of modifications to the command’s output format or content. * `command`: The name of the command group or command. If it is a command group, the warning would show in the execution of all commands in the group. * `description`: The description of the breaking change. The description will display in warning messages. @@ -155,15 +156,16 @@ Announce breaking changes that affect the output of a command. This ensures user * `doc_link`: A link to related documentation, which will be displayed in warning messages. ```python -from azure.cli.core.breaking_change import announce_output_breaking_change +from azure.cli.core.breaking_change import register_output_breaking_change -announce_output_breaking_change('bar foo', description='Reduce the output field `baz`', guide='You could retrieve this field through `az another command`.') +register_output_breaking_change('bar foo', description='Reduce the output field `baz`', + guide='You could retrieve this field through `az another command`.') # The output will be changed in next breaking change release(2.61.0). Reduce the output field `baz`. You could retrieve this field through `az another command`. ``` **Logic Change** -Announce breaking changes in the logic of the command. +Declare breaking changes in the logic of the command. * `command`: The name of the command. * `summary`: Summary of the breaking change, which will be displayed in warning messages. @@ -172,15 +174,15 @@ Announce breaking changes in the logic of the command. * `doc_link`: A link to related documentation, which will be displayed in warning messages. ```python -from azure.cli.core.breaking_change import announce_logic_breaking_change +from azure.cli.core.breaking_change import register_logic_breaking_change -announce_logic_breaking_change('bar foo', 'Update the validator', detail='The xxx will not be accepted.') +register_logic_breaking_change('bar foo', 'Update the validator', detail='The xxx will not be accepted.') # Update the validator in next breaking change release(2.61.0). The xxx will not be accepted. ``` **Default Change** -Announce breaking changes caused by changes in default values. This ensures users are aware of modifications to default values. +Declare breaking changes caused by changes in default values. This ensures users are aware of modifications to default values. * `command`: The name of the command. * `arg`: The name of the argument or one of its options. The default change warning will display whether the argument is used or not. @@ -191,15 +193,15 @@ Announce breaking changes caused by changes in default values. This ensures user * `doc_link`: A link to related documentation, which will be displayed in warning messages. ```python -from azure.cli.core.breaking_change import announce_default_value_breaking_change +from azure.cli.core.breaking_change import register_default_value_breaking_change -announce_default_value_breaking_change('bar foo', '--type', 'TypeA', 'TypeB') +register_default_value_breaking_change('bar foo', '--type', 'TypeA', 'TypeB') # The default value of `--type` will be changed to `TypeB` from `TypeA` in next breaking change release(2.61.0). ``` **Be Required** -Announce breaking changes that will make an argument required in a future release. This ensures users are aware of upcoming mandatory parameters. +Declare breaking changes that will make an argument required in a future release. This ensures users are aware of upcoming mandatory parameters. * `command`: The name of the command. * `arg`: The name of the argument that will become required. @@ -208,15 +210,15 @@ Announce breaking changes that will make an argument required in a future releas * `doc_link`: A link to related documentation, which will be displayed in warning messages. ```python -from azure.cli.core.breaking_change import announce_required_flag_breaking_change +from azure.cli.core.breaking_change import register_required_flag_breaking_change -announce_required_flag_breaking_change('bar foo', '--type') +register_required_flag_breaking_change('bar foo', '--type') # The argument `--type` will become required in next breaking change release(2.61.0). ``` **Other Changes** -Announce other custom breaking changes that do not fall into the predefined categories. This allows for flexibility in communicating various types of breaking changes to users. +Declare other custom breaking changes that do not fall into the predefined categories. This allows for flexibility in communicating various types of breaking changes to users. * `command`: The name of the command. * `message`: A description of the breaking change. @@ -224,9 +226,9 @@ Announce other custom breaking changes that do not fall into the predefined cate * `target_version`: The version in which the breaking change will occur. By default, this is set to the next breaking change window. This value won't display in warning message but is used to generate upcoming breaking change document. ```python -from azure.cli.core.breaking_change import announce_other_breaking_change +from azure.cli.core.breaking_change import register_other_breaking_change -announce_other_breaking_change('bar foo', 'During May 2024, another Breaking Change would happen in Build Event.') +register_other_breaking_change('bar foo', 'During May 2024, another Breaking Change would happen in Build Event.') # During May 2024, another Breaking Change would happen in Build Event. ``` @@ -238,12 +240,12 @@ To enhance flexibility, the CLI supports using a designated tag to specify a Bre ```python # src/azure-cli/azure/cli/command_modules/vm/custom.py -from azure.cli.core.breaking_change import announce_conditional_breaking_change, AzCLIOtherChange +from azure.cli.core.breaking_change import register_conditional_breaking_change, AzCLIOtherChange -announce_conditional_breaking_change(tag='SpecialBreakingChangeA', breaking_change=( -'vm create', 'This is special Breaking Change Warning A. This breaking change is happend in "vm create" command.')) -announce_conditional_breaking_change(tag='SpecialBreakingChangeB', breaking_change=( -'vm', 'This is special Breaking Change Warning B. This breaking change is happend in "vm" command group.')) +register_conditional_breaking_change(tag='SpecialBreakingChangeA', breaking_change=( + 'vm create', 'This is special Breaking Change Warning A. This breaking change is happend in "vm create" command.')) +register_conditional_breaking_change(tag='SpecialBreakingChangeB', breaking_change=( + 'vm', 'This is special Breaking Change Warning B. This breaking change is happend in "vm" command group.')) # src/azure-cli/azure/cli/command_modules/vm/custom.py diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 5aeedcd3050..6c520e2f3d8 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -14,7 +14,7 @@ logger = get_logger() NEXT_BREAKING_CHANGE_RELEASE = '2.67.0' -DEFAULT_BREAKING_CHANGE_TAG = '[BrkChange]' +DEFAULT_BREAKING_CHANGE_TAG = '[BreakingChange]' def _get_action_class(cli_ctx, action): @@ -550,53 +550,53 @@ def update_breaking_change_info(cli_ctx, **kwargs): # pylint: disable=unused-ar cli_ctx.register_event(events.EVENT_INVOKER_POST_CMD_TBL_CREATE, update_breaking_change_info) -def announce_deprecate_info(command_name, arg=None, target_version=NextBreakingChangeWindow(), **kwargs): +def register_deprecate_info(command_name, arg=None, target_version=NextBreakingChangeWindow(), **kwargs): upcoming_breaking_changes[command_name].append(AzCLIDeprecate(command_name, arg, target_version, **kwargs)) -def announce_output_breaking_change(command_name, description, target_version=NextBreakingChangeWindow(), guide=None, +def register_output_breaking_change(command_name, description, target_version=NextBreakingChangeWindow(), guide=None, doc_link=None): upcoming_breaking_changes[command_name].append( AzCLIOutputChange(command_name, description, target_version, guide, doc_link)) -def announce_logic_breaking_change(command_name, summary, target_version=NextBreakingChangeWindow(), detail=None, +def register_logic_breaking_change(command_name, summary, target_version=NextBreakingChangeWindow(), detail=None, doc_link=None): upcoming_breaking_changes[command_name].append( AzCLILogicChange(command_name, summary, target_version, detail, doc_link)) -def announce_default_value_breaking_change(command_name, arg, current_default, new_default, +def register_default_value_breaking_change(command_name, arg, current_default, new_default, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): upcoming_breaking_changes[command_name].append( AzCLIDefaultChange(command_name, arg, current_default, new_default, target_version, target, doc_link)) -def announce_required_flag_breaking_change(command_name, arg, target_version=NextBreakingChangeWindow(), target=None, +def register_required_flag_breaking_change(command_name, arg, target_version=NextBreakingChangeWindow(), target=None, doc_link=None): upcoming_breaking_changes[command_name].append(AzCLIBeRequired(command_name, arg, target_version, target, doc_link)) -def announce_other_breaking_change(command_name, message, arg=None, target_version=NextBreakingChangeWindow()): +def register_other_breaking_change(command_name, message, arg=None, target_version=NextBreakingChangeWindow()): upcoming_breaking_changes[command_name].append(AzCLIOtherChange(command_name, message, arg, target_version)) -def announce_command_group_deprecate(command_group, redirect=None, hide=None, +def register_command_group_deprecate(command_group, redirect=None, hide=None, target_version=NextBreakingChangeWindow(), **kwargs): - announce_deprecate_info(command_group, redirect=redirect, hide=hide, target_version=target_version, **kwargs) + register_deprecate_info(command_group, redirect=redirect, hide=hide, target_version=target_version, **kwargs) -def announce_command_deprecate(command, redirect=None, hide=None, +def register_command_deprecate(command, redirect=None, hide=None, target_version=NextBreakingChangeWindow(), **kwargs): - announce_deprecate_info(command, redirect=redirect, hide=hide, target_version=target_version, **kwargs) + register_deprecate_info(command, redirect=redirect, hide=hide, target_version=target_version, **kwargs) -def announce_argument_deprecate(command, argument, redirect=None, hide=None, +def register_argument_deprecate(command, argument, redirect=None, hide=None, target_version=NextBreakingChangeWindow(), **kwargs): - announce_deprecate_info(command, argument, redirect=redirect, hide=hide, target_version=target_version, **kwargs) + register_deprecate_info(command, argument, redirect=redirect, hide=hide, target_version=target_version, **kwargs) -def announce_conditional_breaking_change(tag, breaking_change): +def register_conditional_breaking_change(tag, breaking_change): upcoming_breaking_changes[breaking_change.command_name + '.' + tag].append(breaking_change) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py b/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py index 5b89937b2f0..01a191aaa98 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py @@ -47,10 +47,10 @@ def setUp(self): def test_register_and_execute(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_other_breaking_change + from azure.cli.core.breaking_change import register_other_breaking_change warning_message = 'Test Breaking Change in Test Group' - announce_other_breaking_change('test', warning_message) + register_other_breaking_change('test', warning_message) cli = DummyCli(commands_loader_cls=TestCommandsLoader) captured_err = io.StringIO() @@ -68,15 +68,15 @@ def test_register_and_execute(self): with redirect_stdout(captured_output): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', '--help']) - self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn('[BreakingChange]', captured_output.getvalue()) self.assertIn(warning_message, captured_output.getvalue()) def test_command_group_deprecate(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_command_group_deprecate + from azure.cli.core.breaking_change import register_command_group_deprecate - announce_command_group_deprecate('test group', redirect='test group1', target_version='2.70.0') + register_command_group_deprecate('test group', redirect='test group1', target_version='2.70.0') implicit_warning = "This command is implicitly deprecated because command group 'test "\ "group' is deprecated and will be removed in a future release. Use 'test "\ "group1' instead." @@ -109,9 +109,9 @@ def test_command_group_deprecate(self): def test_command_deprecate(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_command_deprecate + from azure.cli.core.breaking_change import register_command_deprecate - announce_command_deprecate('test group cmd', redirect='test group cmd1', target_version='2.70.0') + register_command_deprecate('test group cmd', redirect='test group cmd1', target_version='2.70.0') warning = "This command has been deprecated and will be removed in 2.70.0. Use \'test group cmd1\' instead." cli = DummyCli(commands_loader_cls=TestCommandsLoader) @@ -136,9 +136,9 @@ def test_command_deprecate(self): def test_argument_deprecate(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_argument_deprecate + from azure.cli.core.breaking_change import register_argument_deprecate - announce_argument_deprecate('test group cmd', argument='arg1', redirect='arg2') + register_argument_deprecate('test group cmd', argument='arg1', redirect='arg2') warning = ("Argument 'arg1' has been deprecated and will be removed in next breaking change release(3.0.0). " "Use 'arg2' instead.") cli = DummyCli(commands_loader_cls=TestCommandsLoader) @@ -159,9 +159,9 @@ def test_argument_deprecate(self): def test_option_deprecate(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_argument_deprecate + from azure.cli.core.breaking_change import register_argument_deprecate - announce_argument_deprecate('test group cmd', argument='--arg1', redirect='--arg1-alias') + register_argument_deprecate('test group cmd', argument='--arg1', redirect='--arg1-alias') warning = ("Option '--arg1' has been deprecated and will be removed in next breaking change release(3.0.0). " "Use '--arg1-alias' instead.") cli = DummyCli(commands_loader_cls=TestCommandsLoader) @@ -183,9 +183,9 @@ def test_option_deprecate(self): def test_be_required(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_required_flag_breaking_change + from azure.cli.core.breaking_change import register_required_flag_breaking_change - announce_required_flag_breaking_change('test group cmd', arg='--arg1') + register_required_flag_breaking_change('test group cmd', arg='--arg1') warning = "The argument '--arg1' will become required in next breaking change release(3.0.0)." cli = DummyCli(commands_loader_cls=TestCommandsLoader) @@ -204,15 +204,15 @@ def test_be_required(self): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', 'cmd', '--help']) self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) - self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn('[BreakingChange]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_default_change(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_default_value_breaking_change + from azure.cli.core.breaking_change import register_default_value_breaking_change - announce_default_value_breaking_change('test group cmd', arg='arg1', new_default='Default', + register_default_value_breaking_change('test group cmd', arg='arg1', new_default='Default', current_default='None') warning = ("The default value of 'arg1' will be changed to 'Default' from 'None' " "in next breaking change release(3.0.0).") @@ -233,15 +233,15 @@ def test_default_change(self): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', 'cmd', '--help']) self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) - self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn('[BreakingChange]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_output_change(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_output_breaking_change + from azure.cli.core.breaking_change import register_output_breaking_change - announce_output_breaking_change('test group cmd', description="The output of 'test group cmd' " + register_output_breaking_change('test group cmd', description="The output of 'test group cmd' " "would be changed.") warning = ("The output will be changed in next breaking change release(3.0.0). The output of 'test group cmd' " "would be changed.") @@ -262,15 +262,15 @@ def test_output_change(self): with redirect_stdout(captured_output): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', '--help']) - self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn('[BreakingChange]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_logic_change(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_logic_breaking_change + from azure.cli.core.breaking_change import register_logic_breaking_change - announce_logic_breaking_change('test group cmd', summary="Logic Change Summary") + register_logic_breaking_change('test group cmd', summary="Logic Change Summary") warning = "Logic Change Summary in next breaking change release(3.0.0)." cli = DummyCli(commands_loader_cls=TestCommandsLoader) @@ -289,21 +289,21 @@ def test_logic_change(self): with redirect_stdout(captured_output): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', '--help']) - self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn('[BreakingChange]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_multi_breaking_change(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_logic_breaking_change, announce_argument_deprecate, \ - announce_required_flag_breaking_change + from azure.cli.core.breaking_change import register_logic_breaking_change, register_argument_deprecate, \ + register_required_flag_breaking_change - announce_argument_deprecate('test group cmd', argument='--arg1', redirect='--arg1-alias') + register_argument_deprecate('test group cmd', argument='--arg1', redirect='--arg1-alias') warning1 = ("Option '--arg1' has been deprecated and will be removed in next breaking change release(3.0.0). " - "Use '--arg1-alias' instead.") - announce_required_flag_breaking_change('test group cmd', arg='--arg1') + "Use '--arg1-alias' instead.") + register_required_flag_breaking_change('test group cmd', arg='--arg1') warning2 = "The argument '--arg1' will become required in next breaking change release(3.0.0)." - announce_logic_breaking_change('test group cmd', summary="Logic Change Summary") + register_logic_breaking_change('test group cmd', summary="Logic Change Summary") warning3 = "Logic Change Summary in next breaking change release(3.0.0)." cli = DummyCli(commands_loader_cls=TestCommandsLoader) @@ -321,24 +321,24 @@ def test_multi_breaking_change(self): self.assertIn(warning1, captured_output.getvalue().replace('\n ', ' ')) self.assertIn(warning2, captured_output.getvalue().replace('\n ', ' ')) self.assertIn(warning3, captured_output.getvalue().replace('\n ', ' ')) - self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn('[BreakingChange]', captured_output.getvalue()) self.assertIn('[Deprecated]', captured_output.getvalue()) captured_output = io.StringIO() with redirect_stdout(captured_output): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', '--help']) - self.assertIn('[BrkChange]', captured_output.getvalue()) + self.assertIn('[BreakingChange]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_conditional_breaking_change(self): from contextlib import redirect_stderr, redirect_stdout - from azure.cli.core.breaking_change import announce_conditional_breaking_change, AzCLIOtherChange, \ + from azure.cli.core.breaking_change import register_conditional_breaking_change, AzCLIOtherChange, \ print_conditional_breaking_change warning_message = 'Test Breaking Change in Test Group' - announce_conditional_breaking_change('TestConditional', AzCLIOtherChange('test group cmd', warning_message)) + register_conditional_breaking_change('TestConditional', AzCLIOtherChange('test group cmd', warning_message)) cli = DummyCli(commands_loader_cls=TestCommandsLoader) captured_err = io.StringIO() @@ -351,7 +351,7 @@ def test_conditional_breaking_change(self): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', 'cmd', '--help']) self.assertNotIn(warning_message, captured_err.getvalue().replace('\n ', ' ')) - self.assertNotIn('[BrkChange]', captured_output.getvalue()) + self.assertNotIn('[BreakingChange]', captured_output.getvalue()) cli_ctx = mock.MagicMock() cli_ctx.invocation.commands_loader.command_name = 'test group cmd' From ef6f8c6670b265c7fddc903097575949f0fb8452 Mon Sep 17 00:00:00 2001 From: Qinkai Wu <32201005+ReaNAiveD@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:55:11 +0800 Subject: [PATCH 41/42] Update src/azure-cli-core/azure/cli/core/breaking_change.py Co-authored-by: Xing Zhou --- src/azure-cli-core/azure/cli/core/breaking_change.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/breaking_change.py b/src/azure-cli-core/azure/cli/core/breaking_change.py index 6c520e2f3d8..cb530d9ea1a 100644 --- a/src/azure-cli-core/azure/cli/core/breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/breaking_change.py @@ -14,7 +14,7 @@ logger = get_logger() NEXT_BREAKING_CHANGE_RELEASE = '2.67.0' -DEFAULT_BREAKING_CHANGE_TAG = '[BreakingChange]' +DEFAULT_BREAKING_CHANGE_TAG = '[Breaking Change]' def _get_action_class(cli_ctx, action): From 85cccba71086320902b4ec9644bca1ed8e1c0356 Mon Sep 17 00:00:00 2001 From: qinkaiwu Date: Tue, 20 Aug 2024 15:07:35 +0800 Subject: [PATCH 42/42] Fix unit test failure caused by tag name change --- .../azure/cli/core/tests/test_breaking_change.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py b/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py index 01a191aaa98..223e12eeb11 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_breaking_change.py @@ -68,7 +68,7 @@ def test_register_and_execute(self): with redirect_stdout(captured_output): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', '--help']) - self.assertIn('[BreakingChange]', captured_output.getvalue()) + self.assertIn('[Breaking Change]', captured_output.getvalue()) self.assertIn(warning_message, captured_output.getvalue()) def test_command_group_deprecate(self): @@ -204,7 +204,7 @@ def test_be_required(self): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', 'cmd', '--help']) self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) - self.assertIn('[BreakingChange]', captured_output.getvalue()) + self.assertIn('[Breaking Change]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_default_change(self): @@ -233,7 +233,7 @@ def test_default_change(self): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', 'cmd', '--help']) self.assertIn(warning, captured_output.getvalue().replace('\n ', ' ')) - self.assertIn('[BreakingChange]', captured_output.getvalue()) + self.assertIn('[Breaking Change]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_output_change(self): @@ -262,7 +262,7 @@ def test_output_change(self): with redirect_stdout(captured_output): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', '--help']) - self.assertIn('[BreakingChange]', captured_output.getvalue()) + self.assertIn('[Breaking Change]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_logic_change(self): @@ -289,7 +289,7 @@ def test_logic_change(self): with redirect_stdout(captured_output): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', '--help']) - self.assertIn('[BreakingChange]', captured_output.getvalue()) + self.assertIn('[Breaking Change]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_multi_breaking_change(self): @@ -321,14 +321,14 @@ def test_multi_breaking_change(self): self.assertIn(warning1, captured_output.getvalue().replace('\n ', ' ')) self.assertIn(warning2, captured_output.getvalue().replace('\n ', ' ')) self.assertIn(warning3, captured_output.getvalue().replace('\n ', ' ')) - self.assertIn('[BreakingChange]', captured_output.getvalue()) + self.assertIn('[Breaking Change]', captured_output.getvalue()) self.assertIn('[Deprecated]', captured_output.getvalue()) captured_output = io.StringIO() with redirect_stdout(captured_output): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', '--help']) - self.assertIn('[BreakingChange]', captured_output.getvalue()) + self.assertIn('[Breaking Change]', captured_output.getvalue()) @mock.patch('azure.cli.core.breaking_change.NEXT_BREAKING_CHANGE_RELEASE', '3.0.0') def test_conditional_breaking_change(self): @@ -351,7 +351,7 @@ def test_conditional_breaking_change(self): with self.assertRaises(SystemExit): cli.invoke(['test', 'group', 'cmd', '--help']) self.assertNotIn(warning_message, captured_err.getvalue().replace('\n ', ' ')) - self.assertNotIn('[BreakingChange]', captured_output.getvalue()) + self.assertNotIn('[Breaking Change]', captured_output.getvalue()) cli_ctx = mock.MagicMock() cli_ctx.invocation.commands_loader.command_name = 'test group cmd'