mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-08-01 19:34:24 +03:00
fix(cli): report an unusable terminal instead of a prompt_toolkit traceback
- Windows terminals without a console buffer raised NoConsoleScreenBufferError before the first prompt, surfacing a raw traceback with no guidance - gate the Windows-only import on sys.platform so a broken prompt_toolkit still surfaces there, and the handler stays inert on other platforms #1138
This commit is contained in:
25
cli/main.py
25
cli/main.py
@@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -52,6 +53,18 @@ from tradingagents.reporting import write_report_tree
|
|||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
# prompt_toolkit's win32 output module is importable only on Windows (it asserts
|
||||||
|
# the platform at import time), so gate on the platform rather than catching the
|
||||||
|
# failure — that way a genuinely broken prompt_toolkit on Windows still surfaces
|
||||||
|
# instead of silently disabling the handler below. Off Windows this stays an
|
||||||
|
# empty tuple, which `except` accepts and never matches (#1138).
|
||||||
|
if sys.platform == "win32": # pragma: no cover - platform dependent
|
||||||
|
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
|
||||||
|
|
||||||
|
_NO_CONSOLE_ERRORS: tuple[type[BaseException], ...] = (NoConsoleScreenBufferError,)
|
||||||
|
else:
|
||||||
|
_NO_CONSOLE_ERRORS = ()
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
name="TradingAgents",
|
name="TradingAgents",
|
||||||
help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework",
|
help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework",
|
||||||
@@ -1285,7 +1298,19 @@ def analyze(
|
|||||||
from tradingagents.graph.checkpointer import clear_all_checkpoints
|
from tradingagents.graph.checkpointer import clear_all_checkpoints
|
||||||
n = clear_all_checkpoints(DEFAULT_CONFIG["data_cache_dir"])
|
n = clear_all_checkpoints(DEFAULT_CONFIG["data_cache_dir"])
|
||||||
console.print(f"[yellow]Cleared {n} checkpoint(s).[/yellow]")
|
console.print(f"[yellow]Cleared {n} checkpoint(s).[/yellow]")
|
||||||
|
try:
|
||||||
run_analysis(checkpoint=checkpoint)
|
run_analysis(checkpoint=checkpoint)
|
||||||
|
except _NO_CONSOLE_ERRORS:
|
||||||
|
# A terminal with no console buffer cannot host the interactive prompts.
|
||||||
|
# Emit one actionable line on stderr instead of a prompt_toolkit
|
||||||
|
# traceback; plain text, since rich may not render here either (#1138).
|
||||||
|
typer.echo(
|
||||||
|
"Error: no Windows console available. The interactive CLI needs a real "
|
||||||
|
"console buffer — run it from Windows Terminal, PowerShell, or cmd.exe "
|
||||||
|
"rather than a piped or embedded terminal.",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
raise typer.Exit(code=1) from None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
57
tests/test_cli_no_console.py
Normal file
57
tests/test_cli_no_console.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""A terminal without a console buffer must fail with one actionable line (#1138).
|
||||||
|
|
||||||
|
prompt_toolkit raises NoConsoleScreenBufferError before the first prompt in
|
||||||
|
non-interactive Windows terminals; the CLI should not surface that traceback.
|
||||||
|
The Windows-only exception import must also stay inert on other platforms.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
import cli.main as m
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_console_error_tuple_matches_platform():
|
||||||
|
# Off Windows the win32 module is never imported (it asserts the platform),
|
||||||
|
# so the tuple is empty — which `except` accepts and never matches. On
|
||||||
|
# Windows it holds the real exception type, and a broken prompt_toolkit
|
||||||
|
# would raise at import rather than silently disabling the handler.
|
||||||
|
assert isinstance(m._NO_CONSOLE_ERRORS, tuple)
|
||||||
|
assert all(issubclass(e, BaseException) for e in m._NO_CONSOLE_ERRORS)
|
||||||
|
if sys.platform == "win32":
|
||||||
|
assert m._NO_CONSOLE_ERRORS, "Windows must resolve the console error type"
|
||||||
|
else:
|
||||||
|
assert m._NO_CONSOLE_ERRORS == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_console_prints_actionable_message(monkeypatch):
|
||||||
|
class _NoConsole(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Simulate the Windows failure on any platform by registering a stand-in.
|
||||||
|
monkeypatch.setattr(m, "_NO_CONSOLE_ERRORS", (_NoConsole,))
|
||||||
|
|
||||||
|
def _boom(*a, **k):
|
||||||
|
raise _NoConsole("No Windows console found. Are you running cmd.exe?")
|
||||||
|
|
||||||
|
monkeypatch.setattr(m, "run_analysis", _boom)
|
||||||
|
|
||||||
|
result = CliRunner().invoke(m.app, [])
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "no Windows console available" in result.output
|
||||||
|
# The raw prompt_toolkit traceback must not reach the user.
|
||||||
|
assert "Traceback" not in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_unrelated_errors_still_propagate(monkeypatch):
|
||||||
|
# The handler must stay narrow: only the console error is translated.
|
||||||
|
monkeypatch.setattr(m, "_NO_CONSOLE_ERRORS", (RuntimeError,))
|
||||||
|
|
||||||
|
def _boom(*a, **k):
|
||||||
|
raise ValueError("unrelated")
|
||||||
|
|
||||||
|
monkeypatch.setattr(m, "run_analysis", _boom)
|
||||||
|
result = CliRunner().invoke(m.app, [])
|
||||||
|
assert isinstance(result.exception, ValueError)
|
||||||
Reference in New Issue
Block a user