mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
feat(tools): adopt esp-pylib for idf.py logging and errors
Migrate idf.py and its actions to the shared esp-pylib library: - add esp-pylib (and esp-pylib[ide]) to the core/ide requirements - base FatalError on esp_pylib.errors.FatalError, keeping the idf.py ctx cleanup - replace the local raw-ANSI helpers (red_print/yellow_print/print_warning/ color_print) with esp_pylib.logger.log (warn/err/note/hint) across the idf.py actions - install esp_pylib exception reporting at the idf.py entry point and silence the logger during shell completion - update affected tests for the new prefixes/line wrapping - add normalize_output() helper and unify terminal env in conftest to handle Rich line-wrapping in CI assertions Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
BOT
co-authored by
Cursor
parent
055ba9d3f9
commit
8c08c75f48
@@ -19,6 +19,8 @@ from typing import cast
|
||||
import rich_click as click
|
||||
import yaml
|
||||
from esp_idf_monitor import get_ansi_converter
|
||||
from esp_pylib.logger import log
|
||||
from rich.markup import escape
|
||||
|
||||
from idf_py_actions.errors import NoSerialPortFoundError
|
||||
|
||||
@@ -35,6 +37,12 @@ SHELL_COMPLETE_VAR = '_IDF.PY_COMPLETE'
|
||||
# was shell completion invoked?
|
||||
SHELL_COMPLETE_RUN = SHELL_COMPLETE_VAR in os.environ
|
||||
|
||||
# During shell completion idf.py must stay silent: any diagnostic written to stdout/stderr
|
||||
# corrupts the completion output. Silence the shared logger once instead of guarding every
|
||||
# call site (this is what the old print_warning() helper did via its SHELL_COMPLETE_RUN check).
|
||||
if SHELL_COMPLETE_RUN:
|
||||
log.set_verbosity('SILENT')
|
||||
|
||||
|
||||
# The ctx dict "abuses" how python evaluates default parameter values.
|
||||
# https://docs.python.org/3/reference/compound_stmts.html#function-definitions
|
||||
@@ -91,7 +99,7 @@ def idf_version_from_cmake() -> str | None:
|
||||
|
||||
return f'v{ver["MAJOR"]}.{ver["MINOR"]}.{ver["PATCH"]}'
|
||||
except (KeyError, OSError):
|
||||
sys.stderr.write('WARNING: Cannot find ESP-IDF version in version.cmake\n')
|
||||
log.warn('Cannot find ESP-IDF version in version.cmake')
|
||||
return None
|
||||
|
||||
|
||||
@@ -123,7 +131,7 @@ def idf_version() -> str | None:
|
||||
)
|
||||
except Exception:
|
||||
# if failed, then try to parse cmake.version file
|
||||
sys.stderr.write('WARNING: Git version unavailable, reading from source\n')
|
||||
log.warn('Git version unavailable, reading from source')
|
||||
version = idf_version_from_cmake()
|
||||
|
||||
return version
|
||||
@@ -156,32 +164,8 @@ def get_default_serial_port() -> Any:
|
||||
raise FatalError(f'An exception occurred during detection of the serial port: {e}')
|
||||
|
||||
|
||||
# function prints warning when autocompletion is not being performed
|
||||
# set argument stream to sys.stderr for errors and exceptions
|
||||
def print_warning(message: str, stream: TextIO | None = None) -> None:
|
||||
if not SHELL_COMPLETE_RUN:
|
||||
print(message, file=stream or sys.stderr)
|
||||
|
||||
|
||||
def color_print(message: str, color: str, newline: str | None = '\n') -> None:
|
||||
"""Print a message to stderr with colored highlighting"""
|
||||
ansi_normal = '\033[0m'
|
||||
sys.stderr.write(f'{color}{message}{ansi_normal}{newline}')
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
def yellow_print(message: str, newline: str | None = '\n') -> None:
|
||||
ansi_yellow = '\033[0;33m'
|
||||
color_print(message, ansi_yellow, newline)
|
||||
|
||||
|
||||
def red_print(message: str, newline: str | None = '\n') -> None:
|
||||
ansi_red = '\033[1;31m'
|
||||
color_print(message, ansi_red, newline)
|
||||
|
||||
|
||||
def debug_print_idf_version() -> None:
|
||||
print_warning(f'ESP-IDF {idf_version() or "version unknown"}')
|
||||
log.note(f'ESP-IDF {idf_version() or "version unknown"}')
|
||||
|
||||
|
||||
def _load_hints_from_directory(directory: str) -> list:
|
||||
@@ -195,7 +179,7 @@ def _load_hints_from_directory(directory: str) -> list:
|
||||
hints = yaml.safe_load(file)
|
||||
return hints if hints else []
|
||||
except (OSError, yaml.YAMLError):
|
||||
yellow_print(f'HINT WARNING: Failed to load hints from "{hints_file}"')
|
||||
log.warn(escape(f'Failed to load hints from "{hints_file}"'))
|
||||
return []
|
||||
|
||||
|
||||
@@ -232,11 +216,9 @@ def _load_idf_hints() -> dict:
|
||||
try:
|
||||
hints['modules'].append(getattr(importlib.import_module(name), 'generate_hint'))
|
||||
except ModuleNotFoundError:
|
||||
red_print(f'Failed to import "{name}" from "{hint_modules_dir}" as a module')
|
||||
raise SystemExit(1)
|
||||
log.die(escape(f'Failed to import "{name}" from "{hint_modules_dir}" as a module'))
|
||||
except AttributeError:
|
||||
red_print(f'Module "{name}" does not have function generate_hint.')
|
||||
raise SystemExit(1)
|
||||
log.die(escape(f'Module "{name}" does not have function generate_hint.'))
|
||||
|
||||
# Load ESP-IDF components
|
||||
idf_path = os.environ.get('IDF_PATH')
|
||||
@@ -304,23 +286,19 @@ def generate_hints_buffer(output: str, hints: dict) -> Generator:
|
||||
try:
|
||||
hint_list.append(hint['hint'].format(*hint_vars))
|
||||
except KeyError as e:
|
||||
red_print(f'Argument {e} missing in {hint}. Check hints.yml file.')
|
||||
sys.exit(1)
|
||||
log.die(escape(f'Argument {e} missing in {hint}. Check hints.yml file.'))
|
||||
else:
|
||||
match = re.compile(hint['re']).search(output)
|
||||
except KeyError as e:
|
||||
red_print(f'Argument {e} missing in {hint}. Check hints.yml file.')
|
||||
sys.exit(1)
|
||||
log.die(escape(f'Argument {e} missing in {hint}. Check hints.yml file.'))
|
||||
except re.error as e:
|
||||
red_print('{} from hints.yml have {} problem. Check hints.yml file.'.format(hint['re'], e))
|
||||
sys.exit(1)
|
||||
log.die(escape('{} from hints.yml have {} problem. Check hints.yml file.'.format(hint['re'], e)))
|
||||
if hint_list:
|
||||
for message in hint_list:
|
||||
yield ' '.join(['HINT:', message])
|
||||
yield from hint_list
|
||||
elif match:
|
||||
extra_info = ', '.join(match.groups()) if hint.get('match_to_output', '') else ''
|
||||
try:
|
||||
yield ' '.join(['HINT:', hint['hint'].format(extra_info)])
|
||||
yield hint['hint'].format(extra_info)
|
||||
except KeyError:
|
||||
raise KeyError(f"Argument 'hint' missing in {hint}. Check hints.yml file.")
|
||||
|
||||
@@ -440,7 +418,7 @@ class RunTool:
|
||||
# hints in interactive mode were already processed, don't print them again
|
||||
if not self.interactive:
|
||||
for hint in generate_hints(stderr_output_file, stdout_output_file):
|
||||
yellow_print(hint)
|
||||
log.hint(escape(hint))
|
||||
raise FatalError(
|
||||
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}'
|
||||
@@ -469,15 +447,14 @@ class RunTool:
|
||||
)
|
||||
except NotImplementedError:
|
||||
message = (
|
||||
f"ERROR: {sys.executable} doesn't support asyncio. "
|
||||
"Workaround: re-run idf.py with the '--no-hints' argument."
|
||||
f"{sys.executable} doesn't support asyncio. Workaround: re-run idf.py with the '--no-hints' argument."
|
||||
)
|
||||
if sys.platform == 'win32':
|
||||
message += (
|
||||
' To fix the issue use the Windows Installer for setting up your python environment, '
|
||||
'available from: https://dl.espressif.com/dl/esp-idf/'
|
||||
)
|
||||
sys.exit(message)
|
||||
log.die(escape(message))
|
||||
|
||||
stderr_output_file = os.path.join(self.build_dir, log_dir_name, f'idf_py_stderr_output_{p.pid}')
|
||||
stdout_output_file = os.path.join(self.build_dir, log_dir_name, f'idf_py_stdout_output_{p.pid}')
|
||||
@@ -495,7 +472,7 @@ class RunTool:
|
||||
# the even loop is closed and we get RuntimeError: Event loop is closed
|
||||
# in the transport __del__ function because it's trying to use the closed
|
||||
# even loop.
|
||||
red_print(f'\n{self.tool_name} process terminated\n')
|
||||
log.err(f'\n{self.tool_name} process terminated')
|
||||
await p.wait() # added for avoiding None returncode
|
||||
return p, stderr_output_file, stdout_output_file
|
||||
|
||||
@@ -612,7 +589,7 @@ class RunTool:
|
||||
last_line += output
|
||||
if last_line[-1] == '\n':
|
||||
for hint in generate_hints_buffer(last_line, hints):
|
||||
yellow_print(hint)
|
||||
log.hint(escape(hint))
|
||||
last_line = ''
|
||||
else:
|
||||
output_b = await read_stream_bytes()
|
||||
@@ -641,9 +618,11 @@ class RunTool:
|
||||
else:
|
||||
write_stdout_bytes(forward_b)
|
||||
except (OSError, RuntimeError) as e:
|
||||
yellow_print(
|
||||
"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('<>'))
|
||||
log.warn(
|
||||
escape(
|
||||
"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('<>'))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -936,16 +915,14 @@ def merge_action_lists(*action_lists: dict, custom_actions: dict[str, Any] | Non
|
||||
existing_identifiers.add(name)
|
||||
existing_identifiers.update(action.get('aliases', []))
|
||||
except UserWarning as e:
|
||||
yellow_print(f'WARNING: {e}. External action will not be added.')
|
||||
log.warn(escape(f'{e}. External action will not be added.'))
|
||||
|
||||
for new_opt in custom_actions.get('global_options', []):
|
||||
if any(
|
||||
set(new_opt.get('names', [])) & set(existing.get('names', []))
|
||||
for existing in merged_actions['global_options']
|
||||
):
|
||||
yellow_print(
|
||||
f'WARNING: Global option {new_opt["names"]} already defined. External option will not be added.'
|
||||
)
|
||||
log.warn(escape(f'Global option {new_opt["names"]} already defined. External option will not be added.'))
|
||||
else:
|
||||
merged_actions['global_options'].append(new_opt)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user