refactor(tools): Strings to f-strings conversion an other pre-commit issues

This commit is contained in:
Jakub Kocka
2025-07-23 09:45:18 +02:00
parent f1143b0b93
commit 7b65c5e0b7
2 changed files with 220 additions and 159 deletions

View File

@@ -15,10 +15,7 @@ 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
from console_output import debug from console_output import debug
from console_output import status_message from console_output import status_message
@@ -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:
@@ -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,15 +131,18 @@ 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.Group): 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)