From 82f804cd1de30d0c73adbefdd423e4c4f62f840c Mon Sep 17 00:00:00 2001 From: Marek Fiala Date: Wed, 22 Apr 2026 15:45:46 +0200 Subject: [PATCH] feat(tools): Replaced click with rich_click --- tools/bt/ble_log_console/console.py | 2 +- tools/idf.py | 40 +++++++++------- tools/idf_py_actions/core_ext.py | 6 +-- tools/idf_py_actions/create_ext.py | 4 +- tools/idf_py_actions/debug_ext.py | 4 +- tools/idf_py_actions/dfu_ext.py | 25 +++++----- tools/idf_py_actions/diag_ext.py | 45 ++++++++---------- tools/idf_py_actions/errors.py | 4 +- tools/idf_py_actions/mcp_ext.py | 2 +- tools/idf_py_actions/qemu_ext.py | 2 +- tools/idf_py_actions/serial_ext.py | 58 ++++++++++++------------ tools/idf_py_actions/tools.py | 2 +- tools/idf_py_actions/uf2_ext.py | 12 +++-- tools/requirements/requirements.core.txt | 1 + tools/test_idf_py/test_idf_py.py | 22 +++++++++ 15 files changed, 127 insertions(+), 102 deletions(-) diff --git a/tools/bt/ble_log_console/console.py b/tools/bt/ble_log_console/console.py index 071761c0320..d2a449d0d03 100644 --- a/tools/bt/ble_log_console/console.py +++ b/tools/bt/ble_log_console/console.py @@ -12,7 +12,7 @@ Usage: from datetime import datetime from pathlib import Path -import click +import rich_click as click from src.app import BLELogApp from src.backend.models import format_bytes from src.backend.uart_transport import validate_uart_port diff --git a/tools/idf.py b/tools/idf.py index 7e0cb033d48..c7aff30db36 100755 --- a/tools/idf.py +++ b/tools/idf.py @@ -157,9 +157,13 @@ def _safe_relpath(path: str, start: str | None = None) -> str: def init_cli(verbose_output: list | None = None) -> Any: - # Click is imported here to run it after check_environment() - import click + # rich-click is imported here to run it after check_environment() + import rich_click as click + from click.core import ParameterSource from click.shell_completion import CompletionItem + from rich_click import Context + from rich_click import RichHelpConfiguration + from rich_click.rich_click import MAX_WIDTH class Deprecation: """Construct deprecation notice for help messages""" @@ -211,7 +215,7 @@ def init_cli(verbose_output: list | None = None) -> Any: text = text or '' return ('Deprecated! ' + text) if self.deprecated else text - def check_deprecation(ctx: click.core.Context) -> None: + def check_deprecation(ctx: Context) -> None: """Prints deprecation warnings for arguments in given context""" for option in ctx.command.params: # Skip non-Option parameters @@ -228,7 +232,7 @@ def init_cli(verbose_output: list | None = None) -> Any: if hasattr(ctx, 'get_parameter_source'): source = ctx.get_parameter_source(option.name) # Skip if option was not explicitly provided by user (only warn when actually used) - if source not in (click.core.ParameterSource.COMMANDLINE, click.core.ParameterSource.ENVIRONMENT): + if source not in (ParameterSource.COMMANDLINE, ParameterSource.ENVIRONMENT): continue else: # Fallback: check if value differs from default @@ -262,15 +266,13 @@ def init_cli(verbose_output: list | None = None) -> Any: self.action_args = action_args self.aliases = aliases - def __call__( - self, context: click.core.Context, global_args: PropertyDict, action_args: dict | None = None - ) -> None: + def __call__(self, context: Context, global_args: PropertyDict, action_args: dict | None = None) -> None: if action_args is None: action_args = self.action_args self.callback(self.name, context, global_args, **action_args) - class Action(click.Command): + class Action(click.RichCommand): callback: Callable def __init__( @@ -331,7 +333,7 @@ def init_cli(verbose_output: list | None = None) -> Any: self.callback: Callable = wrapped_callback - def invoke(self, ctx: click.core.Context) -> click.core.Context: + def invoke(self, ctx: Context) -> Context: if self.deprecated: deprecation = Deprecation(self.deprecated) message = deprecation.full_message(f'Command "{self.name}"') @@ -347,7 +349,7 @@ def init_cli(verbose_output: list | None = None) -> Any: check_deprecation(ctx) return super().invoke(ctx) - class Argument(click.Argument): + class Argument(click.RichArgument): """ Positional argument @@ -390,7 +392,7 @@ def init_cli(verbose_output: list | None = None) -> Any: def __str__(self) -> str: return self._scope - class Option(click.Option): + class Option(click.RichOption): """Option that knows whether it should be global""" def __init__( @@ -428,14 +430,14 @@ def init_cli(verbose_output: list | None = None) -> Any: if self.scope.is_global: self.help += ' This option can be used at most once either globally, or for one subcommand.' - def get_help_record(self, ctx: click.core.Context) -> Any: + def get_help_record(self, ctx: Context) -> Any: # Backport "hidden" parameter to click 5.0 if self.hidden: return None return super().get_help_record(ctx) - class CLI(click.Group): + class CLI(click.RichGroup): """Action list contains all actions with options available for CLI""" def __init__( @@ -449,7 +451,10 @@ def init_cli(verbose_output: list | None = None) -> Any: invoke_without_command=True, result_callback=self.execute_tasks, no_args_is_help=True, - context_settings={'max_content_width': 140}, + context_settings={ + 'help_option_names': ['-h', '--help'], + 'rich_help_config': RichHelpConfiguration(max_width=MAX_WIDTH), + }, help=cli_help, ) self._actions = {} @@ -489,6 +494,7 @@ def init_cli(verbose_output: list | None = None) -> Any: options = [] self._actions[name] = Action(name=name, **action) + self.commands[name] = self._actions[name] for alias in [name] + action.get('aliases', []): self.commands_with_aliases[alias] = name @@ -514,10 +520,10 @@ def init_cli(verbose_output: list | None = None) -> Any: self._actions[name].params.append(option) - def list_commands(self, ctx: click.core.Context) -> list: + def list_commands(self, ctx: Context) -> list: return sorted(filter(lambda name: not self._actions[name].hidden, self._actions)) - def get_command(self, ctx: click.core.Context, name: str) -> Action | None: + def get_command(self, ctx: Context, name: str) -> Action | None: if name in self.commands_with_aliases: return self._actions.get(self.commands_with_aliases.get(name)) @@ -528,7 +534,7 @@ def init_cli(verbose_output: list | None = None) -> Any: return Action(name=name, callback=callback.unwrapped_callback) return None - def shell_complete(self, ctx: click.core.Context, incomplete: str) -> list[CompletionItem]: + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: # Enable @-argument completion in bash only if @ is not present in # COMP_WORDBREAKS. When @ is included, the @-argument is not considered # part of the completion word, causing @-argument completion to function diff --git a/tools/idf_py_actions/core_ext.py b/tools/idf_py_actions/core_ext.py index b1d3d359bc2..9f51c334e9e 100644 --- a/tools/idf_py_actions/core_ext.py +++ b/tools/idf_py_actions/core_ext.py @@ -15,8 +15,8 @@ from urllib.request import Request from urllib.request import urlopen from webbrowser import open_new_tab -import click -from click.core import Context +import rich_click as click +from rich_click import Context from idf_py_actions.constants import GENERATORS from idf_py_actions.constants import PREVIEW_TARGETS @@ -167,7 +167,7 @@ def action_extensions(base_actions: dict, project_path: str) -> Any: os.environ.pop('ESP_IDF_KCONFIG_MIN_LABELS', None) build_target(target_name, ctx, args) - def refresh_config(action: str, ctx: click.core.Context, args: PropertyDict, policy: str) -> None: + def refresh_config(action: str, ctx: Context, args: PropertyDict, policy: str) -> None: ensure_build_directory(args, ctx.info_name) run_target('refresh-config', args=args, env={'KCONFIG_DEFAULTS_POLICY': policy}, interactive=True) diff --git a/tools/idf_py_actions/create_ext.py b/tools/idf_py_actions/create_ext.py index 2feab908f07..5cafafb5914 100644 --- a/tools/idf_py_actions/create_ext.py +++ b/tools/idf_py_actions/create_ext.py @@ -8,7 +8,7 @@ from collections.abc import Callable from shutil import copyfile from shutil import copytree -import click +from rich_click import Context from idf_py_actions.tools import PropertyDict @@ -106,7 +106,7 @@ def create_component(target_path: str, name: str) -> None: def action_extensions(base_actions: dict, project_path: str) -> dict: - def create_new(action: str, ctx: click.core.Context, global_args: PropertyDict, **action_args: str) -> dict: + def create_new(action: str, ctx: Context, global_args: PropertyDict, **action_args: str) -> dict: target_path = action_args.get('path') or os.path.join(project_path, action_args['name']) is_empty_and_create(target_path, action) diff --git a/tools/idf_py_actions/debug_ext.py b/tools/idf_py_actions/debug_ext.py index ca4aeacf7b6..5efb28dc9fe 100644 --- a/tools/idf_py_actions/debug_ext.py +++ b/tools/idf_py_actions/debug_ext.py @@ -11,9 +11,9 @@ import time from threading import Thread from typing import Any -from click import INT -from click.core import Context from esp_coredump import CoreDump +from rich_click import INT +from rich_click import Context from idf_py_actions.errors import FatalError from idf_py_actions.serial_ext import BAUD_RATE diff --git a/tools/idf_py_actions/dfu_ext.py b/tools/idf_py_actions/dfu_ext.py index 4c72f6db829..b86bb309964 100644 --- a/tools/idf_py_actions/dfu_ext.py +++ b/tools/idf_py_actions/dfu_ext.py @@ -1,17 +1,16 @@ -# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 -from typing import Dict -from click.core import Context +from rich_click import Context + from idf_py_actions.errors import FatalError +from idf_py_actions.tools import PropertyDict from idf_py_actions.tools import ensure_build_directory from idf_py_actions.tools import is_target_supported -from idf_py_actions.tools import PropertyDict from idf_py_actions.tools import run_target -def action_extensions(base_actions: Dict, project_path: str) -> Dict: - +def action_extensions(base_actions: dict, project_path: str) -> dict: SUPPORTED_TARGETS = ['esp32s2', 'esp32s3', 'esp32p4'] def dfu_target(target_name: str, ctx: Context, args: PropertyDict, part_size: str) -> None: @@ -29,8 +28,10 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: run_target(target_name, args, {'ESP_DFU_PATH': path}) except FatalError: # Cannot capture the error from dfu-util here so the best advise is: - print('Please have a look at the "Device Firmware Upgrade through USB" chapter in API Guides of the ' - 'ESP-IDF documentation for solving common dfu-util issues.') + print( + 'Please have a look at the "Device Firmware Upgrade through USB" chapter in API Guides of the ' + 'ESP-IDF documentation for solving common dfu-util issues.' + ) raise dfu_actions = { @@ -43,8 +44,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: { 'names': ['--part-size'], 'help': 'Large files are split up into smaller partitions in order to avoid timeout during ' - 'erasing flash. This option allows to overwrite the default partition size of ' - 'mkdfu.py.' + 'erasing flash. This option allows to overwrite the default partition size of ' + 'mkdfu.py.', } ], }, @@ -62,8 +63,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: 'names': ['--path'], 'default': '', 'help': 'Specify path to DFU device. The default empty path works if there is just one ' - 'ESP device with the same product identifier. See the device list for paths ' - 'of available devices.' + 'ESP device with the same product identifier. See the device list for paths ' + 'of available devices.', } ], }, diff --git a/tools/idf_py_actions/diag_ext.py b/tools/idf_py_actions/diag_ext.py index 8950d04fef7..2a8961491ca 100644 --- a/tools/idf_py_actions/diag_ext.py +++ b/tools/idf_py_actions/diag_ext.py @@ -1,13 +1,10 @@ -# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import sys import uuid from typing import Any -from typing import Dict -from typing import Optional -from typing import Tuple -import click +from rich_click import Context from idf_py_actions.tools import PropertyDict from idf_py_actions.tools import RunTool @@ -16,20 +13,20 @@ from idf_py_actions.tools import yellow_print def diag( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, debug: bool, log_prefix: bool, force: bool, no_color: bool, - zip_directory: Optional[str], + zip_directory: str | None, list_recipes: bool, check_recipes: bool, - cmdl_recipes: Tuple, - cmdl_tags: Tuple, - purge_file: Optional[str], + cmdl_recipes: tuple, + cmdl_tags: tuple, + purge_file: str | None, append: bool, - output: Optional[str], + output: str | None, ) -> None: diag_args: list = [sys.executable, '-m', 'esp_idf_diag'] @@ -106,12 +103,10 @@ def diag( diag_args += ['--port', args.port] else: yellow_print( - ( - 'The target serial port is not specified, so ' - 'autodetection will be used. To set it manually, use ' - 'the "--port" option. Example: "idf.py --port ' - '/dev/ttyUSB0 diag".' - ) + 'The target serial port is not specified, so ' + 'autodetection will be used. To set it manually, use ' + 'the "--port" option. Example: "idf.py --port ' + '/dev/ttyUSB0 diag".' ) try: @@ -121,18 +116,16 @@ def diag( if command == 'create': yellow_print( - ( - f'Please make sure to thoroughly check it for any sensitive ' - f'information before sharing and remove files you do not want ' - f'to share. Kindly include any additional files you find ' - f'relevant that were not automatically added. Please archive ' - f'the contents of the final report directory using the command:\n' - f'"idf.py diag --zip {output}".' - ) + f'Please make sure to thoroughly check it for any sensitive ' + f'information before sharing and remove files you do not want ' + f'to share. Kindly include any additional files you find ' + f'relevant that were not automatically added. Please archive ' + f'the contents of the final report directory using the command:\n' + f'"idf.py diag --zip {output}".' ) -def action_extensions(base_actions: Dict, project_path: str) -> Any: +def action_extensions(base_actions: dict, project_path: str) -> Any: return { 'actions': { 'diag': { diff --git a/tools/idf_py_actions/errors.py b/tools/idf_py_actions/errors.py index 8648952c29a..a4f5f69158a 100644 --- a/tools/idf_py_actions/errors.py +++ b/tools/idf_py_actions/errors.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 -from click.core import Context +from rich_click import Context class FatalError(RuntimeError): @@ -8,7 +8,7 @@ class FatalError(RuntimeError): Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build process. """ - def __init__(self, message: str, ctx: Context=None): + def __init__(self, message: str, ctx: Context = None): super(RuntimeError, self).__init__(message) # if context is defined, check for the cleanup tasks if ctx is not None and 'cleanup' in ctx.meta: diff --git a/tools/idf_py_actions/mcp_ext.py b/tools/idf_py_actions/mcp_ext.py index 51174e45550..72bd5c3c1f6 100644 --- a/tools/idf_py_actions/mcp_ext.py +++ b/tools/idf_py_actions/mcp_ext.py @@ -8,7 +8,7 @@ import sys from pathlib import Path from typing import Any -from click.core import Context +from rich_click import Context from idf_py_actions.errors import FatalError from idf_py_actions.tools import PropertyDict diff --git a/tools/idf_py_actions/qemu_ext.py b/tools/idf_py_actions/qemu_ext.py index 680d361cd59..6aa8a7a5fc8 100644 --- a/tools/idf_py_actions/qemu_ext.py +++ b/tools/idf_py_actions/qemu_ext.py @@ -14,7 +14,7 @@ import time from dataclasses import dataclass from typing import Any -from click.core import Context +from rich_click import Context try: from idf_py_actions.tools import PropertyDict diff --git a/tools/idf_py_actions/serial_ext.py b/tools/idf_py_actions/serial_ext.py index 02b962520a6..4685708b12c 100644 --- a/tools/idf_py_actions/serial_ext.py +++ b/tools/idf_py_actions/serial_ext.py @@ -8,7 +8,9 @@ import sys from pathlib import Path from typing import Any -import click +import rich_click as click +from click.core import ParameterSource +from rich_click import Context from idf_py_actions.errors import FatalError from idf_py_actions.global_options import global_options @@ -50,7 +52,7 @@ def yellow_print(message: str, newline: str | None = '\n') -> None: def action_extensions(base_actions: dict, project_path: str) -> dict: - def _get_project_desc(ctx: click.core.Context, args: PropertyDict) -> Any: + def _get_project_desc(ctx: Context, args: PropertyDict) -> Any: desc_path = os.path.join(args.build_dir, 'project_description.json') if not os.path.exists(desc_path): ensure_build_directory(args, ctx.info_name) @@ -83,7 +85,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: result += ['--no-stub'] return result - def _get_commandline_options(ctx: click.core.Context) -> list: + def _get_commandline_options(ctx: Context) -> list: """Return all the command line options up to first action""" # This approach ignores argument parsing done Click result = [] @@ -98,7 +100,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: def monitor( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, print_filter: str, monitor_baud: str, @@ -136,7 +138,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: # Use the global baud rate if it has been changed by the command line. # Use project_desc['monitor_baud'] as the last option. - global_baud_defined = ctx._parameter_source['baud'] == click.core.ParameterSource.COMMANDLINE + global_baud_defined = ctx._parameter_source['baud'] == ParameterSource.COMMANDLINE baud = args.baud if global_baud_defined else project_desc['monitor_baud'] monitor_args += ['-b', baud] @@ -205,7 +207,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: def flash( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, flash_all: bool, trust_flash_content: bool, @@ -244,13 +246,13 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: env['IDF_TRUST_FLASH_CONTENT'] = '1' run_target(action, args, env, force_progression=True, interactive=True) - def erase_flash(action: str, ctx: click.core.Context, args: PropertyDict) -> None: + def erase_flash(action: str, ctx: Context, args: PropertyDict) -> None: ensure_build_directory(args, ctx.info_name) esptool_args = _get_esptool_args(args) esptool_args += ['erase-flash'] RunTool('esptool', esptool_args, args.build_dir, hints=not args.no_hints, interactive=True)() - def global_callback(ctx: click.core.Context, global_args: dict, tasks: PropertyDict) -> None: + def global_callback(ctx: Context, global_args: dict, tasks: PropertyDict) -> None: encryption = any([task.name in ('encrypted-flash', 'encrypted-app-flash') for task in tasks]) if encryption: for task in tasks: @@ -258,7 +260,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: task.action_args['encrypted'] = True break - def ota_targets(target_name: str, ctx: click.core.Context, args: PropertyDict) -> None: + def ota_targets(target_name: str, ctx: Context, args: PropertyDict) -> None: """ Execute the target build system to build target 'target_name'. Additionally set global variables for baud and port. @@ -271,7 +273,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: def merge_bin( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, output: str, format: str, # noqa: A002 @@ -319,7 +321,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: def secure_decrypt_flash_data( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, aes_xts: bool, keyfile: str, @@ -345,7 +347,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: RunTool('espsecure', decrypt_flash_data_args, args.build_dir)() def secure_digest_secure_bootloader( - action: str, ctx: click.core.Context, args: PropertyDict, keyfile: str, output: str, iv: str, **extra_args: str + action: str, ctx: Context, args: PropertyDict, keyfile: str, output: str, iv: str, **extra_args: str ) -> None: ensure_build_directory(args, ctx.info_name) digest_secure_bootloader_args = [PYTHON, '-m', 'espsecure', 'digest-secure-bootloader'] @@ -361,7 +363,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: def secure_encrypt_flash_data( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, aes_xts: bool, keyfile: str, @@ -387,7 +389,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: RunTool('espsecure', encrypt_flash_data_args, args.build_dir)() def secure_generate_flash_encryption_key( - action: str, ctx: click.core.Context, args: PropertyDict, keylen: str, **extra_args: str + action: str, ctx: Context, args: PropertyDict, keylen: str, **extra_args: str ) -> None: ensure_build_directory(args, ctx.info_name) generate_flash_encryption_key_args = [PYTHON, '-m', 'espsecure', 'generate-flash-encryption-key'] @@ -398,7 +400,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: RunTool('espsecure', generate_flash_encryption_key_args, args.project_dir)() def secure_generate_signing_key( - action: str, ctx: click.core.Context, args: PropertyDict, version: str, scheme: str, **extra_args: str + action: str, ctx: Context, args: PropertyDict, version: str, scheme: str, **extra_args: str ) -> None: ensure_build_directory(args, ctx.info_name) generate_signing_key_args = [PYTHON, '-m', 'espsecure', 'generate-signing-key'] @@ -419,7 +421,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: RunTool('espsecure', generate_signing_key_args, args.project_dir)() def secure_generate_key_digest( - action: str, ctx: click.core.Context, args: PropertyDict, keyfile: str, output: str, **extra_args: str + action: str, ctx: Context, args: PropertyDict, keyfile: str, output: str, **extra_args: str ) -> None: ensure_build_directory(args, ctx.info_name) generate_key_digest_args = [PYTHON, '-m', 'espsecure', 'digest-sbv2-public-key'] @@ -431,7 +433,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: def secure_sign_data( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, version: str, keyfile: str, @@ -460,7 +462,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: RunTool('espsecure', sign_data_args, args.build_dir)() def secure_verify_signature( - action: str, ctx: click.core.Context, args: PropertyDict, version: str, keyfile: str, **extra_args: str + action: str, ctx: Context, args: PropertyDict, version: str, keyfile: str, **extra_args: str ) -> None: ensure_build_directory(args, ctx.info_name) verify_signature_args = [PYTHON, '-m', 'espsecure', 'verify-signature'] @@ -474,7 +476,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: def secure_generate_nvs_partition_key( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, encryption_scheme: str, keyfile: str, @@ -492,7 +494,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: RunTool('espsecure', generate_nvs_partition_key_args, args.project_dir)() def secure_encrypt_nvs_partition( - action: str, ctx: click.core.Context, args: PropertyDict, keyfile: str, **extra_args: str + action: str, ctx: Context, args: PropertyDict, keyfile: str, **extra_args: str ) -> None: ensure_build_directory(args, ctx.info_name) encrypt_nvs_partition_args = [PYTHON, '-m', 'esp_idf_nvs_partition_gen', 'encrypt'] @@ -505,7 +507,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: encrypt_nvs_partition_args += [extra_args['partition_size']] RunTool('espsecure', encrypt_nvs_partition_args, args.project_dir)() - def _parse_efuse_args(ctx: click.core.Context, args: PropertyDict, extra_args: dict) -> list: + def _parse_efuse_args(ctx: Context, args: PropertyDict, extra_args: dict) -> list: efuse_args = [] if args.port: efuse_args += ['-p', args.port] @@ -522,7 +524,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: efuse_args += ['--do-not-confirm'] return efuse_args - def efuse_burn(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None: + def efuse_burn(action: str, ctx: Context, args: PropertyDict, **extra_args: dict) -> None: ensure_build_directory(args, ctx.info_name) burn_efuse_args = [PYTHON, '-m', 'espefuse'] burn_efuse_args += _parse_efuse_args(ctx, args, extra_args) @@ -531,7 +533,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: burn_efuse_args += list(extra_args['efuse_positional_args']) RunTool('espefuse', burn_efuse_args, args.build_dir)() - def efuse_burn_key(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: str) -> None: + def efuse_burn_key(action: str, ctx: Context, args: PropertyDict, **extra_args: str) -> None: ensure_build_directory(args, ctx.info_name) burn_key_args = [PYTHON, '-m', 'espefuse'] burn_key_args += _parse_efuse_args(ctx, args, extra_args) @@ -546,9 +548,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: burn_key_args += extra_args['efuse_positional_args'] RunTool('espefuse', burn_key_args, args.project_dir, build_dir=args.build_dir)() - def efuse_dump( - action: str, ctx: click.core.Context, args: PropertyDict, file_name: str, **extra_args: dict - ) -> None: + def efuse_dump(action: str, ctx: Context, args: PropertyDict, file_name: str, **extra_args: dict) -> None: ensure_build_directory(args, ctx.info_name) dump_args = [PYTHON, '-m', 'espefuse'] dump_args += _parse_efuse_args(ctx, args, extra_args) @@ -557,7 +557,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: dump_args += ['--file-name', file_name] RunTool('espefuse', dump_args, args.build_dir)() - def efuse_read_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None: + def efuse_read_protect(action: str, ctx: Context, args: PropertyDict, **extra_args: dict) -> None: ensure_build_directory(args, ctx.info_name) read_protect_args = [PYTHON, '-m', 'espefuse'] read_protect_args += _parse_efuse_args(ctx, args, extra_args) @@ -568,7 +568,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: def efuse_summary( action: str, - ctx: click.core.Context, + ctx: Context, args: PropertyDict, format: str, # noqa: A002 **extra_args: dict, @@ -583,7 +583,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: summary_args += [str(extra_args['efuse_name'])] RunTool('espefuse', summary_args, args.build_dir)() - def efuse_write_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None: + def efuse_write_protect(action: str, ctx: Context, args: PropertyDict, **extra_args: dict) -> None: ensure_build_directory(args, ctx.info_name) write_protect_args = [PYTHON, '-m', 'espefuse'] write_protect_args += _parse_efuse_args(ctx, args, extra_args) diff --git a/tools/idf_py_actions/tools.py b/tools/idf_py_actions/tools.py index 8ab06824cf1..dd646001a72 100644 --- a/tools/idf_py_actions/tools.py +++ b/tools/idf_py_actions/tools.py @@ -16,7 +16,7 @@ from typing import Any from typing import TextIO from typing import cast -import click +import rich_click as click import yaml from esp_idf_monitor import get_ansi_converter diff --git a/tools/idf_py_actions/uf2_ext.py b/tools/idf_py_actions/uf2_ext.py index a6b2f467003..416735718f6 100644 --- a/tools/idf_py_actions/uf2_ext.py +++ b/tools/idf_py_actions/uf2_ext.py @@ -1,12 +1,14 @@ -# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, List -from click.core import Context -from idf_py_actions.tools import PropertyDict, ensure_build_directory, run_target +from rich_click import Context + +from idf_py_actions.tools import PropertyDict +from idf_py_actions.tools import ensure_build_directory +from idf_py_actions.tools import run_target -def action_extensions(base_actions: Dict, project_path: List) -> Dict: +def action_extensions(base_actions: dict, project_path: list) -> dict: def uf2_target(target_name: str, ctx: Context, args: PropertyDict, md5_disable: bool) -> None: ensure_build_directory(args, ctx.info_name) extra = list() diff --git a/tools/requirements/requirements.core.txt b/tools/requirements/requirements.core.txt index aef6c3e9a7e..854a0a3a206 100644 --- a/tools/requirements/requirements.core.txt +++ b/tools/requirements/requirements.core.txt @@ -7,6 +7,7 @@ setuptools packaging click +rich-click pyserial cryptography pyparsing diff --git a/tools/test_idf_py/test_idf_py.py b/tools/test_idf_py/test_idf_py.py index c1a1b064417..bd673c1a6d2 100755 --- a/tools/test_idf_py/test_idf_py.py +++ b/tools/test_idf_py/test_idf_py.py @@ -33,6 +33,28 @@ py_actions_path = os.path.normpath(os.path.join(current_dir, '..', 'idf_py_actio link_path = os.path.join(py_actions_path, 'test_ext') +# As idf.py uses rich-click, unite modification variables to ensure constant results on various CI terminals +_idf_py_test_env_saved: dict[str, str | None] = {} + + +def setUpModule() -> None: + for key in ('COLUMNS', 'LINES', 'NO_COLOR', 'FORCE_COLOR', 'PY_COLORS', 'TERM'): + _idf_py_test_env_saved[key] = os.environ.get(key) + os.environ['COLUMNS'] = '200' + os.environ['LINES'] = '40' + os.environ['NO_COLOR'] = '1' + for unset in ('FORCE_COLOR', 'PY_COLORS'): + os.environ.pop(unset, None) + + +def tearDownModule() -> None: + for key, previous in _idf_py_test_env_saved.items(): + if previous is None: + os.environ.pop(key, None) + else: + os.environ[key] = previous + + class TestWithoutExtensions(TestCase): @classmethod def setUpClass(cls):