Merge branch 'fix/click_version' into 'master'

Fixed click deprecation warnings

Closes IDF-13075 and IDF-13088

See merge request espressif/esp-idf!40765
This commit is contained in:
Jakub Kocka
2025-08-29 14:48:55 +08:00
5 changed files with 297 additions and 212 deletions

View File

@@ -9,7 +9,7 @@ repos:
hooks: hooks:
- id: ruff-format - id: ruff-format
- id: ruff - id: ruff
args: [ "--fix" ] args: [ "--fix", "--show-fixes" ]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0 rev: v4.5.0
hooks: hooks:

View File

@@ -9,17 +9,14 @@ import sys
import textwrap import textwrap
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from importlib.metadata import version as importlib_version
from pathlib import Path from pathlib import Path
from subprocess import run from subprocess import run
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from tempfile import gettempdir from tempfile import gettempdir
from typing import Dict
from typing import List
from typing import TextIO from typing import TextIO
from typing import Union
import click
from console_output import debug from console_output import debug
from console_output import status_message from console_output import status_message
from console_output import warn from console_output import warn
@@ -28,7 +25,7 @@ from utils import run_cmd
class Shell: class Shell:
def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: Dict[str, str]): def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: dict[str, str]):
self.shell = shell self.shell = shell
self.deactivate_cmd = deactivate_cmd self.deactivate_cmd = deactivate_cmd
self.new_esp_idf_env = new_esp_idf_env self.new_esp_idf_env = new_esp_idf_env
@@ -65,7 +62,7 @@ class Shell:
def export(self) -> None: def export(self) -> None:
raise NotImplementedError('Subclass must implement abstract method "export"') raise NotImplementedError('Subclass must implement abstract method "export"')
def expanded_env(self) -> Dict[str, str]: def expanded_env(self) -> dict[str, str]:
expanded_env = self.new_esp_idf_env.copy() expanded_env = self.new_esp_idf_env.copy()
if 'PATH' not in expanded_env: if 'PATH' not in expanded_env:
@@ -88,7 +85,7 @@ class Shell:
class UnixShell(Shell): class UnixShell(Shell):
def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: Dict[str, str]): def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: dict[str, str]):
super().__init__(shell, deactivate_cmd, new_esp_idf_env) super().__init__(shell, deactivate_cmd, new_esp_idf_env)
with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_') as fd: with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_') as fd:
@@ -110,11 +107,9 @@ class UnixShell(Shell):
if stdout is not None: if stdout is not None:
fd.write(f'{stdout}\n') fd.write(f'{stdout}\n')
fd.write( fd.write(
(
'echo "\nDone! You can now compile ESP-IDF projects.\n' 'echo "\nDone! You can now compile ESP-IDF projects.\n'
'Go to the project directory and run:\n\n idf.py build"\n' 'Go to the project directory and run:\n\n idf.py build"\n'
) )
)
def export(self) -> None: def export(self) -> None:
with open(self.script_file_path, 'w', encoding='utf-8') as fd: with open(self.script_file_path, 'w', encoding='utf-8') as fd:
@@ -122,7 +117,7 @@ class UnixShell(Shell):
print(f'. {self.script_file_path}') print(f'. {self.script_file_path}')
def click_ver(self) -> int: def click_ver(self) -> int:
return int(click.__version__.split('.')[0]) return int(importlib_version('click').split('.')[0])
class BashShell(UnixShell): class BashShell(UnixShell):
@@ -208,7 +203,7 @@ class ZshShell(UnixShell):
class FishShell(UnixShell): class FishShell(UnixShell):
def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: Dict[str, str]): def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: dict[str, str]):
super().__init__(shell, deactivate_cmd, new_esp_idf_env) super().__init__(shell, deactivate_cmd, new_esp_idf_env)
self.new_esp_idf_env['IDF_TOOLS_INSTALL_CMD'] = os.path.join(conf.IDF_PATH, 'install.fish') self.new_esp_idf_env['IDF_TOOLS_INSTALL_CMD'] = os.path.join(conf.IDF_PATH, 'install.fish')
self.new_esp_idf_env['IDF_TOOLS_EXPORT_CMD'] = os.path.join(conf.IDF_PATH, 'export.fish') self.new_esp_idf_env['IDF_TOOLS_EXPORT_CMD'] = os.path.join(conf.IDF_PATH, 'export.fish')
@@ -232,7 +227,7 @@ class FishShell(UnixShell):
class PowerShell(Shell): class PowerShell(Shell):
def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: Dict[str, str]): def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: dict[str, str]):
super().__init__(shell, deactivate_cmd, new_esp_idf_env) super().__init__(shell, deactivate_cmd, new_esp_idf_env)
with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_', suffix='.ps1') as fd: with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_', suffix='.ps1') as fd:
@@ -270,22 +265,20 @@ class PowerShell(Shell):
functions = self.get_functions() functions = self.get_functions()
fd.write(f'{functions}\n') fd.write(f'{functions}\n')
fd.write( fd.write(
(
'echo "\nDone! You can now compile ESP-IDF projects.\n' 'echo "\nDone! You can now compile ESP-IDF projects.\n'
'Go to the project directory and run:\n\n idf.py build\n"' 'Go to the project directory and run:\n\n idf.py build\n"'
) )
)
def spawn(self) -> None: def spawn(self) -> None:
self.init_file() self.init_file()
new_env = os.environ.copy() new_env = os.environ.copy()
arguments = ['-NoExit', '-Command', f'{self.script_file_path}'] arguments = ['-NoExit', '-Command', f'{self.script_file_path}']
cmd: Union[str, List[str]] = [self.shell] + arguments cmd: str | list[str] = [self.shell] + arguments
run(cmd, env=new_env) run(cmd, env=new_env)
class WinCmd(Shell): class WinCmd(Shell):
def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: Dict[str, str]): def __init__(self, shell: str, deactivate_cmd: str, new_esp_idf_env: dict[str, str]):
super().__init__(shell, deactivate_cmd, new_esp_idf_env) super().__init__(shell, deactivate_cmd, new_esp_idf_env)
with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_', suffix='.bat') as fd: with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_', suffix='.bat') as fd:
@@ -338,7 +331,7 @@ class WinCmd(Shell):
self.init_file() self.init_file()
new_env = os.environ.copy() new_env = os.environ.copy()
arguments = ['/k', f'{self.script_file_path}'] arguments = ['/k', f'{self.script_file_path}']
cmd: Union[str, List[str]] = [self.shell] + arguments cmd: str | list[str] = [self.shell] + arguments
cmd = ' '.join(cmd) cmd = ' '.join(cmd)
run(cmd, env=new_env) run(cmd, env=new_env)

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD # SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
@@ -22,14 +22,11 @@ import subprocess
import sys import sys
from collections import Counter from collections import Counter
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Callable
from collections.abc import KeysView from collections.abc import KeysView
from importlib import import_module from importlib import import_module
from pkgutil import iter_modules from pkgutil import iter_modules
from typing import Any from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Union from typing import Union
# pyc files remain in the filesystem when switching between branches which might raise errors for incompatible # pyc files remain in the filesystem when switching between branches which might raise errors for incompatible
@@ -39,19 +36,30 @@ sys.dont_write_bytecode = True
import python_version_checker # noqa: E402 import python_version_checker # noqa: E402
try: try:
from idf_py_actions.errors import FatalError # noqa: E402 from idf_py_actions.errors import FatalError
from idf_py_actions.tools import (PROG, SHELL_COMPLETE_RUN, SHELL_COMPLETE_VAR, PropertyDict, # noqa: E402 from idf_py_actions.tools import PROG
debug_print_idf_version, get_target, merge_action_lists, print_warning) from idf_py_actions.tools import SHELL_COMPLETE_RUN
from idf_py_actions.tools import SHELL_COMPLETE_VAR
from idf_py_actions.tools import PropertyDict
from idf_py_actions.tools import debug_print_idf_version
from idf_py_actions.tools import get_target
from idf_py_actions.tools import merge_action_lists
from idf_py_actions.tools import print_warning
if os.getenv('IDF_COMPONENT_MANAGER') != '0': if os.getenv('IDF_COMPONENT_MANAGER') != '0':
from idf_component_manager import idf_extensions from idf_component_manager import idf_extensions
except ImportError as e: except ImportError as e:
print((f'{e}\n' print(
f'This usually means that "idf.py" was not ' (
f'spawned within an ESP-IDF shell environment or the python virtual ' f'{e}\n'
f'environment used by "idf.py" is corrupted.\n' 'This usually means that "idf.py" was not '
f'Please use idf.py only in an ESP-IDF shell environment. If problem persists, ' 'spawned within an ESP-IDF shell environment or the python virtual '
f'please try to install ESP-IDF tools again as described in the Get Started guide.'), 'environment used by "idf.py" is corrupted.\n'
file=sys.stderr) 'Please use idf.py only in an ESP-IDF shell environment. If problem persists, '
'please try to install ESP-IDF tools again as described in the Get Started guide.'
),
file=sys.stderr,
)
if e.name is None: if e.name is None:
# The ImportError or ModuleNotFoundError might be raised without # The ImportError or ModuleNotFoundError might be raised without
# specifying a module name. In this not so common situation, re-raise # specifying a module name. In this not so common situation, re-raise
@@ -69,7 +77,7 @@ PYTHON = sys.executable
os.environ['PYTHON'] = sys.executable os.environ['PYTHON'] = sys.executable
def check_environment() -> List: def check_environment() -> list:
""" """
Verify the environment contains the top-level tools we need to operate Verify the environment contains the top-level tools we need to operate
@@ -84,11 +92,12 @@ def check_environment() -> List:
set_idf_path = os.path.realpath(os.environ['IDF_PATH']) set_idf_path = os.path.realpath(os.environ['IDF_PATH'])
if set_idf_path != detected_idf_path: if set_idf_path != detected_idf_path:
print_warning( print_warning(
'WARNING: IDF_PATH environment variable is set to %s but %s path indicates IDF directory %s. ' f'WARNING: IDF_PATH environment variable is set to {set_idf_path}'
'Using the environment variable directory, but results may be unexpected...' % f' but {PROG} path indicates IDF directory {detected_idf_path}. '
(set_idf_path, PROG, detected_idf_path)) 'Using the environment variable directory, but results may be unexpected...'
)
else: else:
print_warning('Setting IDF_PATH environment variable: %s' % detected_idf_path) print_warning(f'Setting IDF_PATH environment variable: {detected_idf_path}')
os.environ['IDF_PATH'] = detected_idf_path os.environ['IDF_PATH'] = detected_idf_path
try: try:
@@ -122,14 +131,17 @@ def check_environment() -> List:
try: try:
python_venv_path = os.environ['IDF_PYTHON_ENV_PATH'] python_venv_path = os.environ['IDF_PYTHON_ENV_PATH']
if python_venv_path and not sys.executable.startswith(python_venv_path): if python_venv_path and not sys.executable.startswith(python_venv_path):
print_warning(f'WARNING: Python interpreter "{sys.executable}" used to start idf.py is not from installed venv "{python_venv_path}"') print_warning(
f'WARNING: Python interpreter "{sys.executable}" used to start idf.py'
f' is not from installed venv "{python_venv_path}"'
)
except KeyError: except KeyError:
print_warning('WARNING: The IDF_PYTHON_ENV_PATH is missing in environmental variables!') print_warning('WARNING: The IDF_PYTHON_ENV_PATH is missing in environmental variables!')
return checks_output return checks_output
def _safe_relpath(path: str, start: Optional[str]=None) -> str: def _safe_relpath(path: str, start: str | None = None) -> str:
"""Return a relative path, same as os.path.relpath, but only if this is possible. """Return a relative path, same as os.path.relpath, but only if this is possible.
It is not possible on Windows, if the start directory and the path are on different drives. It is not possible on Windows, if the start directory and the path are on different drives.
@@ -140,14 +152,15 @@ def _safe_relpath(path: str, start: Optional[str]=None) -> str:
return os.path.abspath(path) return os.path.abspath(path)
def init_cli(verbose_output: Optional[List]=None) -> Any: def init_cli(verbose_output: list | None = None) -> Any:
# Click is imported here to run it after check_environment() # Click is imported here to run it after check_environment()
import click import click
from click.shell_completion import CompletionItem from click.shell_completion import CompletionItem
class Deprecation(object): class Deprecation:
"""Construct deprecation notice for help messages""" """Construct deprecation notice for help messages"""
def __init__(self, deprecated: Union[Dict, str, bool]=False) -> None:
def __init__(self, deprecated: dict | str | bool = False) -> None:
self.deprecated = deprecated self.deprecated = deprecated
self.since = None self.since = None
self.removed = None self.removed = None
@@ -162,25 +175,33 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
elif isinstance(deprecated, str): elif isinstance(deprecated, str):
self.custom_message = deprecated self.custom_message = deprecated
def full_message(self, type: str='Option') -> str: def full_message(self, msg_type: str = 'Option') -> str:
if self.exit_with_error: if self.exit_with_error:
return '%s is deprecated %sand was removed%s.%s' % ( msg = f'{msg_type} is deprecated'
type, if self.since:
'since %s ' % self.since if self.since else '', msg += f' since {self.since}'
' in %s' % self.removed if self.removed else '', msg += ' and was removed'
' %s' % self.custom_message if self.custom_message else '', if self.removed:
) msg += f' in {self.removed}. '
if self.custom_message:
msg += self.custom_message
return msg
else: else:
return '%s is deprecated %sand will be removed in%s.%s' % ( msg = f'{msg_type} is deprecated'
type, if self.since:
'since %s ' % self.since if self.since else '', msg += f' since {self.since}'
' %s' % self.removed if self.removed else ' future versions', msg += ' and will be removed'
' %s' % self.custom_message if self.custom_message else '', if self.removed:
) msg += f' in {self.removed}. '
else:
msg += ' in future versions. '
if self.custom_message:
msg += self.custom_message
return msg
def help(self, text: str, type: str='Option', separator: str=' ') -> str: def help(self, text: str, msg_type: str = 'Option', separator: str = ' ') -> str:
text = text or '' text = text or ''
return self.full_message(type) + separator + text if self.deprecated else text return self.full_message(msg_type) + separator + text if self.deprecated else text
def short_help(self, text: str) -> str: def short_help(self, text: str) -> str:
text = text or '' text = text or ''
@@ -193,13 +214,22 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if isinstance(option, Option) and option.deprecated and ctx.params[option.name] != default: if isinstance(option, Option) and option.deprecated and ctx.params[option.name] != default:
deprecation = Deprecation(option.deprecated) deprecation = Deprecation(option.deprecated)
if deprecation.exit_with_error: if deprecation.exit_with_error:
raise FatalError('Error: %s' % deprecation.full_message('Option "%s"' % option.name)) error = deprecation.full_message(f'Option "{option.name}"')
raise FatalError(f'Error: {error}')
else: else:
print_warning('Warning: %s' % deprecation.full_message('Option "%s"' % option.name)) error = deprecation.full_message(f'Option "{option.name}"')
print_warning(f'Warning: {error}')
class Task(object): class Task:
def __init__(self, callback: Callable, name: str, aliases: List, dependencies: Optional[List], def __init__(
order_dependencies: Optional[List], action_args: Dict) -> None: self,
callback: Callable,
name: str,
aliases: list,
dependencies: list | None,
order_dependencies: list | None,
action_args: dict,
) -> None:
self.callback = callback self.callback = callback
self.name = name self.name = name
self.dependencies = dependencies self.dependencies = dependencies
@@ -207,7 +237,9 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
self.action_args = action_args self.action_args = action_args
self.aliases = aliases self.aliases = aliases
def __call__(self, context: click.core.Context, global_args: PropertyDict, action_args: Optional[Dict]=None) -> None: def __call__(
self, context: click.core.Context, global_args: PropertyDict, action_args: dict | None = None
) -> None:
if action_args is None: if action_args is None:
action_args = self.action_args action_args = self.action_args
@@ -216,17 +248,18 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
class Action(click.Command): class Action(click.Command):
def __init__( def __init__(
self, self,
name: Optional[str]=None, name: str | None = None,
aliases: Optional[List]=None, aliases: list | None = None,
deprecated: Union[Dict, str, bool]=False, deprecated: dict | str | bool = False,
dependencies: Optional[List]=None, dependencies: list | None = None,
order_dependencies: Optional[List]=None, order_dependencies: list | None = None,
hidden: bool = False, hidden: bool = False,
**kwargs: Any) -> None: **kwargs: Any,
super(Action, self).__init__(name, **kwargs) ) -> None:
super().__init__(name, **kwargs)
self.name: str = self.name or self.callback.__name__ self.name: str = self.name or self.callback.__name__
self.deprecated: Union[Dict, str, bool] = deprecated self.deprecated: dict | str | bool = deprecated
self.hidden: bool = hidden self.hidden: bool = hidden
if aliases is None: if aliases is None:
@@ -247,11 +280,11 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if deprecated: if deprecated:
deprecation = Deprecation(deprecated) deprecation = Deprecation(deprecated)
self.short_help = deprecation.short_help(self.short_help) self.short_help = deprecation.short_help(self.short_help)
self.help = deprecation.help(self.help, type='Command', separator='\n') self.help = deprecation.help(self.help, msg_type='Command', separator='\n')
# Add aliases to help string # Add aliases to help string
if aliases: if aliases:
aliases_help = 'Aliases: %s.' % ', '.join(aliases) aliases_help = f'Aliases: {", ".join(aliases)}.'
self.help = '\n'.join([self.help, aliases_help]) self.help = '\n'.join([self.help, aliases_help])
self.short_help = ' '.join([aliases_help, self.short_help]) self.short_help = ' '.join([aliases_help, self.short_help])
@@ -269,23 +302,23 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
aliases=self.aliases, aliases=self.aliases,
) )
self.callback = wrapped_callback self.callback: Callable = wrapped_callback
def invoke(self, ctx: click.core.Context) -> click.core.Context: def invoke(self, ctx: click.core.Context) -> click.core.Context:
if self.deprecated: if self.deprecated:
deprecation = Deprecation(self.deprecated) deprecation = Deprecation(self.deprecated)
message = deprecation.full_message('Command "%s"' % self.name) message = deprecation.full_message(f'Command "{self.name}"')
if deprecation.exit_with_error: if deprecation.exit_with_error:
raise FatalError('Error: %s' % message) raise FatalError(f'Error: {message}')
else: else:
print_warning('Warning: %s' % message) print_warning(f'Warning: {message}')
self.deprecated = False # disable Click's built-in deprecation handling self.deprecated = False # disable Click's built-in deprecation handling
# Print warnings for options # Print warnings for options
check_deprecation(ctx) check_deprecation(ctx)
return super(Action, self).invoke(ctx) return super().invoke(ctx)
class Argument(click.Argument): class Argument(click.Argument):
""" """
@@ -293,11 +326,12 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
names - alias of 'param_decls' names - alias of 'param_decls'
""" """
def __init__(self, **kwargs: str): def __init__(self, **kwargs: str):
names = kwargs.pop('names') names = kwargs.pop('names')
super(Argument, self).__init__(names, **kwargs) super().__init__(names, **kwargs)
class Scope(object): class Scope:
""" """
Scope for sub-command option. Scope for sub-command option.
possible values: possible values:
@@ -308,7 +342,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
SCOPES = ('default', 'global', 'shared') SCOPES = ('default', 'global', 'shared')
def __init__(self, scope: Optional[Union['Scope', str]]=None) -> None: # noqa: F821 def __init__(self, scope: Union['Scope', str] | None = None) -> None: # noqa: F821
if scope is None: if scope is None:
self._scope = 'default' self._scope = 'default'
elif isinstance(scope, str) and scope in self.SCOPES: elif isinstance(scope, str) and scope in self.SCOPES:
@@ -316,7 +350,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
elif isinstance(scope, Scope): elif isinstance(scope, Scope):
self._scope = str(scope) self._scope = str(scope)
else: else:
raise FatalError('Unknown scope for option: %s' % scope) raise FatalError(f'Unknown scope for option: {scope}')
@property @property
def is_global(self) -> bool: def is_global(self) -> bool:
@@ -331,7 +365,14 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
class Option(click.Option): class Option(click.Option):
"""Option that knows whether it should be global""" """Option that knows whether it should be global"""
def __init__(self, scope: Optional[Union[Scope, str]]=None, deprecated: Union[Dict, str, bool]=False, hidden: bool=False, **kwargs: str) -> None:
def __init__(
self,
scope: Scope | str | None = None,
deprecated: dict | str | bool = False,
hidden: bool = False,
**kwargs: str,
) -> None:
""" """
Keyword arguments additional to Click's Option class: Keyword arguments additional to Click's Option class:
@@ -344,7 +385,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
""" """
kwargs['param_decls'] = kwargs.pop('names') kwargs['param_decls'] = kwargs.pop('names')
super(Option, self).__init__(**kwargs) super().__init__(**kwargs)
self.deprecated = deprecated self.deprecated = deprecated
self.scope = Scope(scope) self.scope = Scope(scope)
@@ -355,7 +396,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
self.help: str = deprecation.help(self.help) self.help: str = deprecation.help(self.help)
if self.envvar: if self.envvar:
self.help += ' The default value can be set with the %s environment variable.' % self.envvar self.help += f' The default value can be set with the {self.envvar} environment variable.'
if self.scope.is_global: if self.scope.is_global:
self.help += ' This option can be used at most once either globally, or for one subcommand.' self.help += ' This option can be used at most once either globally, or for one subcommand.'
@@ -365,18 +406,24 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if self.hidden: if self.hidden:
return None return None
return super(Option, self).get_help_record(ctx) return super().get_help_record(ctx)
class CLI(click.MultiCommand): class CLI(click.Group):
"""Action list contains all actions with options available for CLI""" """Action list contains all actions with options available for CLI"""
def __init__(self, all_actions: Optional[Dict]=None, verbose_output: Optional[List]=None, help: Optional[str]=None) -> None:
super(CLI, self).__init__( def __init__(
self,
all_actions: dict | None = None,
verbose_output: list | None = None,
cli_help: str | None = None,
) -> None:
super().__init__(
chain=True, chain=True,
invoke_without_command=True, invoke_without_command=True,
result_callback=self.execute_tasks, result_callback=self.execute_tasks,
no_args_is_help=True, no_args_is_help=True,
context_settings={'max_content_width': 140}, context_settings={'max_content_width': 140},
help=help, help=cli_help,
) )
self._actions = {} self._actions = {}
self.global_action_callbacks = [] self.global_action_callbacks = []
@@ -430,8 +477,9 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if option.scope.is_shared: if option.scope.is_shared:
raise FatalError( raise FatalError(
'"%s" is defined for action "%s". ' f'"{option.name}" is defined for action "{name}". '
' "shared" options can be declared only on global level' % (option.name, name)) '"shared" options can be declared only on global level'
)
# Promote options to global if see for the first time # Promote options to global if see for the first time
if option.scope.is_global and option.name not in [o.name for o in self.params]: if option.scope.is_global and option.name not in [o.name for o in self.params]:
@@ -439,10 +487,10 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
self._actions[name].params.append(option) self._actions[name].params.append(option)
def list_commands(self, ctx: click.core.Context) -> List: def list_commands(self, ctx: click.core.Context) -> list:
return sorted(filter(lambda name: not self._actions[name].hidden, self._actions)) return sorted(filter(lambda name: not self._actions[name].hidden, self._actions))
def get_command(self, ctx: click.core.Context, name: str) -> Optional[Action]: def get_command(self, ctx: click.core.Context, name: str) -> Action | None:
if name in self.commands_with_aliases: if name in self.commands_with_aliases:
return self._actions.get(self.commands_with_aliases.get(name)) return self._actions.get(self.commands_with_aliases.get(name))
@@ -453,19 +501,20 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
return Action(name=name, callback=callback.unwrapped_callback) return Action(name=name, callback=callback.unwrapped_callback)
return None return None
def shell_complete(self, ctx: click.core.Context, incomplete: str) -> List[CompletionItem]: def shell_complete(self, ctx: click.core.Context, incomplete: str) -> list[CompletionItem]:
# Enable @-argument completion in bash only if @ is not present in # Enable @-argument completion in bash only if @ is not present in
# COMP_WORDBREAKS. When @ is included, the @-argument is not considered # COMP_WORDBREAKS. When @ is included, the @-argument is not considered
# part of the completion word, causing @-argument completion to function # part of the completion word, causing @-argument completion to function
# unreliably in bash. # unreliably in bash.
complete_file = ('bash' not in os.environ.get('_IDF.PY_COMPLETE', '') or complete_file = 'bash' not in os.environ.get('_IDF.PY_COMPLETE', '') or '@' not in os.environ.get(
'@' not in os.environ.get('IDF_PY_COMP_WORDBREAKS', '')) 'IDF_PY_COMP_WORDBREAKS', ''
)
if incomplete.startswith('@') and complete_file: if incomplete.startswith('@') and complete_file:
path_prefix = incomplete[1:] path_prefix = incomplete[1:]
candidates = glob.glob(path_prefix + '*') candidates = glob.glob(path_prefix + '*')
result = [CompletionItem(f'@{c}') for c in candidates] result = [CompletionItem(f'@{c}') for c in candidates]
return result return result
return super(CLI, self).shell_complete(ctx, incomplete) # type: ignore return super().shell_complete(ctx, incomplete) # type: ignore
def _print_closing_message(self, args: PropertyDict, actions: KeysView) -> None: def _print_closing_message(self, args: PropertyDict, actions: KeysView) -> None:
# print a closing message of some kind, # print a closing message of some kind,
@@ -482,7 +531,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
# how to flash them # how to flash them
def print_flashing_message(title: str, key: str) -> None: def print_flashing_message(title: str, key: str) -> None:
with open(os.path.join(args.build_dir, 'flasher_args.json'), encoding='utf-8') as file: with open(os.path.join(args.build_dir, 'flasher_args.json'), encoding='utf-8') as file:
flasher_args: Dict[str, Any] = json.load(file) flasher_args: dict[str, Any] = json.load(file)
def flasher_path(f: Union[str, 'os.PathLike[str]']) -> str: def flasher_path(f: Union[str, 'os.PathLike[str]']) -> str:
if type(args.build_dir) is bytes: if type(args.build_dir) is bytes:
@@ -491,11 +540,12 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if key != 'project': # flashing a single item if key != 'project': # flashing a single item
if key not in flasher_args: if key not in flasher_args:
# This is the case for 'idf.py bootloader' if Secure Boot is on, need to follow manual flashing steps # This is the case for 'idf.py bootloader'
print('\n%s build complete.' % title) # if Secure Boot is on, need to follow manual flashing steps
print(f'\n{title} build complete.')
return return
cmd = '' cmd = ''
if (key == 'bootloader'): # bootloader needs --flash-mode, etc to be passed in if key == 'bootloader': # bootloader needs --flash-mode, etc to be passed in
cmd = ' '.join(flasher_args['write_flash_args']) + ' ' cmd = ' '.join(flasher_args['write_flash_args']) + ' '
cmd += flasher_args[key]['offset'] + ' ' cmd += flasher_args[key]['offset'] + ' '
@@ -518,11 +568,13 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
print('or') print('or')
print(f' idf.py -p PORT {flash_target}') print(f' idf.py -p PORT {flash_target}')
esptool_cmd = ['python -m esptool', esptool_cmd = [
'python -m esptool',
'--chip {}'.format(flasher_args['extra_esptool_args']['chip']), '--chip {}'.format(flasher_args['extra_esptool_args']['chip']),
f'-b {args.baud}', f'-b {args.baud}',
'--before {}'.format(flasher_args['extra_esptool_args']['before']), '--before {}'.format(flasher_args['extra_esptool_args']['before']),
'--after {}'.format(flasher_args['extra_esptool_args']['after'])] '--after {}'.format(flasher_args['extra_esptool_args']['after']),
]
if not flasher_args['extra_esptool_args']['stub']: if not flasher_args['extra_esptool_args']['stub']:
esptool_cmd += ['--no-stub'] esptool_cmd += ['--no-stub']
@@ -549,20 +601,24 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if 'bootloader' in actions: if 'bootloader' in actions:
print_flashing_message('Bootloader', 'bootloader') print_flashing_message('Bootloader', 'bootloader')
def execute_tasks(self, tasks: List, **kwargs: str) -> OrderedDict: def execute_tasks(self, tasks: list, **kwargs: str) -> OrderedDict:
ctx = click.get_current_context() ctx = click.get_current_context()
global_args = PropertyDict(kwargs) global_args = PropertyDict(kwargs)
# Show warning if some tasks are present several times in the list # Show warning if some tasks are present several times in the list
dupplicated_tasks = sorted( dupplicated_tasks = sorted(
[item for item, count in Counter(task.name for task in tasks).items() if count > 1]) [item for item, count in Counter(task.name for task in tasks).items() if count > 1]
)
if dupplicated_tasks: if dupplicated_tasks:
dupes = ', '.join('"%s"' % t for t in dupplicated_tasks) print('----------------------------------------------------------------------------------------')
dupes = ', '.join(f'"{t}"' for t in dupplicated_tasks)
print_warning( print_warning(
'WARNING: Command%s found in the list of commands more than once. ' % f'WARNING: {"Commands" if len(dupplicated_tasks) > 1 else "Command"} {dupes} '
('s %s are' % dupes if len(dupplicated_tasks) > 1 else ' %s is' % dupes) + f'{"are" if len(dupplicated_tasks) > 1 else "is"} '
'Only first occurrence will be executed.') 'found in the list of commands more than once. '
'Only first occurrence will be executed.'
)
for task in tasks: for task in tasks:
# Set propagated global options. # Set propagated global options.
@@ -577,13 +633,17 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if global_value != default and local_value != default and global_value != local_value: if global_value != default and local_value != default and global_value != local_value:
if hasattr(option, 'envvar') and option.envvar and os.getenv(option.envvar) != default: if hasattr(option, 'envvar') and option.envvar and os.getenv(option.envvar) != default:
msg = (f'This option cannot be set in command line if the {option.envvar} ' msg = (
'environment variable is set to a different value.') f'This option cannot be set in command line if the {option.envvar} '
'environment variable is set to a different value.'
)
else: else:
msg = 'This option can appear at most once in the command line.' msg = 'This option can appear at most once in the command line.'
raise FatalError(f'Option "{key}" provided for "{task.name}" is already defined to ' raise FatalError(
f'a different value. {msg}') f'Option "{key}" provided for "{task.name}" is already defined to '
f'a different value. {msg}'
)
if local_value != default: if local_value != default:
global_args[key] = local_value global_args[key] = local_value
@@ -615,8 +675,9 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
# and put to the front of the list of unprocessed tasks # and put to the front of the list of unprocessed tasks
else: else:
print( print(
'Adding "%s"\'s dependency "%s" to list of commands with default set of options.' % f'Adding "{task.name}"\'s dependency "{dep}" '
(task.name, dep)) 'to list of commands with default set of options.'
)
dep_task = ctx.invoke(ctx.command.get_command(ctx, dep)) dep_task = ctx.invoke(ctx.command.get_command(ctx, dep))
# Remove options with global scope from invoke tasks because they are already in global_args # Remove options with global scope from invoke tasks because they are already in global_args
@@ -648,11 +709,12 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
for task in tasks_to_run.values(): for task in tasks_to_run.values():
name_with_aliases = task.name name_with_aliases = task.name
if task.aliases: if task.aliases:
name_with_aliases += ' (aliases: %s)' % ', '.join(task.aliases) name_with_aliases += f' (aliases: {", ".join(task.aliases)})'
# When machine-readable json format for help is printed, don't show info about executing action so the output is deserializable # When machine-readable json format for help is printed,
# don't show info about executing action so the output is deserializable
if name_with_aliases != 'help' or not task.action_args.get('json_option', False): if name_with_aliases != 'help' or not task.action_args.get('json_option', False):
print('Executing action: %s' % name_with_aliases) print(f'Executing action: {name_with_aliases}')
task(ctx, global_args, task.action_args) task(ctx, global_args, task.action_args)
self._print_closing_message(global_args, tasks_to_run.keys()) self._print_closing_message(global_args, tasks_to_run.keys())
@@ -663,10 +725,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
# fully featured click parser to be sure that extensions are loaded from the right place # fully featured click parser to be sure that extensions are loaded from the right place
@click.command( @click.command(
add_help_option=False, add_help_option=False,
context_settings={ context_settings={'allow_extra_args': True, 'ignore_unknown_options': True},
'allow_extra_args': True,
'ignore_unknown_options': True
},
) )
@click.option('-C', '--project-dir', default=os.getcwd(), type=click.Path()) @click.option('-C', '--project-dir', default=os.getcwd(), type=click.Path())
def parse_project_dir(project_dir: str) -> Any: def parse_project_dir(project_dir: str) -> Any:
@@ -675,7 +734,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
# Set `complete_var` to not existing environment variable name to prevent early cmd completion # Set `complete_var` to not existing environment variable name to prevent early cmd completion
project_dir = parse_project_dir(standalone_mode=False, complete_var='_IDF.PY_COMPLETE_NOT_EXISTING') project_dir = parse_project_dir(standalone_mode=False, complete_var='_IDF.PY_COMPLETE_NOT_EXISTING')
all_actions: Dict = {} all_actions: dict = {}
# Load extensions from components dir # Load extensions from components dir
idf_py_extensions_path = os.path.join(os.environ['IDF_PATH'], 'tools', 'idf_py_actions') idf_py_extensions_path = os.path.join(os.environ['IDF_PATH'], 'tools', 'idf_py_actions')
extension_dirs = [os.path.realpath(idf_py_extensions_path)] extension_dirs = [os.path.realpath(idf_py_extensions_path)]
@@ -689,7 +748,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
extensions = [] extensions = []
for directory in extension_dirs: for directory in extension_dirs:
if directory and not os.path.exists(directory): if directory and not os.path.exists(directory):
print_warning('WARNING: Directory with idf.py extensions doesn\'t exist:\n %s' % directory) print_warning(f"WARNING: Directory with idf.py extensions doesn't exist:\n\t{directory}")
continue continue
sys.path.append(directory) sys.path.append(directory)
@@ -713,7 +772,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
try: try:
all_actions = merge_action_lists(all_actions, extension.action_extensions(all_actions, project_dir)) all_actions = merge_action_lists(all_actions, extension.action_extensions(all_actions, project_dir))
except AttributeError: except AttributeError:
print_warning('WARNING: Cannot load idf.py extension "%s"' % name) print_warning(f'WARNING: Cannot load idf.py extension "{name}"')
# Load extensions from project dir # Load extensions from project dir
if os.path.exists(os.path.join(project_dir, 'idf_ext.py')): if os.path.exists(os.path.join(project_dir, 'idf_ext.py')):
@@ -723,7 +782,8 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
except ImportError: except ImportError:
print_warning('Error importing extension file idf_ext.py. Skipping.') print_warning('Error importing extension file idf_ext.py. Skipping.')
print_warning( print_warning(
"Please make sure that it contains implementation (even if it's empty) of add_action_extensions") "Please make sure that it contains implementation (even if it's empty) of add_action_extensions"
)
try: try:
all_actions = merge_action_lists(all_actions, action_extensions(all_actions, project_dir)) all_actions = merge_action_lists(all_actions, action_extensions(all_actions, project_dir))
@@ -733,12 +793,13 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
cli_help = ( cli_help = (
'ESP-IDF CLI build management tool. ' 'ESP-IDF CLI build management tool. '
'For commands that are not known to idf.py an attempt to execute it as a build system target will be made. ' 'For commands that are not known to idf.py an attempt to execute it as a build system target will be made. '
'Selected target: {}'.format(get_target(project_dir))) f'Selected target: {get_target(project_dir)}'
)
return CLI(help=cli_help, verbose_output=verbose_output, all_actions=all_actions) return CLI(cli_help=cli_help, verbose_output=verbose_output, all_actions=all_actions)
def main(argv: Optional[List[Any]] = None) -> None: def main(argv: list[Any] | None = None) -> None:
# Check the environment only when idf.py is invoked regularly from command line. # Check the environment only when idf.py is invoked regularly from command line.
checks_output = None if SHELL_COMPLETE_RUN else check_environment() checks_output = None if SHELL_COMPLETE_RUN else check_environment()
@@ -761,7 +822,7 @@ def main(argv: Optional[List[Any]] = None) -> None:
cli(argv, prog_name=PROG, complete_var=SHELL_COMPLETE_VAR) cli(argv, prog_name=PROG, complete_var=SHELL_COMPLETE_VAR)
def expand_file_arguments(argv: List[Any]) -> List[Any]: def expand_file_arguments(argv: list[Any]) -> list[Any]:
""" """
Any argument starting with "@" gets replaced with all values read from a text file. Any argument starting with "@" gets replaced with all values read from a text file.
Text file arguments can be split by newline or by space. Text file arguments can be split by newline or by space.
@@ -771,7 +832,7 @@ def expand_file_arguments(argv: List[Any]) -> List[Any]:
visited = set() visited = set()
expanded = False expanded = False
def expand_args(args: List[Any], parent_path: str, file_stack: List[str]) -> List[str]: def expand_args(args: list[Any], parent_path: str, file_stack: list[str]) -> list[str]:
expanded_args = [] expanded_args = []
for arg in args: for arg in args:
if not arg.startswith('@'): if not arg.startswith('@'):
@@ -789,13 +850,17 @@ def expand_file_arguments(argv: List[Any]) -> List[Any]:
visited.add(rel_path) visited.add(rel_path)
try: try:
with open(rel_path, 'r', encoding='utf-8') as f: with open(rel_path, encoding='utf-8') as f:
for line in f: for line in f:
expanded_args.extend(expand_args(shlex.split(line), os.path.dirname(rel_path), file_stack + [file_name])) expanded_args.extend(
except IOError: expand_args(shlex.split(line), os.path.dirname(rel_path), file_stack + [file_name])
)
except OSError:
file_stack_str = ' -> '.join(['@' + f for f in file_stack + [file_name]]) file_stack_str = ' -> '.join(['@' + f for f in file_stack + [file_name]])
raise FatalError(f"File '{rel_path}' (expansion of {file_stack_str}) could not be opened. " raise FatalError(
'Please ensure the file exists and you have the necessary permissions to read it.') f"File '{rel_path}' (expansion of {file_stack_str}) could not be opened. "
'Please ensure the file exists and you have the necessary permissions to read it.'
)
return expanded_args return expanded_args
argv = expand_args(argv, os.getcwd(), []) argv = expand_args(argv, os.getcwd(), [])
@@ -806,11 +871,7 @@ def expand_file_arguments(argv: List[Any]) -> List[Any]:
return argv return argv
def _valid_unicode_config() -> Union[codecs.CodecInfo, bool]: def _valid_unicode_config() -> codecs.CodecInfo | bool:
# Python 2 is always good
if sys.version_info[0] == 2:
return True
# With python 3 unicode environment is required # With python 3 unicode environment is required
try: try:
return codecs.lookup(locale.getpreferredencoding()).name != 'ascii' return codecs.lookup(locale.getpreferredencoding()).name != 'ascii'
@@ -820,11 +881,15 @@ def _valid_unicode_config() -> Union[codecs.CodecInfo, bool]:
def _find_usable_locale() -> str: def _find_usable_locale() -> str:
try: try:
locales = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode('ascii', 'replace') locales = (
subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
.communicate()[0]
.decode('ascii', 'replace')
)
except OSError: except OSError:
locales = '' locales = ''
usable_locales: List[str] = [] usable_locales: list[str] = []
for line in locales.splitlines(): for line in locales.splitlines():
locale = line.strip() locale = line.strip()
locale_name = locale.lower().replace('-', '') locale_name = locale.lower().replace('-', '')
@@ -843,7 +908,8 @@ def _find_usable_locale() -> str:
if not usable_locales: if not usable_locales:
raise FatalError( raise FatalError(
'Support for Unicode filenames is required, but no suitable UTF-8 locale was found on your system.' 'Support for Unicode filenames is required, but no suitable UTF-8 locale was found on your system.'
' Please refer to the manual for your operating system for details on locale reconfiguration.') ' Please refer to the manual for your operating system for details on locale reconfiguration.'
)
return usable_locales[0] return usable_locales[0]
@@ -853,14 +919,16 @@ if __name__ == '__main__':
if 'MSYSTEM' in os.environ: if 'MSYSTEM' in os.environ:
print_warning( print_warning(
'MSys/Mingw is no longer supported. Please follow the getting started guide of the ' 'MSys/Mingw is no longer supported. Please follow the getting started guide of the '
'documentation in order to set up a suitiable environment, or continue at your own risk.') 'documentation in order to set up a suitiable environment, or continue at your own risk.'
)
elif os.name == 'posix' and not _valid_unicode_config(): elif os.name == 'posix' and not _valid_unicode_config():
# Trying to find best utf-8 locale available on the system and restart python with it # Trying to find best utf-8 locale available on the system and restart python with it
best_locale = _find_usable_locale() best_locale = _find_usable_locale()
print_warning( print_warning(
'Your environment is not configured to handle unicode filenames outside of ASCII range.' 'Your environment is not configured to handle unicode filenames outside of ASCII range.'
' Environment variable LC_ALL is temporary set to %s for unicode support.' % best_locale) f' Environment variable LC_ALL is temporary set to {best_locale} for unicode support.'
)
os.environ['LC_ALL'] = best_locale os.environ['LC_ALL'] = best_locale
ret = subprocess.call([sys.executable] + sys.argv, env=os.environ) ret = subprocess.call([sys.executable] + sys.argv, env=os.environ)

View File

@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import json import json
import logging import logging
@@ -9,28 +9,23 @@ import subprocess
import sys import sys
import textwrap import textwrap
from pathlib import Path from pathlib import Path
from typing import List
import pytest import pytest
from test_build_system_helpers import append_to_file
from test_build_system_helpers import EnvDict from test_build_system_helpers import EnvDict
from test_build_system_helpers import IdfPyFunc
from test_build_system_helpers import append_to_file
from test_build_system_helpers import file_contains from test_build_system_helpers import file_contains
from test_build_system_helpers import find_python from test_build_system_helpers import find_python
from test_build_system_helpers import get_snapshot from test_build_system_helpers import get_snapshot
from test_build_system_helpers import IdfPyFunc
from test_build_system_helpers import replace_in_file from test_build_system_helpers import replace_in_file
from test_build_system_helpers import run_idf_py from test_build_system_helpers import run_idf_py
def get_subdirs_absolute_paths(path: Path) -> List[str]: def get_subdirs_absolute_paths(path: Path) -> list[str]:
""" """
Get a list of files with absolute path in a given `path` folder Get a list of files with absolute path in a given `path` folder
""" """
return [ return [f'{dir_path}/{file_name}' for dir_path, _, file_names in os.walk(path) for file_name in file_names]
'{}/{}'.format(dir_path, file_name)
for dir_path, _, file_names in os.walk(path)
for file_name in file_names
]
@pytest.mark.usefixtures('test_app_copy') @pytest.mark.usefixtures('test_app_copy')
@@ -51,10 +46,11 @@ def test_hints_no_color_output_when_noninteractive(idf_py: IdfPyFunc) -> None:
"""Check that idf.py hints don't include color escape codes in non-interactive builds""" """Check that idf.py hints don't include color escape codes in non-interactive builds"""
# make the build fail in such a way that idf.py shows a hint # make the build fail in such a way that idf.py shows a hint
replace_in_file('main/build_test_app.c', '// placeholder_inside_main', replace_in_file(
'esp_chip_info_t chip_info; esp_chip_info(&chip_info);') 'main/build_test_app.c', '// placeholder_inside_main', 'esp_chip_info_t chip_info; esp_chip_info(&chip_info);'
)
with (pytest.raises(subprocess.CalledProcessError)) as exc_info: with pytest.raises(subprocess.CalledProcessError) as exc_info:
idf_py('build') idf_py('build')
# Should not actually include a color escape sequence! # Should not actually include a color escape sequence!
@@ -70,10 +66,7 @@ def test_idf_copy(idf_copy: Path, idf_py: IdfPyFunc) -> None:
idf_py('build') idf_py('build')
def test_idf_build_with_env_var_sdkconfig_defaults( def test_idf_build_with_env_var_sdkconfig_defaults(test_app_copy: Path, default_idf_env: EnvDict) -> None:
test_app_copy: Path,
default_idf_env: EnvDict
) -> None:
with open(test_app_copy / 'sdkconfig.test', 'w') as fw: with open(test_app_copy / 'sdkconfig.test', 'w') as fw:
fw.write('CONFIG_BT_ENABLED=y') fw.write('CONFIG_BT_ENABLED=y')
@@ -86,9 +79,7 @@ def test_idf_build_with_env_var_sdkconfig_defaults(
@pytest.mark.usefixtures('test_app_copy') @pytest.mark.usefixtures('test_app_copy')
@pytest.mark.test_app_copy('examples/system/efuse') @pytest.mark.test_app_copy('examples/system/efuse')
def test_efuse_summary_cmake_functions( def test_efuse_summary_cmake_functions(default_idf_env: EnvDict) -> None:
default_idf_env: EnvDict
) -> None:
default_idf_env['IDF_CI_BUILD'] = '1' default_idf_env['IDF_CI_BUILD'] = '1'
output = run_idf_py('efuse-filter', env=default_idf_env) output = run_idf_py('efuse-filter', env=default_idf_env)
assert 'FROM_CMAKE: MAC: 00:00:00:00:00:00' in output.stdout assert 'FROM_CMAKE: MAC: 00:00:00:00:00:00' in output.stdout
@@ -117,16 +108,22 @@ def test_python_interpreter_unix(test_app_copy: Path) -> None:
logging.info("Make sure idf.py never runs '/usr/bin/env python' or similar") logging.info("Make sure idf.py never runs '/usr/bin/env python' or similar")
env_dict = dict(**os.environ) env_dict = dict(**os.environ)
python = find_python(env_dict['PATH']) python = find_python(env_dict['PATH'])
(test_app_copy / 'python').write_text(textwrap.dedent("""#!/bin/sh (test_app_copy / 'python').write_text(
textwrap.dedent(
"""
#!/bin/sh
echo "idf.py has executed '/usr/bin/env python' or similar" echo "idf.py has executed '/usr/bin/env python' or similar"
exit 1 exit 1
""")) """
)
)
st = os.stat(test_app_copy / 'python') st = os.stat(test_app_copy / 'python')
# equivalent to 'chmod +x ./python' # equivalent to 'chmod +x ./python'
os.chmod((test_app_copy / 'python'), st.st_mode | stat.S_IEXEC) os.chmod((test_app_copy / 'python'), st.st_mode | stat.S_IEXEC)
env_dict['PATH'] = str(test_app_copy) + os.pathsep + env_dict['PATH'] env_dict['PATH'] = str(test_app_copy) + os.pathsep + env_dict['PATH']
# python is loaded from env:$PATH, but since false interpreter is provided there, python needs to be specified as argument # python is loaded from env:$PATH
# but since false interpreter is provided there, python needs to be specified as argument
# if idf.py is reconfigured during it's execution, it would load a false interpreter # if idf.py is reconfigured during it's execution, it would load a false interpreter
run_idf_py('reconfigure', env=env_dict, python=python) run_idf_py('reconfigure', env=env_dict, python=python)
@@ -140,7 +137,8 @@ def test_python_interpreter_win(test_app_copy: Path) -> None:
# on windows python interpreter has compiled code '.exe' format, so this false file can be empty # on windows python interpreter has compiled code '.exe' format, so this false file can be empty
(test_app_copy / 'python.exe').write_text('') (test_app_copy / 'python.exe').write_text('')
env_dict['PATH'] = str(test_app_copy) + os.pathsep + env_dict['PATH'] env_dict['PATH'] = str(test_app_copy) + os.pathsep + env_dict['PATH']
# python is loaded from env:$PATH, but since false interpreter is provided there, python needs to be specified as argument # python is loaded from env:$PATH
# but since false interpreter is provided there, python needs to be specified as argument
# if idf.py is reconfigured during it's execution, it would load a false interpreter # if idf.py is reconfigured during it's execution, it would load a false interpreter
run_idf_py('reconfigure', env=env_dict, python=python) run_idf_py('reconfigure', env=env_dict, python=python)
@@ -172,7 +170,7 @@ def test_ccache_used_to_build(test_app_copy: Path) -> None:
def test_toolchain_prefix_in_description_file(idf_py: IdfPyFunc, test_app_copy: Path) -> None: def test_toolchain_prefix_in_description_file(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
logging.info('Toolchain prefix is set in project description file') logging.info('Toolchain prefix is set in project description file')
idf_py('reconfigure') idf_py('reconfigure')
data = json.load(open(test_app_copy / 'build' / 'project_description.json', 'r')) data = json.load(open(test_app_copy / 'build' / 'project_description.json'))
assert 'monitor_toolprefix' in data assert 'monitor_toolprefix' in data
@@ -195,8 +193,10 @@ def test_subcommands_with_options(idf_py: IdfPyFunc, default_idf_env: EnvDict) -
def test_fallback_to_build_system_target(idf_py: IdfPyFunc, test_app_copy: Path) -> None: def test_fallback_to_build_system_target(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
logging.info('idf.py fallback to build system target') logging.info('idf.py fallback to build system target')
msg = 'Custom target is running' msg = 'Custom target is running'
append_to_file(test_app_copy / 'CMakeLists.txt', append_to_file(
'add_custom_target(custom_target COMMAND ${{CMAKE_COMMAND}} -E echo "{}")'.format(msg)) test_app_copy / 'CMakeLists.txt',
f'add_custom_target(custom_target COMMAND ${{CMAKE_COMMAND}} -E echo "{msg}")',
)
ret = idf_py('custom_target') ret = idf_py('custom_target')
assert msg in ret.stdout, 'Custom target did not produce expected output' assert msg in ret.stdout, 'Custom target did not produce expected output'
@@ -205,10 +205,16 @@ def test_create_component_project(idf_copy: Path) -> None:
logging.info('Create project and component using idf.py and build it') logging.info('Create project and component using idf.py and build it')
run_idf_py('-C', 'projects', 'create-project', 'temp_test_project', workdir=idf_copy) run_idf_py('-C', 'projects', 'create-project', 'temp_test_project', workdir=idf_copy)
run_idf_py('-C', 'components', 'create-component', 'temp_test_component', workdir=idf_copy) run_idf_py('-C', 'components', 'create-component', 'temp_test_component', workdir=idf_copy)
replace_in_file(idf_copy / 'projects' / 'temp_test_project' / 'main' / 'temp_test_project.c', '{\n\n}', replace_in_file(
'\n'.join(['{', '\tfunc();', '}'])) idf_copy / 'projects' / 'temp_test_project' / 'main' / 'temp_test_project.c',
replace_in_file(idf_copy / 'projects' / 'temp_test_project' / 'main' / 'temp_test_project.c', '#include <stdio.h>', '{\n\n}',
'\n'.join(['#include <stdio.h>', '#include "temp_test_component.h"'])) '\n'.join(['{', '\tfunc();', '}']),
)
replace_in_file(
idf_copy / 'projects' / 'temp_test_project' / 'main' / 'temp_test_project.c',
'#include <stdio.h>',
'\n'.join(['#include <stdio.h>', '#include "temp_test_component.h"']),
)
run_idf_py('build', workdir=(idf_copy / 'projects' / 'temp_test_project')) run_idf_py('build', workdir=(idf_copy / 'projects' / 'temp_test_project'))
@@ -244,6 +250,7 @@ def test_create_project_with_idf_readonly(idf_copy: Path) -> None:
if '/bin/' in path: if '/bin/' in path:
continue # skip executables continue # skip executables
os.chmod(os.path.join(root, name), file_permission) os.chmod(os.path.join(root, name), file_permission)
logging.info('Check that command for creating new project will success if the IDF itself is readonly.') logging.info('Check that command for creating new project will success if the IDF itself is readonly.')
change_file_permissions(idf_copy, write_permission=False) change_file_permissions(idf_copy, write_permission=False)
try: try:
@@ -267,7 +274,18 @@ def test_docs_command(idf_py: IdfPyFunc) -> None:
assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1' in ret.stdout assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1' in ret.stdout
ret = idf_py('docs', '--no-browser', '--language', 'en', '--version', 'v4.2.1', '--target', 'esp32') ret = idf_py('docs', '--no-browser', '--language', 'en', '--version', 'v4.2.1', '--target', 'esp32')
assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1/esp32' in ret.stdout assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1/esp32' in ret.stdout
ret = idf_py('docs', '--no-browser', '--language', 'en', '--version', 'v4.2.1', '--target', 'esp32', '--starting-page', 'get-started') ret = idf_py(
'docs',
'--no-browser',
'--language',
'en',
'--version',
'v4.2.1',
'--target',
'esp32',
'--starting-page',
'get-started',
)
assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1/esp32/get-started' in ret.stdout assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1/esp32/get-started' in ret.stdout
@@ -285,25 +303,31 @@ def test_deprecation_warning(idf_py: IdfPyFunc) -> None:
def test_save_defconfig_check(idf_py: IdfPyFunc, test_app_copy: Path) -> None: def test_save_defconfig_check(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
logging.info('Save-defconfig checks') logging.info('Save-defconfig checks')
(test_app_copy / 'sdkconfig').write_text('\n'.join(['CONFIG_COMPILER_OPTIMIZATION_SIZE=y', (test_app_copy / 'sdkconfig').write_text(
'CONFIG_ESPTOOLPY_FLASHFREQ_80M=y'])) '\n'.join(['CONFIG_COMPILER_OPTIMIZATION_SIZE=y', 'CONFIG_ESPTOOLPY_FLASHFREQ_80M=y'])
)
idf_py('save-defconfig') idf_py('save-defconfig')
assert not file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_IDF_TARGET'), \ assert not file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_IDF_TARGET'), (
'CONFIG_IDF_TARGET should not be in sdkconfig.defaults' 'CONFIG_IDF_TARGET should not be in sdkconfig.defaults'
assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_COMPILER_OPTIMIZATION_SIZE=y'), \ )
assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_COMPILER_OPTIMIZATION_SIZE=y'), (
'Missing CONFIG_COMPILER_OPTIMIZATION_SIZE=y in sdkconfig.defaults' 'Missing CONFIG_COMPILER_OPTIMIZATION_SIZE=y in sdkconfig.defaults'
assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_ESPTOOLPY_FLASHFREQ_80M=y'), \ )
assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_ESPTOOLPY_FLASHFREQ_80M=y'), (
'Missing CONFIG_ESPTOOLPY_FLASHFREQ_80M=y in sdkconfig.defaults' 'Missing CONFIG_ESPTOOLPY_FLASHFREQ_80M=y in sdkconfig.defaults'
)
idf_py('fullclean') idf_py('fullclean')
(test_app_copy / 'sdkconfig').unlink() (test_app_copy / 'sdkconfig').unlink()
(test_app_copy / 'sdkconfig.defaults').unlink() (test_app_copy / 'sdkconfig.defaults').unlink()
idf_py('set-target', 'esp32c3') idf_py('set-target', 'esp32c3')
(test_app_copy / 'sdkconfig').write_text('CONFIG_PARTITION_TABLE_OFFSET=0x8001') (test_app_copy / 'sdkconfig').write_text('CONFIG_PARTITION_TABLE_OFFSET=0x8001')
idf_py('save-defconfig') idf_py('save-defconfig')
assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_IDF_TARGET="esp32c3"'), \ assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_IDF_TARGET="esp32c3"'), (
'Missing CONFIG_IDF_TARGET="esp32c3" in sdkconfig.defaults' 'Missing CONFIG_IDF_TARGET="esp32c3" in sdkconfig.defaults'
assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_PARTITION_TABLE_OFFSET=0x8001'), \ )
assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_PARTITION_TABLE_OFFSET=0x8001'), (
'Missing CONFIG_PARTITION_TABLE_OFFSET=0x8001 in sdkconfig.defaults' 'Missing CONFIG_PARTITION_TABLE_OFFSET=0x8001 in sdkconfig.defaults'
)
def test_merge_bin_cmd(idf_py: IdfPyFunc, test_app_copy: Path) -> None: def test_merge_bin_cmd(idf_py: IdfPyFunc, test_app_copy: Path) -> None:

View File

@@ -204,7 +204,8 @@ class TestDeprecations(TestWithoutExtensions):
[sys.executable, idf_py_path, '-C', current_dir, 'test-2'], env=os.environ, stderr=subprocess.STDOUT [sys.executable, idf_py_path, '-C', current_dir, 'test-2'], env=os.environ, stderr=subprocess.STDOUT
) )
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
self.assertIn('Error: Command "test-2" is deprecated and was removed.', e.output.decode('utf-8', 'ignore')) output = e.output.decode('utf-8', 'ignore').replace('\r\n', '\n')
self.assertIn('Error: Command "test-2" is deprecated and was removed\n', output)
def test_exit_with_error_for_option(self): def test_exit_with_error_for_option(self):
try: try:
@@ -239,7 +240,6 @@ class TestDeprecations(TestWithoutExtensions):
env=os.environ, env=os.environ,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
).decode('utf-8', 'ignore') ).decode('utf-8', 'ignore')
self.assertIn('Warning: Option "test_sub_1" is deprecated and will be removed in future versions.', output) self.assertIn('Warning: Option "test_sub_1" is deprecated and will be removed in future versions.', output)
self.assertIn( self.assertIn(
'Warning: Command "test-1" is deprecated and will be removed in future versions. ' 'Warning: Command "test-1" is deprecated and will be removed in future versions. '
@@ -374,8 +374,8 @@ class TestWrapperCommands(TestCase):
class TestEFuseCommands(TestWrapperCommands): class TestEFuseCommands(TestWrapperCommands):
""" """
Test if wrapper commands for espefuse.py are working as expected. Test if wrapper commands for espefuse.py are working as expected.
The goal is NOT to test the functionality of espefuse.py, but to test if the wrapper commands The goal is NOT to test the functionality of espefuse.py
are working as expected. but to test if the wrapper commands are working as expected.
""" """
def test_efuse_summary(self): def test_efuse_summary(self):
@@ -438,8 +438,8 @@ class TestEFuseCommands(TestWrapperCommands):
class TestSecureCommands(TestWrapperCommands): class TestSecureCommands(TestWrapperCommands):
""" """
Test if wrapper commands for espsecure.py are working as expected. Test if wrapper commands for espsecure.py are working as expected.
The goal is NOT to test the functionality of espsecure.py, but to test if the wrapper commands are The goal is NOT to test the functionality of espsecure.py
working as expected. but to test if the wrapper commands are working as expected.
""" """
@classmethod @classmethod
@@ -577,8 +577,8 @@ class TestSecureCommands(TestWrapperCommands):
class TestMergeBinCommands(TestWrapperCommands): class TestMergeBinCommands(TestWrapperCommands):
""" """
Test if merge-bin command is invoked as expected. Test if merge-bin command is invoked as expected.
This test is not testing the functionality of esptool.py merge_bin command, but the invocation of This test is not testing the functionality of esptool.py merge_bin command
the command from idf.py. but the invocation of the command from idf.py.
""" """
def test_merge_bin(self): def test_merge_bin(self):