Skip to content

Commit 4a82c9c

Browse files
fix: Windows Unicode encoding error in CLI help command (fixes #1543) (#1545)
* fix: Windows Unicode encoding error in CLI help command (fixes #1543) - Detect Windows legacy encoding (CP1252, CP850, etc.) in Rich Console initialization - Create console with safe Windows options: legacy_windows=True, safe_box=True, emoji=False - Set PYTHONIOENCODING=utf-8 for subprocess safety - Add UnicodeEncodeError handling in Typer runner with helpful error message - Add ASCII fallback in print methods for encoding failures This ensures 'praisonai --help' works on Windows default terminals without crashing. Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com> * fix: address critical encoding detection and UnicodeEncodeError protection issues - Fix overly broad 'cp' in encoding check to exclude cp65001 (Windows UTF-8 codepage) - Add UnicodeEncodeError protection to all print_* methods with emoji literals - Change --help exit code from 1 to 0 on encoding failure (conventional behavior) - Add fallback handling for Rich console failures in all output methods Resolves P1 issues identified by code reviewers while maintaining backward compatibility. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com> --------- Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
1 parent 77381f1 commit 4a82c9c

2 files changed

Lines changed: 116 additions & 15 deletions

File tree

src/praisonai/praisonai/__main__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,28 @@ def _find_first_command(argv):
7272

7373
def _run_typer(argv):
7474
"""Dispatch to the Typer CLI app."""
75+
import os
76+
77+
# Set up safer encoding for Windows legacy terminals
78+
if sys.platform == "win32" and hasattr(sys.stdout, 'encoding'):
79+
encoding = getattr(sys.stdout, 'encoding', '').lower()
80+
if encoding in ('cp1252', 'cp1251', 'cp850', 'ascii') or ('cp' in encoding and encoding != 'cp65001'):
81+
# Force UTF-8 mode for subprocess safety
82+
if 'PYTHONIOENCODING' not in os.environ:
83+
os.environ['PYTHONIOENCODING'] = 'utf-8'
84+
7585
from praisonai.cli.app import app, register_commands
7686
register_commands() # idempotent
7787

7888
original = sys.argv
7989
sys.argv = ["praisonai"] + list(argv)
8090
try:
8191
app()
92+
except UnicodeEncodeError as e:
93+
# Handle Unicode encoding errors gracefully
94+
print("Error: Unable to display help due to terminal encoding limitations.", file=sys.stderr)
95+
print("Try setting: $env:PYTHONIOENCODING='utf-8' (PowerShell) or set PYTHONIOENCODING=utf-8 (cmd)", file=sys.stderr)
96+
sys.exit(0)
8297
except SystemExit as e:
8398
sys.exit(e.code if isinstance(e.code, int) else 0)
8499
finally:

src/praisonai/praisonai/cli/output/console.py

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,36 @@ def _get_console():
4040
global _console
4141
if _console is None and _get_rich_available():
4242
from rich.console import Console
43-
_console = Console()
43+
import sys
44+
import os
45+
46+
# Detect Windows legacy encoding (CP1252) and use safe fallback
47+
if sys.platform == "win32" and hasattr(sys.stdout, 'encoding'):
48+
encoding = getattr(sys.stdout, 'encoding', '').lower()
49+
# Check for Windows legacy code pages that can't handle Unicode
50+
if encoding in ('cp1252', 'cp1251', 'cp850', 'ascii') or ('cp' in encoding and encoding != 'cp65001'):
51+
# Force UTF-8 mode or create console with safe encoding handling
52+
try:
53+
# Set PYTHONIOENCODING to utf-8 for subprocess safety (won't affect current process)
54+
if 'PYTHONIOENCODING' not in os.environ:
55+
os.environ['PYTHONIOENCODING'] = 'utf-8'
56+
57+
# Create console with safe encoding options
58+
_console = Console(
59+
force_terminal=True,
60+
legacy_windows=True, # Use legacy Windows mode
61+
safe_box=True, # Use safe box characters
62+
emoji=False, # Disable emojis on Windows legacy
63+
color_system='standard', # Use basic colors
64+
_environ=os.environ # Pass updated environment
65+
)
66+
except Exception:
67+
# Fallback to basic console if Rich options fail
68+
_console = Console(force_terminal=False, no_color=True)
69+
else:
70+
_console = Console()
71+
else:
72+
_console = Console()
4473
return _console
4574

4675

@@ -137,6 +166,19 @@ def is_screen_reader(self) -> bool:
137166
"""Check if in screen reader mode."""
138167
return self.mode == OutputMode.SCREEN_READER
139168

169+
def _safe_console_print(self, *args, **kwargs) -> None:
170+
"""Safely print to console with UnicodeEncodeError protection."""
171+
if self.console:
172+
try:
173+
self.console.print(*args, **kwargs)
174+
except UnicodeEncodeError:
175+
# Fallback to plain text if Rich can't handle the encoding
176+
message = str(args[0]) if args else ""
177+
print(message.encode('ascii', 'replace').decode('ascii'))
178+
else:
179+
message = str(args[0]) if args else ""
180+
print(message)
181+
140182
def print(self, message: str, style: Optional[str] = None, **kwargs) -> None:
141183
"""Print a message respecting the current mode."""
142184
if self.is_quiet:
@@ -150,11 +192,8 @@ def print(self, message: str, style: Optional[str] = None, **kwargs) -> None:
150192
# Plain text output
151193
print(message)
152194
else:
153-
# Rich formatted output
154-
if self.console:
155-
self.console.print(message, style=style, **kwargs)
156-
else:
157-
print(message)
195+
# Rich formatted output with encoding safety
196+
self._safe_console_print(message, style=style, **kwargs)
158197

159198
def print_error(self, message: str, code: Optional[str] = None, remediation: Optional[str] = None) -> None:
160199
"""Print an error message with optional remediation."""
@@ -187,10 +226,16 @@ def print_error(self, message: str, code: Optional[str] = None, remediation: Opt
187226
if remediation:
188227
content.append(f"\n💡 Fix: {remediation}", style="yellow")
189228

190-
if self.console:
191-
self.console.print(Panel(content, title="Error", border_style="red"))
192-
else:
229+
try:
230+
if self.console:
231+
self.console.print(Panel(content, title="Error", border_style="red"))
232+
else:
233+
print(f"ERROR: {message}", file=sys.stderr)
234+
except UnicodeEncodeError:
235+
# Fallback to plain text if emoji rendering fails
193236
print(f"ERROR: {message}", file=sys.stderr)
237+
if remediation:
238+
print(f"FIX: {remediation}", file=sys.stderr)
194239

195240
def print_success(self, message: str, data: Optional[Dict[str, Any]] = None) -> None:
196241
"""Print a success message."""
@@ -214,7 +259,10 @@ def print_success(self, message: str, data: Optional[Dict[str, Any]] = None) ->
214259
if self.is_screen_reader or self.no_color or not _get_rich_available():
215260
print(f"SUCCESS: {message}")
216261
else:
217-
self.print(f"✅ {message}", style="bold green")
262+
try:
263+
self.print(f"✅ {message}", style="bold green")
264+
except UnicodeEncodeError:
265+
print(f"SUCCESS: {message}")
218266

219267
def print_warning(self, message: str) -> None:
220268
"""Print a warning message."""
@@ -224,7 +272,10 @@ def print_warning(self, message: str) -> None:
224272
if self.is_screen_reader or self.no_color or not _get_rich_available():
225273
print(f"WARNING: {message}")
226274
else:
227-
self.print(f"⚠️ {message}", style="bold yellow")
275+
try:
276+
self.print(f"⚠️ {message}", style="bold yellow")
277+
except UnicodeEncodeError:
278+
print(f"WARNING: {message}")
228279

229280
def print_info(self, message: str) -> None:
230281
"""Print an info message."""
@@ -234,7 +285,10 @@ def print_info(self, message: str) -> None:
234285
if self.is_screen_reader or self.no_color or not _get_rich_available():
235286
print(f"INFO: {message}")
236287
else:
237-
self.print(f"ℹ️ {message}", style="bold blue")
288+
try:
289+
self.print(f"ℹ️ {message}", style="bold blue")
290+
except UnicodeEncodeError:
291+
print(f"INFO: {message}")
238292

239293
def print_debug(self, message: str) -> None:
240294
"""Print a debug message (only in verbose mode)."""
@@ -244,7 +298,10 @@ def print_debug(self, message: str) -> None:
244298
if self.is_screen_reader or self.no_color or not _get_rich_available():
245299
print(f"DEBUG: {message}")
246300
else:
247-
self.print(f"🔍 {message}", style="dim")
301+
try:
302+
self.print(f"🔍 {message}", style="dim")
303+
except UnicodeEncodeError:
304+
print(f"DEBUG: {message}")
248305

249306
def emit_event(self, event_type: str, message: Optional[str] = None, data: Optional[Dict[str, Any]] = None, agent_id: Optional[str] = None) -> None:
250307
"""Emit a stream event (for stream-json mode)."""
@@ -321,7 +378,28 @@ def print_table(self, headers: List[str], rows: List[List[Any]], title: Optional
321378
table.add_row(*[str(cell) for cell in row])
322379

323380
if self.console:
324-
self.console.print(table)
381+
try:
382+
self.console.print(table)
383+
except UnicodeEncodeError:
384+
# Fallback to plain text table if Rich fails
385+
if title:
386+
print(f"\n{title}")
387+
print("-" * len(title))
388+
389+
# Calculate column widths
390+
widths = [len(h) for h in headers]
391+
for row in rows:
392+
for i, cell in enumerate(row):
393+
widths[i] = max(widths[i], len(str(cell)))
394+
395+
# Print header
396+
header_line = " | ".join(h.ljust(widths[i]) for i, h in enumerate(headers))
397+
print(header_line)
398+
print("-" * len(header_line))
399+
400+
# Print rows
401+
for row in rows:
402+
print(" | ".join(str(cell).ljust(widths[i]) for i, cell in enumerate(row)))
325403

326404
def print_panel(self, content: str, title: Optional[str] = None, style: str = "cyan") -> None:
327405
"""Print a panel."""
@@ -338,7 +416,15 @@ def print_panel(self, content: str, title: Optional[str] = None, style: str = "c
338416
from rich.panel import Panel
339417

340418
if self.console:
341-
self.console.print(Panel(content, title=title, border_style=style))
419+
try:
420+
self.console.print(Panel(content, title=title, border_style=style))
421+
except UnicodeEncodeError:
422+
# Fallback to plain text if panel rendering fails
423+
if title:
424+
print(f"\n=== {title} ===")
425+
print(content)
426+
if title:
427+
print("=" * (len(title) + 8))
342428

343429
def get_events(self) -> List[Dict[str, Any]]:
344430
"""Get all collected events."""

0 commit comments

Comments
 (0)