change(tools): idf.py & tools.py ruff formatting

This commit is contained in:
Marek Fiala
2025-06-13 15:11:11 +02:00
committed by BOT
parent 22383e6be8
commit 2ec170b2fb
2 changed files with 201 additions and 133 deletions

View File

@@ -246,6 +246,8 @@ def init_cli(verbose_output: list | None = None) -> Any:
self.callback(self.name, context, global_args, **action_args) self.callback(self.name, context, global_args, **action_args)
class Action(click.Command): class Action(click.Command):
callback: Callable
def __init__( def __init__(
self, self,
name: str | None = None, name: str | None = None,

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 asyncio import asyncio
import importlib import importlib
@@ -8,21 +8,17 @@ import re
import subprocess import subprocess
import sys import sys
from asyncio.subprocess import Process from asyncio.subprocess import Process
from collections.abc import Generator
from pkgutil import iter_modules from pkgutil import iter_modules
from re import Match
from types import FunctionType from types import FunctionType
from typing import Any from typing import Any
from typing import Dict
from typing import Generator
from typing import List
from typing import Match
from typing import Optional
from typing import TextIO from typing import TextIO
from typing import Tuple
from typing import Union
import click import click
import yaml import yaml
from esp_idf_monitor import get_ansi_converter from esp_idf_monitor import get_ansi_converter
from idf_py_actions.errors import NoSerialPortFoundError from idf_py_actions.errors import NoSerialPortFoundError
from .constants import GENERATORS from .constants import GENERATORS
@@ -43,7 +39,7 @@ SHELL_COMPLETE_RUN = SHELL_COMPLETE_VAR in os.environ
# https://docs.python.org/3/reference/compound_stmts.html#function-definitions # https://docs.python.org/3/reference/compound_stmts.html#function-definitions
# Default parameter values are evaluated from left to right # Default parameter values are evaluated from left to right
# when the function definition is executed # when the function definition is executed
def get_build_context(ctx: Dict={}) -> Dict: def get_build_context(ctx: dict = {}) -> dict:
""" """
The build context is set in the ensure_build_directory function. It can be used The build context is set in the ensure_build_directory function. It can be used
in modules or other code, which don't have direct access to such information. in modules or other code, which don't have direct access to such information.
@@ -64,13 +60,13 @@ def _set_build_context(args: 'PropertyDict') -> None:
proj_desc_fn = f'{args.build_dir}/project_description.json' proj_desc_fn = f'{args.build_dir}/project_description.json'
try: try:
with open(proj_desc_fn, 'r', encoding='utf-8') as f: with open(proj_desc_fn, encoding='utf-8') as f:
ctx['proj_desc'] = json.load(f) ctx['proj_desc'] = json.load(f)
except (OSError, ValueError) as e: except (OSError, ValueError) as e:
raise FatalError(f'Cannot load {proj_desc_fn}: {e}') raise FatalError(f'Cannot load {proj_desc_fn}: {e}')
def executable_exists(args: List) -> bool: def executable_exists(args: list) -> bool:
try: try:
subprocess.check_output(args) subprocess.check_output(args)
return True return True
@@ -79,7 +75,7 @@ def executable_exists(args: List) -> bool:
return False return False
def _idf_version_from_cmake() -> Optional[str]: def _idf_version_from_cmake() -> str | None:
"""Acquires version of ESP-IDF from version.cmake""" """Acquires version of ESP-IDF from version.cmake"""
version_path = os.path.join(os.environ['IDF_PATH'], 'tools/cmake/version.cmake') version_path = os.path.join(os.environ['IDF_PATH'], 'tools/cmake/version.cmake')
regex = re.compile(r'^\s*set\s*\(\s*IDF_VERSION_([A-Z]{5})\s+(\d+)') regex = re.compile(r'^\s*set\s*\(\s*IDF_VERSION_([A-Z]{5})\s+(\d+)')
@@ -92,28 +88,38 @@ def _idf_version_from_cmake() -> Optional[str]:
if m: if m:
ver[m.group(1)] = m.group(2) ver[m.group(1)] = m.group(2)
return 'v%s.%s.%s' % (ver['MAJOR'], ver['MINOR'], ver['PATCH']) return f'v{ver["MAJOR"]}.{ver["MINOR"]}.{ver["PATCH"]}'
except (KeyError, OSError): except (KeyError, OSError):
sys.stderr.write('WARNING: Cannot find ESP-IDF version in version.cmake\n') sys.stderr.write('WARNING: Cannot find ESP-IDF version in version.cmake\n')
return None return None
def get_target(path: str, sdkconfig_filename: str='sdkconfig') -> Optional[str]: def get_target(path: str, sdkconfig_filename: str = 'sdkconfig') -> str | None:
path = os.path.join(path, sdkconfig_filename) path = os.path.join(path, sdkconfig_filename)
return get_sdkconfig_value(path, 'CONFIG_IDF_TARGET') return get_sdkconfig_value(path, 'CONFIG_IDF_TARGET')
def idf_version() -> Optional[str]: def idf_version() -> str | None:
"""Print version of ESP-IDF""" """Print version of ESP-IDF"""
# Try to get version from git: # Try to get version from git:
try: try:
version: Optional[str] = subprocess.check_output([ version: str | None = (
'git', subprocess.check_output(
'--git-dir=%s' % os.path.join(os.environ['IDF_PATH'], '.git'), [
'--work-tree=%s' % os.environ['IDF_PATH'], 'git',
'describe', '--tags', '--dirty', '--match', 'v*.*', f'--git-dir={os.path.join(os.environ["IDF_PATH"], ".git")}',
]).decode('utf-8', 'ignore').strip() f'--work-tree={os.environ["IDF_PATH"]}',
'describe',
'--tags',
'--dirty',
'--match',
'v*.*',
]
)
.decode('utf-8', 'ignore')
.strip()
)
except Exception: except Exception:
# if failed, then try to parse cmake.version file # if failed, then try to parse cmake.version file
sys.stderr.write('WARNING: Git version unavailable, reading from source\n') sys.stderr.write('WARNING: Git version unavailable, reading from source\n')
@@ -128,19 +134,18 @@ def get_default_serial_port() -> Any:
try: try:
import esptool import esptool
import serial.tools.list_ports import serial.tools.list_ports
ports = list(sorted(p.device for p in serial.tools.list_ports.comports())) ports = list(sorted(p.device for p in serial.tools.list_ports.comports()))
if sys.platform == 'darwin': if sys.platform == 'darwin':
ports = [ ports = [port for port in ports if not port.endswith(('Bluetooth-Incoming-Port', 'wlan-debug'))]
port
for port in ports
if not port.endswith(('Bluetooth-Incoming-Port', 'wlan-debug'))
]
# high baud rate could cause the failure of creation of the connection # high baud rate could cause the failure of creation of the connection
esp = esptool.get_default_connected_device(serial_list=ports, port=None, connect_attempts=4, esp = esptool.get_default_connected_device(
initial_baud=115200) serial_list=ports, port=None, connect_attempts=4, initial_baud=115200
)
if esp is None: if esp is None:
raise NoSerialPortFoundError( raise NoSerialPortFoundError(
"No serial ports found. Connect a device, or use '-p PORT' option to set a specific port.") "No serial ports found. Connect a device, or use '-p PORT' option to set a specific port."
)
serial_port = esp.serial_port serial_port = esp.serial_port
esp._port.close() esp._port.close()
@@ -150,29 +155,29 @@ def get_default_serial_port() -> Any:
except NoSerialPortFoundError: except NoSerialPortFoundError:
raise raise
except Exception as e: except Exception as e:
raise FatalError('An exception occurred during detection of the serial port: {}'.format(e)) raise FatalError(f'An exception occurred during detection of the serial port: {e}')
# function prints warning when autocompletion is not being performed # function prints warning when autocompletion is not being performed
# set argument stream to sys.stderr for errors and exceptions # set argument stream to sys.stderr for errors and exceptions
def print_warning(message: str, stream: Optional[TextIO]=None) -> None: def print_warning(message: str, stream: TextIO | None = None) -> None:
if not SHELL_COMPLETE_RUN: if not SHELL_COMPLETE_RUN:
print(message, file=stream or sys.stderr) print(message, file=stream or sys.stderr)
def color_print(message: str, color: str, newline: Optional[str]='\n') -> None: def color_print(message: str, color: str, newline: str | None = '\n') -> None:
""" Print a message to stderr with colored highlighting """ """Print a message to stderr with colored highlighting"""
ansi_normal = '\033[0m' ansi_normal = '\033[0m'
sys.stderr.write('%s%s%s%s' % (color, message, ansi_normal, newline)) sys.stderr.write(f'{color}{message}{ansi_normal}{newline}')
sys.stderr.flush() sys.stderr.flush()
def yellow_print(message: str, newline: Optional[str]='\n') -> None: def yellow_print(message: str, newline: str | None = '\n') -> None:
ansi_yellow = '\033[0;33m' ansi_yellow = '\033[0;33m'
color_print(message, ansi_yellow, newline) color_print(message, ansi_yellow, newline)
def red_print(message: str, newline: Optional[str]='\n') -> None: def red_print(message: str, newline: str | None = '\n') -> None:
ansi_red = '\033[1;31m' ansi_red = '\033[1;31m'
color_print(message, ansi_red, newline) color_print(message, ansi_red, newline)
@@ -181,15 +186,12 @@ def debug_print_idf_version() -> None:
print_warning(f'ESP-IDF {idf_version() or "version unknown"}') print_warning(f'ESP-IDF {idf_version() or "version unknown"}')
def load_hints() -> Dict: def load_hints() -> dict:
"""Helper function to load hints yml file""" """Helper function to load hints yml file"""
hints: Dict = { hints: dict = {'yml': [], 'modules': []}
'yml': [],
'modules': []
}
current_module_dir = os.path.dirname(__file__) current_module_dir = os.path.dirname(__file__)
with open(os.path.join(current_module_dir, 'hints.yml'), 'r', encoding='utf-8') as file: with open(os.path.join(current_module_dir, 'hints.yml'), encoding='utf-8') as file:
hints['yml'] = yaml.safe_load(file) hints['yml'] = yaml.safe_load(file)
hint_modules_dir = os.path.join(current_module_dir, 'hint_modules') hint_modules_dir = os.path.join(current_module_dir, 'hint_modules')
@@ -206,13 +208,13 @@ def load_hints() -> Dict:
red_print(f'Failed to import "{name}" from "{hint_modules_dir}" as a module') red_print(f'Failed to import "{name}" from "{hint_modules_dir}" as a module')
raise SystemExit(1) raise SystemExit(1)
except AttributeError: except AttributeError:
red_print('Module "{}" does not have function generate_hint.'.format(name)) red_print(f'Module "{name}" does not have function generate_hint.')
raise SystemExit(1) raise SystemExit(1)
return hints return hints
def generate_hints_buffer(output: str, hints: Dict) -> Generator: def generate_hints_buffer(output: str, hints: dict) -> Generator:
"""Helper function to process hints within a string buffer""" """Helper function to process hints within a string buffer"""
# Call modules for possible hints with unchanged output. Note that # Call modules for possible hints with unchanged output. Note that
# hints in hints.yml expect new line trimmed, but modules should # hints in hints.yml expect new line trimmed, but modules should
@@ -227,7 +229,7 @@ def generate_hints_buffer(output: str, hints: Dict) -> Generator:
for hint in hints['yml']: for hint in hints['yml']:
variables_list = hint.get('variables') variables_list = hint.get('variables')
hint_list, hint_vars, re_vars = [], [], [] hint_list, hint_vars, re_vars = [], [], []
match: Optional[Match[str]] = None match: Match[str] | None = None
try: try:
if variables_list: if variables_list:
for variables in variables_list: for variables in variables_list:
@@ -238,12 +240,12 @@ def generate_hints_buffer(output: str, hints: Dict) -> Generator:
try: try:
hint_list.append(hint['hint'].format(*hint_vars)) hint_list.append(hint['hint'].format(*hint_vars))
except KeyError as e: except KeyError as e:
red_print('Argument {} missing in {}. Check hints.yml file.'.format(e, hint)) red_print(f'Argument {e} missing in {hint}. Check hints.yml file.')
sys.exit(1) sys.exit(1)
else: else:
match = re.compile(hint['re']).search(output) match = re.compile(hint['re']).search(output)
except KeyError as e: except KeyError as e:
red_print('Argument {} missing in {}. Check hints.yml file.'.format(e, hint)) red_print(f'Argument {e} missing in {hint}. Check hints.yml file.')
sys.exit(1) sys.exit(1)
except re.error as e: except re.error as e:
red_print('{} from hints.yml have {} problem. Check hints.yml file.'.format(hint['re'], e)) red_print('{} from hints.yml have {} problem. Check hints.yml file.'.format(hint['re'], e))
@@ -256,14 +258,14 @@ def generate_hints_buffer(output: str, hints: Dict) -> Generator:
try: try:
yield ' '.join(['HINT:', hint['hint'].format(extra_info)]) yield ' '.join(['HINT:', hint['hint'].format(extra_info)])
except KeyError: except KeyError:
raise KeyError("Argument 'hint' missing in {}. Check hints.yml file.".format(hint)) raise KeyError(f"Argument 'hint' missing in {hint}. Check hints.yml file.")
def generate_hints(*filenames: str) -> Generator: def generate_hints(*filenames: str) -> Generator:
"""Getting output files and printing hints on how to resolve errors based on the output.""" """Getting output files and printing hints on how to resolve errors based on the output."""
hints = load_hints() hints = load_hints()
for file_name in filenames: for file_name in filenames:
with open(file_name, 'r', encoding='utf-8') as file: with open(file_name, encoding='utf-8') as file:
yield from generate_hints_buffer(file.read(), hints) yield from generate_hints_buffer(file.read(), hints)
@@ -279,14 +281,24 @@ def fit_text_in_terminal(out: str) -> str:
if len(out) >= terminal_width: if len(out) >= terminal_width:
elide_size = (terminal_width - space_for_dots) // 2 elide_size = (terminal_width - space_for_dots) // 2
# cut out the middle part of the output if it does not fit in the terminal # cut out the middle part of the output if it does not fit in the terminal
return '...'.join([out[:elide_size], out[len(out) - elide_size:]]) return '...'.join([out[:elide_size], out[len(out) - elide_size :]])
return out return out
class RunTool: class RunTool:
def __init__(self, tool_name: str, args: List, cwd: str, env: Optional[Dict]=None, custom_error_handler: Optional[FunctionType]=None, def __init__(
build_dir: Optional[str]=None, hints: bool=True, force_progression: bool=False, interactive: bool=False, convert_output: bool=False self,
) -> None: tool_name: str,
args: list,
cwd: str,
env: dict | None = None,
custom_error_handler: FunctionType | None = None,
build_dir: str | None = None,
hints: bool = True,
force_progression: bool = False,
interactive: bool = False,
convert_output: bool = False,
) -> None:
self.tool_name = tool_name self.tool_name = tool_name
self.args = args self.args = args
self.cwd = cwd self.cwd = cwd
@@ -301,20 +313,23 @@ class RunTool:
def __call__(self) -> None: def __call__(self) -> None:
def quote_arg(arg: str) -> str: def quote_arg(arg: str) -> str:
""" Quote the `arg` with whitespace in them because it can cause problems when we call it from a subprocess.""" """
if re.match(r"^(?![\'\"]).*\s.*", arg): Quote the `arg` with whitespace in them because
it can cause problems when we call it from a subprocess.
"""
if re.match(r'^(?![\'\"]).*\s.*', arg):
return ''.join(["'", arg, "'"]) return ''.join(["'", arg, "'"])
return arg return arg
self.args = [str(arg) for arg in self.args] self.args = [str(arg) for arg in self.args]
display_args = ' '.join(quote_arg(arg) for arg in self.args) display_args = ' '.join(quote_arg(arg) for arg in self.args)
print('Running %s in directory %s' % (self.tool_name, quote_arg(self.cwd))) print(f'Running {self.tool_name} in directory {quote_arg(self.cwd)}')
print('Executing "%s"...' % str(display_args)) print(f'Executing "{str(display_args)}"...')
env_copy = dict(os.environ) env_copy = dict(os.environ)
env_copy.update(self.env or {}) env_copy.update(self.env or {})
process: Union[Process, subprocess.CompletedProcess[bytes]] process: Process | subprocess.CompletedProcess[bytes]
if self.hints: if self.hints:
process, stderr_output_file, stdout_output_file = asyncio.run(self.run_command(self.args, env_copy)) process, stderr_output_file, stdout_output_file = asyncio.run(self.run_command(self.args, env_copy))
else: else:
@@ -332,14 +347,16 @@ class RunTool:
if not self.interactive: if not self.interactive:
for hint in generate_hints(stderr_output_file, stdout_output_file): for hint in generate_hints(stderr_output_file, stdout_output_file):
yellow_print(hint) yellow_print(hint)
raise FatalError('{} failed with exit code {}, output of the command is in the {} and {}'.format(self.tool_name, process.returncode, raise FatalError(
stderr_output_file, stdout_output_file)) f'{self.tool_name} failed with exit code {process.returncode}, '
f'output of the command is in the {stderr_output_file} and {stdout_output_file}'
)
raise FatalError('{} failed with exit code {}'.format(self.tool_name, process.returncode)) raise FatalError(f'{self.tool_name} failed with exit code {process.returncode}')
async def run_command(self, cmd: List, env_copy: Dict) -> Tuple[Process, Optional[str], Optional[str]]: async def run_command(self, cmd: list, env_copy: dict) -> tuple[Process, str | None, str | None]:
""" Run the `cmd` command with capturing stderr and stdout from that function and return returncode """Run the `cmd` command with capturing stderr and stdout from that function and return returncode
and of the command, the id of the process, paths to captured output """ and of the command, the id of the process, paths to captured output"""
log_dir_name = 'log' log_dir_name = 'log'
try: try:
os.mkdir(os.path.join(self.build_dir, log_dir_name)) os.mkdir(os.path.join(self.build_dir, log_dir_name))
@@ -348,13 +365,24 @@ class RunTool:
# Note: we explicitly pass in os.environ here, as we may have set IDF_PATH there during startup # Note: we explicitly pass in os.environ here, as we may have set IDF_PATH there during startup
# limit was added for avoiding error in idf.py confserver # limit was added for avoiding error in idf.py confserver
try: try:
p = await asyncio.create_subprocess_exec(*cmd, env=env_copy, limit=1024 * 256, cwd=self.cwd, stdout=asyncio.subprocess.PIPE, p = await asyncio.create_subprocess_exec(
stderr=asyncio.subprocess.PIPE) *cmd,
env=env_copy,
limit=1024 * 256,
cwd=self.cwd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
except NotImplementedError: except NotImplementedError:
message = f'ERROR: {sys.executable} doesn\'t support asyncio. The issue can be worked around by re-running idf.py with the "--no-hints" argument.' message = (
f"ERROR: {sys.executable} doesn't support asyncio. "
"Workaround: re-run idf.py with the '--no-hints' argument."
)
if sys.platform == 'win32': if sys.platform == 'win32':
message += ' To fix the issue use the Windows Installer for setting up your python environment, ' \ message += (
' To fix the issue use the Windows Installer for setting up your python environment, '
'available from: https://dl.espressif.com/dl/esp-idf/' 'available from: https://dl.espressif.com/dl/esp-idf/'
)
sys.exit(message) sys.exit(message)
stderr_output_file = os.path.join(self.build_dir, log_dir_name, f'idf_py_stderr_output_{p.pid}') stderr_output_file = os.path.join(self.build_dir, log_dir_name, f'idf_py_stderr_output_{p.pid}')
@@ -363,7 +391,8 @@ class RunTool:
try: try:
await asyncio.gather( await asyncio.gather(
self.read_and_write_stream(p.stderr, stderr_output_file, sys.stderr), self.read_and_write_stream(p.stderr, stderr_output_file, sys.stderr),
self.read_and_write_stream(p.stdout, stdout_output_file, sys.stdout)) self.read_and_write_stream(p.stdout, stdout_output_file, sys.stdout),
)
except asyncio.CancelledError: except asyncio.CancelledError:
# The process we are trying to read from was terminated. Print the # The process we are trying to read from was terminated. Print the
# message here and let the asyncio to finish, because # message here and let the asyncio to finish, because
@@ -376,9 +405,11 @@ class RunTool:
await p.wait() # added for avoiding None returncode await p.wait() # added for avoiding None returncode
return p, stderr_output_file, stdout_output_file return p, stderr_output_file, stdout_output_file
async def read_and_write_stream(self, input_stream: asyncio.StreamReader, output_filename: str, async def read_and_write_stream(
output_stream: TextIO) -> None: self, input_stream: asyncio.StreamReader, output_filename: str, output_stream: TextIO
) -> None:
"""read the output of the `input_stream` and then write it into `output_filename` and `output_stream`""" """read the output of the `input_stream` and then write it into `output_filename` and `output_stream`"""
def delete_ansi_escape(text: str) -> str: def delete_ansi_escape(text: str) -> str:
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
return ansi_escape.sub('', text) return ansi_escape.sub('', text)
@@ -394,7 +425,7 @@ class RunTool:
return True return True
return False return False
async def read_stream() -> Optional[str]: async def read_stream() -> str | None:
try: try:
output_b = await input_stream.readline() output_b = await input_stream.readline()
return output_b.decode(errors='ignore') return output_b.decode(errors='ignore')
@@ -404,7 +435,7 @@ class RunTool:
except AttributeError: except AttributeError:
return None return None
async def read_interactive_stream() -> Optional[str]: async def read_interactive_stream() -> str | None:
buffer = b'' buffer = b''
while True: while True:
output_b = await input_stream.read(1) output_b = await input_stream.read(1)
@@ -470,9 +501,11 @@ class RunTool:
for hint in generate_hints_buffer(last_line, hints): for hint in generate_hints_buffer(last_line, hints):
yellow_print(hint) yellow_print(hint)
last_line = '' last_line = ''
except (RuntimeError, EnvironmentError) as e: except (OSError, RuntimeError) as e:
yellow_print('WARNING: The exception {} was raised and we can\'t capture all your {} and ' yellow_print(
'hints on how to resolve errors can be not accurate.'.format(e, output_stream.name.strip('<>'))) "WARNING: The exception {} was raised and we can't capture all your {} and "
'hints on how to resolve errors can be not accurate.'.format(e, output_stream.name.strip('<>'))
)
def run_tool(*args: Any, **kwargs: Any) -> None: def run_tool(*args: Any, **kwargs: Any) -> None:
@@ -480,8 +513,14 @@ def run_tool(*args: Any, **kwargs: Any) -> None:
return RunTool(*args, **kwargs)() return RunTool(*args, **kwargs)()
def run_target(target_name: str, args: 'PropertyDict', env: Optional[Dict]=None, def run_target(
custom_error_handler: Optional[FunctionType]=None, force_progression: bool=False, interactive: bool=False) -> None: target_name: str,
args: 'PropertyDict',
env: dict | None = None,
custom_error_handler: FunctionType | None = None,
force_progression: bool = False,
interactive: bool = False,
) -> None:
"""Run target in build directory.""" """Run target in build directory."""
if env is None: if env is None:
env = {} env = {}
@@ -498,11 +537,19 @@ def run_target(target_name: str, args: 'PropertyDict', env: Optional[Dict]=None,
if 'CLICOLOR_FORCE' not in env: if 'CLICOLOR_FORCE' not in env:
env['CLICOLOR_FORCE'] = '1' env['CLICOLOR_FORCE'] = '1'
RunTool(generator_cmd[0], generator_cmd + [target_name], args.build_dir, env, custom_error_handler, hints=not args.no_hints, RunTool(
force_progression=force_progression, interactive=interactive)() generator_cmd[0],
generator_cmd + [target_name],
args.build_dir,
env,
custom_error_handler,
hints=not args.no_hints,
force_progression=force_progression,
interactive=interactive,
)()
def _strip_quotes(value: str, regexp: re.Pattern=re.compile(r"^\"(.*)\"$|^'(.*)'$|^(.*)$")) -> Optional[str]: def _strip_quotes(value: str, regexp: re.Pattern = re.compile(r"^\"(.*)\"$|^'(.*)'$|^(.*)$")) -> str | None:
""" """
Strip quotes like CMake does during parsing cache entries Strip quotes like CMake does during parsing cache entries
""" """
@@ -510,7 +557,7 @@ def _strip_quotes(value: str, regexp: re.Pattern=re.compile(r"^\"(.*)\"$|^'(.*)'
return [x for x in matching_values.groups() if x is not None][0].rstrip() if matching_values is not None else None return [x for x in matching_values.groups() if x is not None][0].rstrip() if matching_values is not None else None
def _parse_cmakecache(path: str) -> Dict: def _parse_cmakecache(path: str) -> dict:
""" """
Parse the CMakeCache file at 'path'. Parse the CMakeCache file at 'path'.
@@ -529,13 +576,13 @@ def _parse_cmakecache(path: str) -> Dict:
return result return result
def _parse_cmdl_cmakecache(entries: List) -> Dict[str, str]: def _parse_cmdl_cmakecache(entries: list) -> dict[str, str]:
""" """
Parse list of CMake cache entries passed in via the -D option. Parse list of CMake cache entries passed in via the -D option.
Returns a dict of name:value. Returns a dict of name:value.
""" """
result: Dict = {} result: dict = {}
for entry in entries: for entry in entries:
key, value = entry.split('=', 1) key, value = entry.split('=', 1)
value = _strip_quotes(value) value = _strip_quotes(value)
@@ -544,7 +591,7 @@ def _parse_cmdl_cmakecache(entries: List) -> Dict[str, str]:
return result return result
def _new_cmakecache_entries(cache: Dict, cache_cmdl: Dict) -> bool: def _new_cmakecache_entries(cache: dict, cache_cmdl: dict) -> bool:
for entry in cache_cmdl: for entry in cache_cmdl:
if entry not in cache: if entry not in cache:
return True return True
@@ -557,14 +604,15 @@ def _detect_cmake_generator(prog_name: str) -> Any:
""" """
Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found. Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
""" """
for (generator_name, generator) in GENERATORS.items(): for generator_name, generator in GENERATORS.items():
if executable_exists(generator['version']): if executable_exists(generator['version']):
return generator_name return generator_name
raise FatalError("To use %s, either the 'ninja' or 'GNU make' build tool must be available in the PATH" % prog_name) raise FatalError(f"To use {prog_name}, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
def ensure_build_directory(args: 'PropertyDict', prog_name: str, always_run_cmake: bool=False, def ensure_build_directory(
env: Optional[Dict]=None) -> None: args: 'PropertyDict', prog_name: str, always_run_cmake: bool = False, env: dict | None = None
) -> None:
"""Check the build directory exists and that cmake has been run there. """Check the build directory exists and that cmake has been run there.
If this isn't the case, create the build directory (if necessary) and If this isn't the case, create the build directory (if necessary) and
@@ -583,11 +631,11 @@ def ensure_build_directory(args: 'PropertyDict', prog_name: str, always_run_cmak
# Verify the project directory # Verify the project directory
if not os.path.isdir(project_dir): if not os.path.isdir(project_dir):
if not os.path.exists(project_dir): if not os.path.exists(project_dir):
raise FatalError('Project directory %s does not exist' % project_dir) raise FatalError(f'Project directory {project_dir} does not exist')
else: else:
raise FatalError('%s must be a project directory' % project_dir) raise FatalError(f'{project_dir} must be a project directory')
if not os.path.exists(os.path.join(project_dir, 'CMakeLists.txt')): if not os.path.exists(os.path.join(project_dir, 'CMakeLists.txt')):
raise FatalError('CMakeLists.txt not found in project directory %s' % project_dir) raise FatalError(f'CMakeLists.txt not found in project directory {project_dir}')
# Verify/create the build directory # Verify/create the build directory
build_dir = args.build_dir build_dir = args.build_dir
@@ -598,7 +646,7 @@ def ensure_build_directory(args: 'PropertyDict', prog_name: str, always_run_cmak
cache_path = os.path.join(build_dir, 'CMakeCache.txt') cache_path = os.path.join(build_dir, 'CMakeCache.txt')
cache = _parse_cmakecache(cache_path) if os.path.exists(cache_path) else {} cache = _parse_cmakecache(cache_path) if os.path.exists(cache_path) else {}
args.define_cache_entry.append('CCACHE_ENABLE=%d' % args.ccache) args.define_cache_entry.append(f'CCACHE_ENABLE={args.ccache:d}')
cache_cmdl = _parse_cmdl_cmakecache(args.define_cache_entry) cache_cmdl = _parse_cmdl_cmakecache(args.define_cache_entry)
@@ -614,7 +662,7 @@ def ensure_build_directory(args: 'PropertyDict', prog_name: str, always_run_cmak
'-G', '-G',
args.generator, args.generator,
'-DPYTHON_DEPS_CHECKED=1', '-DPYTHON_DEPS_CHECKED=1',
'-DPYTHON={}'.format(sys.executable), f'-DPYTHON={sys.executable}',
'-DESP_PLATFORM=1', '-DESP_PLATFORM=1',
] ]
if args.cmake_warn_uninitialized: if args.cmake_warn_uninitialized:
@@ -641,17 +689,20 @@ def ensure_build_directory(args: 'PropertyDict', prog_name: str, always_run_cmak
except KeyError: except KeyError:
generator = _detect_cmake_generator(prog_name) generator = _detect_cmake_generator(prog_name)
if args.generator is None: if args.generator is None:
args.generator = (generator) # reuse the previously configured generator, if none was given args.generator = generator # reuse the previously configured generator, if none was given
if generator != args.generator: if generator != args.generator:
raise FatalError("Build is configured for generator '%s' not '%s'. Run '%s fullclean' to start again." % raise FatalError(
(generator, args.generator, prog_name)) f"Build is configured for generator '{generator}' not '{args.generator}'. "
f"Run '{prog_name} fullclean' to start again."
)
try: try:
home_dir = cache['CMAKE_HOME_DIRECTORY'] home_dir = cache['CMAKE_HOME_DIRECTORY']
if os.path.realpath(home_dir) != os.path.realpath(project_dir): if os.path.realpath(home_dir) != os.path.realpath(project_dir):
raise FatalError( raise FatalError(
"Build directory '%s' configured for project '%s' not '%s'. Run '%s fullclean' to start again." % f"Build directory '{build_dir}' configured for project '{os.path.realpath(home_dir)}' "
(build_dir, os.path.realpath(home_dir), os.path.realpath(project_dir), prog_name)) f"not '{os.path.realpath(project_dir)}'. Run '{prog_name} fullclean' to start again."
)
except KeyError: except KeyError:
pass # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet pass # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
@@ -659,8 +710,10 @@ def ensure_build_directory(args: 'PropertyDict', prog_name: str, always_run_cmak
python = cache['PYTHON'] python = cache['PYTHON']
if os.path.normcase(python) != os.path.normcase(sys.executable): if os.path.normcase(python) != os.path.normcase(sys.executable):
raise FatalError( raise FatalError(
"'{}' is currently active in the environment while the project was configured with '{}'. " f"'{sys.executable}' is currently active in the environment while the project was "
"Run '{} fullclean' to start again.".format(sys.executable, python, prog_name)) f"configured with '{python}'. "
f"Run '{prog_name} fullclean' to start again."
)
except KeyError: except KeyError:
pass pass
@@ -668,8 +721,8 @@ def ensure_build_directory(args: 'PropertyDict', prog_name: str, always_run_cmak
_set_build_context(args) _set_build_context(args)
def merge_action_lists(*action_lists: Dict) -> Dict: def merge_action_lists(*action_lists: dict) -> dict:
merged_actions: Dict = { merged_actions: dict = {
'global_options': [], 'global_options': [],
'actions': {}, 'actions': {},
'global_action_callbacks': [], 'global_action_callbacks': [],
@@ -681,7 +734,7 @@ def merge_action_lists(*action_lists: Dict) -> Dict:
return merged_actions return merged_actions
def get_sdkconfig_filename(args: 'PropertyDict', cache_cmdl: Optional[Dict]=None) -> str: def get_sdkconfig_filename(args: 'PropertyDict', cache_cmdl: dict | None = None) -> str:
""" """
Get project's sdkconfig file name. Get project's sdkconfig file name.
""" """
@@ -693,7 +746,7 @@ def get_sdkconfig_filename(args: 'PropertyDict', cache_cmdl: Optional[Dict]=None
proj_desc_path = os.path.join(args.build_dir, 'project_description.json') proj_desc_path = os.path.join(args.build_dir, 'project_description.json')
try: try:
with open(proj_desc_path, 'r', encoding='utf-8') as f: with open(proj_desc_path, encoding='utf-8') as f:
proj_desc = json.load(f) proj_desc = json.load(f)
return str(proj_desc['config_file']) return str(proj_desc['config_file'])
except (OSError, KeyError): except (OSError, KeyError):
@@ -702,7 +755,7 @@ def get_sdkconfig_filename(args: 'PropertyDict', cache_cmdl: Optional[Dict]=None
return os.path.join(args.project_dir, 'sdkconfig') return os.path.join(args.project_dir, 'sdkconfig')
def get_sdkconfig_value(sdkconfig_file: str, key: str) -> Optional[str]: def get_sdkconfig_value(sdkconfig_file: str, key: str) -> str | None:
""" """
Return the value of given key from sdkconfig_file. Return the value of given key from sdkconfig_file.
If sdkconfig_file does not exist or the option is not present, returns None. If sdkconfig_file does not exist or the option is not present, returns None.
@@ -713,8 +766,8 @@ def get_sdkconfig_value(sdkconfig_file: str, key: str) -> Optional[str]:
# keep track of the last seen value for the given key # keep track of the last seen value for the given key
value = None value = None
# if the value is quoted, this excludes the quotes from the value # if the value is quoted, this excludes the quotes from the value
pattern = re.compile(r"^{}=\"?([^\"]*)\"?$".format(key)) pattern = re.compile(rf'^{key}=\"?([^\"]*)\"?$')
with open(sdkconfig_file, 'r', encoding='utf-8') as f: with open(sdkconfig_file, encoding='utf-8') as f:
for line in f: for line in f:
match = re.match(pattern, line) match = re.match(pattern, line)
if match: if match:
@@ -722,15 +775,16 @@ def get_sdkconfig_value(sdkconfig_file: str, key: str) -> Optional[str]:
return value return value
def is_target_supported(project_path: str, supported_targets: List) -> bool: def is_target_supported(project_path: str, supported_targets: list) -> bool:
""" """
Returns True if the active target is supported, or False otherwise. Returns True if the active target is supported, or False otherwise.
""" """
return get_target(project_path) in supported_targets return get_target(project_path) in supported_targets
def _check_idf_target(args: 'PropertyDict', prog_name: str, cache: Dict, def _check_idf_target(
cache_cmdl: Dict, env: Optional[Dict]=None) -> None: args: 'PropertyDict', prog_name: str, cache: dict, cache_cmdl: dict, env: dict | None = None
) -> None:
""" """
Cross-check the three settings (sdkconfig, CMakeCache, environment) and if there is Cross-check the three settings (sdkconfig, CMakeCache, environment) and if there is
mismatch, fail with instructions on how to fix this. mismatch, fail with instructions on how to fix this.
@@ -750,34 +804,45 @@ def _check_idf_target(args: 'PropertyDict', prog_name: str, cache: Dict,
if idf_target_from_env: if idf_target_from_env:
# Let's check that IDF_TARGET values are consistent # Let's check that IDF_TARGET values are consistent
if idf_target_from_sdkconfig and idf_target_from_sdkconfig != idf_target_from_env: if idf_target_from_sdkconfig and idf_target_from_sdkconfig != idf_target_from_env:
raise FatalError("Project sdkconfig '{cfg}' was generated for target '{t_conf}', but environment variable IDF_TARGET " raise FatalError(
"is set to '{t_env}'. Run '{prog} set-target {t_env}' to generate new sdkconfig file for target {t_env}." f"Project sdkconfig '{sdkconfig}' was generated for target '{idf_target_from_sdkconfig}', "
.format(cfg=sdkconfig, t_conf=idf_target_from_sdkconfig, t_env=idf_target_from_env, prog=prog_name)) f"but environment variable IDF_TARGET is set to '{idf_target_from_env}'. "
f"Run '{prog_name} set-target {idf_target_from_env}' to generate new sdkconfig "
f'file for target {idf_target_from_env}.'
)
if idf_target_from_cache and idf_target_from_cache != idf_target_from_env: if idf_target_from_cache and idf_target_from_cache != idf_target_from_env:
raise FatalError("Target settings are not consistent: '{t_env}' in the environment, '{t_cache}' in CMakeCache.txt. " raise FatalError(
"Run '{prog} fullclean' to start again." f"Target settings are not consistent: '{idf_target_from_env}' in the environment, "
.format(t_env=idf_target_from_env, t_cache=idf_target_from_cache, prog=prog_name)) f"'{idf_target_from_cache}' in CMakeCache.txt. "
f"Run '{prog_name} fullclean' to start again."
)
if idf_target_from_cache_cmdl and idf_target_from_cache_cmdl != idf_target_from_env: if idf_target_from_cache_cmdl and idf_target_from_cache_cmdl != idf_target_from_env:
raise FatalError("Target '{t_cmdl}' specified on command line is not consistent with " raise FatalError(
"target '{t_env}' in the environment." f"Target '{idf_target_from_cache_cmdl}' specified on command line is not consistent with "
.format(t_cmdl=idf_target_from_cache_cmdl, t_env=idf_target_from_env)) f"target '{idf_target_from_env}' in the environment."
)
elif idf_target_from_cache_cmdl: elif idf_target_from_cache_cmdl:
# Check if -DIDF_TARGET is consistent with target in CMakeCache.txt # Check if -DIDF_TARGET is consistent with target in CMakeCache.txt
if idf_target_from_cache and idf_target_from_cache != idf_target_from_cache_cmdl: if idf_target_from_cache and idf_target_from_cache != idf_target_from_cache_cmdl:
raise FatalError("Target '{t_cmdl}' specified on command line is not consistent with " raise FatalError(
"target '{t_cache}' in CMakeCache.txt. Run '{prog} set-target {t_cmdl}' to re-generate " f"Target '{idf_target_from_cache_cmdl}' specified on command line is not consistent with "
'CMakeCache.txt.' f"target '{idf_target_from_cache}' in CMakeCache.txt. "
.format(t_cache=idf_target_from_cache, t_cmdl=idf_target_from_cache_cmdl, prog=prog_name)) f"Run '{prog_name} set-target {idf_target_from_cache_cmdl}' to re-generate "
'CMakeCache.txt.'
)
elif idf_target_from_cache: elif idf_target_from_cache:
# This shouldn't happen, unless the user manually edits CMakeCache.txt or sdkconfig, but let's check anyway. # This shouldn't happen, unless the user manually edits CMakeCache.txt or sdkconfig, but let's check anyway.
if idf_target_from_sdkconfig and idf_target_from_cache != idf_target_from_sdkconfig: if idf_target_from_sdkconfig and idf_target_from_cache != idf_target_from_sdkconfig:
raise FatalError("Project sdkconfig '{cfg}' was generated for target '{t_conf}', but CMakeCache.txt contains '{t_cache}'. " raise FatalError(
"To keep the setting in sdkconfig ({t_conf}) and re-generate CMakeCache.txt, run '{prog} fullclean'. " f"Project sdkconfig '{sdkconfig}' was generated for target '{idf_target_from_sdkconfig}', but "
"To re-generate sdkconfig for '{t_cache}' target, run '{prog} set-target {t_cache}'." f"CMakeCache.txt contains '{idf_target_from_cache}'. To keep the setting in sdkconfig "
.format(cfg=sdkconfig, t_conf=idf_target_from_sdkconfig, t_cache=idf_target_from_cache, prog=prog_name)) f"({idf_target_from_sdkconfig}) and re-generate CMakeCache.txt, run '{prog_name} fullclean'. To "
f"re-generate sdkconfig for '{idf_target_from_cache}' target, run '{prog_name} set-target "
f"{idf_target_from_cache}'."
)
class TargetChoice(click.Choice): class TargetChoice(click.Choice):
@@ -786,8 +851,9 @@ class TargetChoice(click.Choice):
- ignores hyphens - ignores hyphens
- not case sensitive - not case sensitive
""" """
def __init__(self, choices: List) -> None:
super(TargetChoice, self).__init__(choices, case_sensitive=False) def __init__(self, choices: list) -> None:
super().__init__(choices, case_sensitive=False)
def convert(self, value: Any, param: click.Parameter, ctx: click.Context) -> Any: def convert(self, value: Any, param: click.Parameter, ctx: click.Context) -> Any:
def normalize(string: str) -> str: def normalize(string: str) -> str:
@@ -797,7 +863,7 @@ class TargetChoice(click.Choice):
ctx.token_normalize_func = normalize ctx.token_normalize_func = normalize
try: try:
return super(TargetChoice, self).convert(value, param, ctx) return super().convert(value, param, ctx)
finally: finally:
ctx.token_normalize_func = saved_token_normalize_func ctx.token_normalize_func = saved_token_normalize_func
@@ -807,7 +873,7 @@ class PropertyDict(dict):
if name in self: if name in self:
return self[name] return self[name]
else: else:
raise AttributeError("'PropertyDict' object has no attribute '%s'" % name) raise AttributeError(f"'PropertyDict' object has no attribute '{name}'")
def __setattr__(self, name: str, value: Any) -> None: def __setattr__(self, name: str, value: Any) -> None:
self[name] = value self[name] = value
@@ -816,4 +882,4 @@ class PropertyDict(dict):
if name in self: if name in self:
del self[name] del self[name]
else: else:
raise AttributeError("'PropertyDict' object has no attribute '%s'" % name) raise AttributeError(f"'PropertyDict' object has no attribute '{name}'")