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 TemporaryDirectory
from tempfile import gettempdir
from typing import Dict
from typing import List
from typing import TextIO
from typing import Union
from console_output import debug
from console_output import status_message
@@ -28,7 +25,7 @@ from utils import run_cmd
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.deactivate_cmd = deactivate_cmd
self.new_esp_idf_env = new_esp_idf_env
@@ -65,7 +62,7 @@ class Shell:
def export(self) -> None:
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()
if 'PATH' not in expanded_env:
@@ -88,7 +85,7 @@ class 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)
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:
fd.write(f'{stdout}\n')
fd.write(
(
'echo "\nDone! You can now compile ESP-IDF projects.\n'
'Go to the project directory and run:\n\n idf.py build"\n'
)
)
def export(self) -> None:
with open(self.script_file_path, 'w', encoding='utf-8') as fd:
@@ -208,7 +203,7 @@ class ZshShell(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)
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')
@@ -232,7 +227,7 @@ class FishShell(UnixShell):
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)
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()
fd.write(f'{functions}\n')
fd.write(
(
'echo "\nDone! You can now compile ESP-IDF projects.\n'
'Go to the project directory and run:\n\n idf.py build\n"'
)
)
def spawn(self) -> None:
self.init_file()
new_env = os.environ.copy()
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)
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)
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()
new_env = os.environ.copy()
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)
run(cmd, env=new_env)

View File

@@ -1,6 +1,6 @@
#!/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
#
@@ -22,14 +22,11 @@ import subprocess
import sys
from collections import Counter
from collections import OrderedDict
from collections.abc import Callable
from collections.abc import KeysView
from importlib import import_module
from pkgutil import iter_modules
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Union
# 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
try:
from idf_py_actions.errors import FatalError # noqa: E402
from idf_py_actions.tools import (PROG, SHELL_COMPLETE_RUN, SHELL_COMPLETE_VAR, PropertyDict, # noqa: E402
debug_print_idf_version, get_target, merge_action_lists, print_warning)
from idf_py_actions.errors import FatalError
from idf_py_actions.tools import PROG
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':
from idf_component_manager import idf_extensions
except ImportError as e:
print((f'{e}\n'
f'This usually means that "idf.py" was not '
f'spawned within an ESP-IDF shell environment or the python virtual '
f'environment used by "idf.py" is corrupted.\n'
f'Please use idf.py only in an ESP-IDF shell environment. If problem persists, '
f'please try to install ESP-IDF tools again as described in the Get Started guide.'),
file=sys.stderr)
print(
(
f'{e}\n'
'This usually means that "idf.py" was not '
'spawned within an ESP-IDF shell environment or the python virtual '
'environment used by "idf.py" is corrupted.\n'
'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:
# The ImportError or ModuleNotFoundError might be raised without
# specifying a module name. In this not so common situation, re-raise
@@ -69,7 +77,7 @@ 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
@@ -84,11 +92,12 @@ def check_environment() -> List:
set_idf_path = os.path.realpath(os.environ['IDF_PATH'])
if set_idf_path != detected_idf_path:
print_warning(
'WARNING: IDF_PATH environment variable is set to %s but %s path indicates IDF directory %s. '
'Using the environment variable directory, but results may be unexpected...' %
(set_idf_path, PROG, detected_idf_path))
f'WARNING: IDF_PATH environment variable is set to {set_idf_path}'
f' but {PROG} path indicates IDF directory {detected_idf_path}. '
'Using the environment variable directory, but results may be unexpected...'
)
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
try:
@@ -122,15 +131,18 @@ def check_environment() -> List:
try:
python_venv_path = os.environ['IDF_PYTHON_ENV_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:
print_warning('WARNING: The IDF_PYTHON_ENV_PATH is missing in environmental variables!')
return checks_output
def _safe_relpath(path: str, start: Optional[str]=None) -> str:
""" Return a relative path, same as os.path.relpath, but only if this is possible.
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.
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)
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()
import click
from click.shell_completion import CompletionItem
class Deprecation(object):
class Deprecation:
"""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.since = None
self.removed = None
@@ -162,25 +175,33 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
elif isinstance(deprecated, str):
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:
return '%s is deprecated %sand was removed%s.%s' % (
type,
'since %s ' % self.since if self.since else '',
' in %s' % self.removed if self.removed else '',
' %s' % self.custom_message if self.custom_message else '',
)
msg = f'{msg_type} is deprecated'
if self.since:
msg += f' since {self.since}'
msg += ' and was removed'
if self.removed:
msg += f' in {self.removed}. '
if self.custom_message:
msg += self.custom_message
return msg
else:
return '%s is deprecated %sand will be removed in%s.%s' % (
type,
'since %s ' % self.since if self.since else '',
' %s' % self.removed if self.removed else ' future versions',
' %s' % self.custom_message if self.custom_message else '',
)
msg = f'{msg_type} is deprecated'
if self.since:
msg += f' since {self.since}'
msg += ' and will be removed'
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 ''
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:
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:
deprecation = Deprecation(option.deprecated)
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:
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):
def __init__(self, callback: Callable, name: str, aliases: List, dependencies: Optional[List],
order_dependencies: Optional[List], action_args: Dict) -> None:
class Task:
def __init__(
self,
callback: Callable,
name: str,
aliases: list,
dependencies: list | None,
order_dependencies: list | None,
action_args: dict,
) -> None:
self.callback = callback
self.name = name
self.dependencies = dependencies
@@ -207,7 +237,9 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
self.action_args = action_args
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:
action_args = self.action_args
@@ -216,17 +248,18 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
class Action(click.Command):
def __init__(
self,
name: Optional[str]=None,
aliases: Optional[List]=None,
deprecated: Union[Dict, str, bool]=False,
dependencies: Optional[List]=None,
order_dependencies: Optional[List]=None,
hidden: bool=False,
**kwargs: Any) -> None:
super(Action, self).__init__(name, **kwargs)
name: str | None = None,
aliases: list | None = None,
deprecated: dict | str | bool = False,
dependencies: list | None = None,
order_dependencies: list | None = None,
hidden: bool = False,
**kwargs: Any,
) -> None:
super().__init__(name, **kwargs)
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
if aliases is None:
@@ -247,11 +280,11 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if deprecated:
deprecation = Deprecation(deprecated)
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
if aliases:
aliases_help = 'Aliases: %s.' % ', '.join(aliases)
aliases_help = f'Aliases: {", ".join(aliases)}.'
self.help = '\n'.join([self.help, aliases_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,
)
self.callback = wrapped_callback
self.callback: Callable = wrapped_callback
def invoke(self, ctx: click.core.Context) -> click.core.Context:
if 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:
raise FatalError('Error: %s' % message)
raise FatalError(f'Error: {message}')
else:
print_warning('Warning: %s' % message)
print_warning(f'Warning: {message}')
self.deprecated = False # disable Click's built-in deprecation handling
# Print warnings for options
check_deprecation(ctx)
return super(Action, self).invoke(ctx)
return super().invoke(ctx)
class Argument(click.Argument):
"""
@@ -293,11 +326,12 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
names - alias of 'param_decls'
"""
def __init__(self, **kwargs: str):
names = kwargs.pop('names')
super(Argument, self).__init__(names, **kwargs)
super().__init__(names, **kwargs)
class Scope(object):
class Scope:
"""
Scope for sub-command option.
possible values:
@@ -308,7 +342,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
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:
self._scope = 'default'
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):
self._scope = str(scope)
else:
raise FatalError('Unknown scope for option: %s' % scope)
raise FatalError(f'Unknown scope for option: {scope}')
@property
def is_global(self) -> bool:
@@ -331,7 +365,14 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
class Option(click.Option):
"""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:
@@ -344,7 +385,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
"""
kwargs['param_decls'] = kwargs.pop('names')
super(Option, self).__init__(**kwargs)
super().__init__(**kwargs)
self.deprecated = deprecated
self.scope = Scope(scope)
@@ -355,7 +396,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
self.help: str = deprecation.help(self.help)
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:
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:
return None
return super(Option, self).get_help_record(ctx)
return super().get_help_record(ctx)
class CLI(click.Group):
"""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,
invoke_without_command=True,
result_callback=self.execute_tasks,
no_args_is_help=True,
context_settings={'max_content_width': 140},
help=help,
help=cli_help,
)
self._actions = {}
self.global_action_callbacks = []
@@ -430,8 +477,9 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if option.scope.is_shared:
raise FatalError(
'"%s" is defined for action "%s". '
' "shared" options can be declared only on global level' % (option.name, name))
f'"{option.name}" is defined for action "{name}". '
'"shared" options can be declared only on global level'
)
# 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]:
@@ -439,10 +487,10 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
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))
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:
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 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
# COMP_WORDBREAKS. When @ is included, the @-argument is not considered
# part of the completion word, causing @-argument completion to function
# unreliably in bash.
complete_file = ('bash' not in os.environ.get('_IDF.PY_COMPLETE', '') or
'@' not in os.environ.get('IDF_PY_COMP_WORDBREAKS', ''))
complete_file = 'bash' not in os.environ.get('_IDF.PY_COMPLETE', '') or '@' not in os.environ.get(
'IDF_PY_COMP_WORDBREAKS', ''
)
if incomplete.startswith('@') and complete_file:
path_prefix = incomplete[1:]
candidates = glob.glob(path_prefix + '*')
result = [CompletionItem(f'@{c}') for c in candidates]
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:
# print a closing message of some kind,
@@ -482,7 +531,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
# how to flash them
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:
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:
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 not in flasher_args:
# This is the case for 'idf.py bootloader' if Secure Boot is on, need to follow manual flashing steps
print('\n%s build complete.' % title)
# This is the case for 'idf.py bootloader'
# if Secure Boot is on, need to follow manual flashing steps
print(f'\n{title} build complete.')
return
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 += flasher_args[key]['offset'] + ' '
@@ -518,11 +568,13 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
print('or')
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']),
f'-b {args.baud}',
'--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']:
esptool_cmd += ['--no-stub']
@@ -549,20 +601,24 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
if 'bootloader' in actions:
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()
global_args = PropertyDict(kwargs)
# Show warning if some tasks are present several times in the list
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:
dupes = ', '.join('"%s"' % t for t in dupplicated_tasks)
print('----------------------------------------------------------------------------------------')
dupes = ', '.join(f'"{t}"' for t in dupplicated_tasks)
print_warning(
'WARNING: Command%s found in the list of commands more than once. ' %
('s %s are' % dupes if len(dupplicated_tasks) > 1 else ' %s is' % dupes) +
'Only first occurrence will be executed.')
f'WARNING: {"Commands" if len(dupplicated_tasks) > 1 else "Command"} {dupes} '
f'{"are" if len(dupplicated_tasks) > 1 else "is"} '
'found in the list of commands more than once. '
'Only first occurrence will be executed.'
)
for task in tasks:
# 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 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} '
'environment variable is set to a different value.')
msg = (
f'This option cannot be set in command line if the {option.envvar} '
'environment variable is set to a different value.'
)
else:
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 '
f'a different value. {msg}')
raise FatalError(
f'Option "{key}" provided for "{task.name}" is already defined to '
f'a different value. {msg}'
)
if local_value != default:
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
else:
print(
'Adding "%s"\'s dependency "%s" to list of commands with default set of options.' %
(task.name, dep))
f'Adding "{task.name}"\'s dependency "{dep}" '
'to list of commands with default set of options.'
)
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
@@ -648,11 +709,12 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
for task in tasks_to_run.values():
name_with_aliases = task.name
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):
print('Executing action: %s' % name_with_aliases)
print(f'Executing action: {name_with_aliases}')
task(ctx, global_args, task.action_args)
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
@click.command(
add_help_option=False,
context_settings={
'allow_extra_args': True,
'ignore_unknown_options': True
},
context_settings={'allow_extra_args': True, 'ignore_unknown_options': True},
)
@click.option('-C', '--project-dir', default=os.getcwd(), type=click.Path())
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
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
idf_py_extensions_path = os.path.join(os.environ['IDF_PATH'], 'tools', 'idf_py_actions')
extension_dirs = [os.path.realpath(idf_py_extensions_path)]
@@ -689,7 +748,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
extensions = []
for directory in extension_dirs:
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
sys.path.append(directory)
@@ -713,7 +772,7 @@ def init_cli(verbose_output: Optional[List]=None) -> Any:
try:
all_actions = merge_action_lists(all_actions, extension.action_extensions(all_actions, project_dir))
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
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:
print_warning('Error importing extension file idf_ext.py. Skipping.')
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:
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 = (
'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. '
'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.
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)
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.
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()
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 = []
for arg in args:
if not arg.startswith('@'):
@@ -789,13 +850,17 @@ def expand_file_arguments(argv: List[Any]) -> List[Any]:
visited.add(rel_path)
try:
with open(rel_path, 'r', encoding='utf-8') as f:
with open(rel_path, encoding='utf-8') as f:
for line in f:
expanded_args.extend(expand_args(shlex.split(line), os.path.dirname(rel_path), file_stack + [file_name]))
except IOError:
expanded_args.extend(
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]])
raise FatalError(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.')
raise FatalError(
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
argv = expand_args(argv, os.getcwd(), [])
@@ -806,11 +871,7 @@ def expand_file_arguments(argv: List[Any]) -> List[Any]:
return argv
def _valid_unicode_config() -> Union[codecs.CodecInfo, bool]:
# Python 2 is always good
if sys.version_info[0] == 2:
return True
def _valid_unicode_config() -> codecs.CodecInfo | bool:
# With python 3 unicode environment is required
try:
return codecs.lookup(locale.getpreferredencoding()).name != 'ascii'
@@ -820,11 +881,15 @@ def _valid_unicode_config() -> Union[codecs.CodecInfo, bool]:
def _find_usable_locale() -> str:
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:
locales = ''
usable_locales: List[str] = []
usable_locales: list[str] = []
for line in locales.splitlines():
locale = line.strip()
locale_name = locale.lower().replace('-', '')
@@ -843,7 +908,8 @@ def _find_usable_locale() -> str:
if not usable_locales:
raise FatalError(
'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]
@@ -853,14 +919,16 @@ if __name__ == '__main__':
if 'MSYSTEM' in os.environ:
print_warning(
'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():
# Trying to find best utf-8 locale available on the system and restart python with it
best_locale = _find_usable_locale()
print_warning(
'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
ret = subprocess.call([sys.executable] + sys.argv, env=os.environ)