diff --git a/tools/ci/test_build_system_cmake.sh b/tools/ci/test_build_system_cmake.sh index 47e1ae23e..0872d3320 100755 --- a/tools/ci/test_build_system_cmake.sh +++ b/tools/ci/test_build_system_cmake.sh @@ -527,6 +527,18 @@ endmenu\n" >> ${IDF_PATH}/Kconfig; rm -rf CMakeLists.txt mv CMakeLists.txt.bak CMakeLists.txt rm -rf CMakeLists.txt.bak + + print_status "Print all required argument deprecation warnings" + idf.py -C${IDF_PATH}/tools/test_idf_py --test-0=a --test-1=b --test-2=c --test-3=d test-0 --test-sub-0=sa --test-sub-1=sb ta test-1 > out.txt + ! grep -e '"test-0" is deprecated' -e '"test_0" is deprecated' out.txt || failure "Deprecation warnings are displayed for non-deprecated option/command" + grep -e 'Warning: Option "test_sub_1" is deprecated and will be removed in future versions.' \ + -e 'Warning: Command "test-1" is deprecated and will be removed in future versions. Please use alternative command.' \ + -e 'Warning: Option "test_1" is deprecated and will be removed in future versions.' \ + -e 'Warning: Option "test_2" is deprecated and will be removed in future versions. Please update your parameters.' \ + -e 'Warning: Option "test_3" is deprecated and will be removed in future versions.' \ + out.txt \ + || failure "Deprecation warnings are not displayed" + rm out.txt print_status "All tests completed" if [ -n "${FAILURES}" ]; then diff --git a/tools/idf.py b/tools/idf.py index 6d4e5fe43..e33152e63 100755 --- a/tools/idf.py +++ b/tools/idf.py @@ -545,6 +545,46 @@ def init_cli(): # Click is imported here to run it after check_environment() import click + class DeprecationMessage(object): + """Construct deprecation notice for help messages""" + + def __init__(self, deprecated=False): + self.deprecated = deprecated + self.since = None + self.removed = None + self.custom_message = "" + + if isinstance(deprecated, dict): + self.custom_message = deprecated.get("message", "") + self.since = deprecated.get("since", None) + self.removed = deprecated.get("removed", None) + elif isinstance(deprecated, str): + self.custom_message = deprecated + + def full_message(self, type="Option"): + return "%s is deprecated %sand will be removed in%s.%s" % ( + type, + "since %s " % self.since if self.since else "", + " %s" % self.removed if self.removed else " future versions", + " %s" % self.custom_message if self.custom_message else "" + ) + + def help(self, text, type="Option", separator=" "): + text = text or "" + return self.full_message(type) + separator + text if self.deprecated else text + + def short_help(self, text): + text = text or "" + return ("Deprecated! " + text) if self.deprecated else text + + def print_deprecation_warning(ctx): + """Prints deprectation warnings for arguments in given context""" + for option in ctx.command.params: + default = () if option.multiple else option.default + if isinstance(option, Option) and option.deprecated and ctx.params[option.name] != default: + print("Warning: %s" % DeprecationMessage(option.deprecated). + full_message('Option "%s"' % option.name)) + class Task(object): def __init__( self, callback, name, aliases, dependencies, order_dependencies, action_args @@ -567,6 +607,7 @@ def init_cli(): self, name=None, aliases=None, + deprecated=False, dependencies=None, order_dependencies=None, **kwargs @@ -574,6 +615,7 @@ def init_cli(): super(Action, self).__init__(name, **kwargs) self.name = self.name or self.callback.__name__ + self.deprecated = deprecated if aliases is None: aliases = [] @@ -592,6 +634,11 @@ def init_cli(): # Show first line of help if short help is missing self.short_help = self.short_help or self.help.split("\n")[0] + if deprecated: + deprecation = DeprecationMessage(deprecated) + self.short_help = deprecation.short_help(self.short_help) + self.help = deprecation.help(self.help, type="Command", separator="\n") + # Add aliases to help string if aliases: aliases_help = "Aliases: %s." % ", ".join(aliases) @@ -614,8 +661,21 @@ def init_cli(): self.callback = wrapped_callback + def invoke(self, ctx): + if self.deprecated: + print("Warning: %s" % DeprecationMessage(self.deprecated).full_message('Command "%s"' % self.name)) + self.deprecated = False # disable Click's built-in deprecation handling + + # Print warnings for options + print_deprecation_warning(ctx) + return super(Action, self).invoke(ctx) + class Argument(click.Argument): - """Positional argument""" + """ + Positional argument + + names - alias of 'param_decls' + """ def __init__(self, **kwargs): names = kwargs.pop("names") @@ -656,12 +716,28 @@ def init_cli(): class Option(click.Option): """Option that knows whether it should be global""" - def __init__(self, scope=None, **kwargs): + def __init__(self, scope=None, deprecated=False, **kwargs): + """ + Keyword arguments additional to Click's Option class: + + names - alias of 'param_decls' + deprecated - marks option as deprecated. May be boolean, string (with custom deprecation message) + or dict with optional keys: + since: version of deprecation + removed: version when option will be removed + custom_message: Additional text to deprecation warning + """ + kwargs["param_decls"] = kwargs.pop("names") super(Option, self).__init__(**kwargs) + self.deprecated = deprecated self.scope = Scope(scope) + if deprecated: + deprecation = DeprecationMessage(deprecated) + self.help = deprecation.help(self.help) + if self.scope.is_global: self.help += " This option can be used at most once either globally, or for one subcommand." @@ -823,6 +899,7 @@ def init_cli(): for task in tasks: for key in list(task.action_args): option = next((o for o in ctx.command.params if o.name == key), None) + if option and (option.scope.is_global or option.scope.is_shared): local_value = task.action_args.pop(key) global_value = global_args[key] @@ -836,6 +913,9 @@ def init_cli(): if local_value != default: global_args[key] = local_value + # Show warnings about global arguments + print_deprecation_warning(ctx) + # Validate global arguments for action_callback in ctx.command.global_action_callbacks: action_callback(ctx, global_args, tasks) diff --git a/tools/test_idf_py/idf_ext.py b/tools/test_idf_py/idf_ext.py new file mode 100644 index 000000000..b8fc8c3f2 --- /dev/null +++ b/tools/test_idf_py/idf_ext.py @@ -0,0 +1,70 @@ +def action_extensions(base_actions, project_path=None): + def echo(name, *args, **kwargs): + print(name, args, kwargs) + + # Add global options + extensions = { + "global_options": [ + { + "names": ["--test-0"], + "help": "Non-deprecated option.", + "deprecated": False + }, + { + "names": ["--test-1"], + "help": "Deprecated option 1.", + "deprecated": True + }, + { + "names": ["--test-2"], + "help": "Deprecated option 2.", + "deprecated": "Please update your parameters." + }, + { + "names": ["--test-3"], + "help": "Deprecated option 3.", + "deprecated": { + "custom_message": "Please update your parameters." + } + }, + { + "names": ["--test-4"], + "help": "Deprecated option 3.", + "deprecated": { + "since": "v4.0", + "removed": "v5.0" + } + }, + ], + "actions": { + "test-0": { + "callback": + echo, + "help": + "Non-deprecated command 0", + "options": [ + { + "names": ["--test-sub-0"], + "help": "Non-deprecated subcommand option 0", + "default": None, + }, + { + "names": ["--test-sub-1"], + "help": "Deprecated subcommand option 1", + "default": None, + "deprecated": True + }, + ], + "arguments": [{ + "names": ["test-arg-0"], + }], + }, + "test-1": { + "callback": echo, + "help": "Deprecated command 1", + "deprecated": "Please use alternative command." + }, + }, + } + + return extensions