Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions studio/install_python_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,38 @@
SCRIPT_DIR / "backend" / "plugins" / "data-designer-unstructured-seed"
)

# ── Unicode-safe printing ─────────────────────────────────────────────
# On Windows the default console encoding can be a legacy code page
# (e.g. CP1252) that cannot represent Unicode glyphs such as ✅ or ❌.
# _safe_print() gracefully degrades to ASCII equivalents so the
# installer never crashes just because of a status glyph.

_UNICODE_TO_ASCII: dict[str, str] = {
"\u2705": "[OK]", # ✅
"\u274c": "[FAIL]", # ❌
"\u26a0\ufe0f": "[!]", # ⚠️ (warning + variation selector)
"\u26a0": "[!]", # ⚠ (warning without variation selector)
}


def _safe_print(*args: object, **kwargs: object) -> None:
"""Drop-in print() replacement that survives non-UTF-8 consoles."""
try:
print(*args, **kwargs)
except UnicodeEncodeError:
# Stringify, then swap emoji for ASCII equivalents
text = " ".join(str(a) for a in args)
for uni, ascii_alt in _UNICODE_TO_ASCII.items():
text = text.replace(uni, ascii_alt)
# Final fallback: replace any remaining unencodable chars
print(
text.encode(sys.stdout.encoding or "ascii", errors = "replace").decode(
sys.stdout.encoding or "ascii", errors = "replace"
Comment on lines +61 to +73

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To make _safe_print a more robust "drop-in replacement" for print() as the docstring states, I suggest a small improvement. The current implementation in the except block doesn't handle keyword arguments like sep correctly. It joins all arguments with a space and then passes the original kwargs to a print call with a single argument, where sep has no effect.

A more robust approach is to process each argument individually to make it safe, and then pass the safe arguments to print. This preserves the behavior of all keyword arguments like sep, end, etc.

Suggested change
def _safe_print(*args: object, **kwargs: object) -> None:
"""Drop-in print() replacement that survives non-UTF-8 consoles."""
try:
print(*args, **kwargs)
except UnicodeEncodeError:
# Stringify, then swap emoji for ASCII equivalents
text = " ".join(str(a) for a in args)
for uni, ascii_alt in _UNICODE_TO_ASCII.items():
text = text.replace(uni, ascii_alt)
# Final fallback: replace any remaining unencodable chars
print(
text.encode(sys.stdout.encoding or "ascii", errors = "replace").decode(
sys.stdout.encoding or "ascii", errors = "replace"
def _safe_print(*args: object, **kwargs: object) -> None:
"""Drop-in print() replacement that survives non-UTF-8 consoles."""
try:
print(*args, **kwargs)
except UnicodeEncodeError:
# Sanitize each argument individually to preserve kwargs like 'sep'
safe_args = []
for arg in args:
s = str(arg)
for uni, ascii_alt in _UNICODE_TO_ASCII.items():
s = s.replace(uni, ascii_alt)
safe_args.append(
s.encode(sys.stdout.encoding or "ascii", errors="replace").decode(
sys.stdout.encoding or "ascii", errors="replace"
)
)
print(*safe_args, **kwargs)

),
**kwargs,
)


# ── Color support ──────────────────────────────────────────────────────


Expand Down Expand Up @@ -119,7 +151,7 @@ def run(
stderr = subprocess.STDOUT if quiet else None,
)
if result.returncode != 0:
print(_red(f"❌ {label} failed (exit code {result.returncode}):"))
_safe_print(_red(f"❌ {label} failed (exit code {result.returncode}):"))
if result.stdout:
print(result.stdout.decode(errors = "replace"))
sys.exit(result.returncode)
Expand Down Expand Up @@ -267,7 +299,9 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None:
text = True,
)
if result.returncode != 0:
print(_red(f" ⚠️ Could not find package {package_name}, skipping patch"))
_safe_print(
_red(f" ⚠️ Could not find package {package_name}, skipping patch")
)
return

location = None
Expand All @@ -277,7 +311,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None:
break

if not location:
print(_red(f" ⚠️ Could not determine location of {package_name}"))
_safe_print(_red(f" ⚠️ Could not determine location of {package_name}"))
return

dest = Path(location) / relative_path
Expand Down Expand Up @@ -393,7 +427,7 @@ def install_python_stack() -> int:

# 11. Local Data Designer seed plugin
if not LOCAL_DD_UNSTRUCTURED_PLUGIN.is_dir():
print(
_safe_print(
_red(
f"❌ Missing local plugin directory: {LOCAL_DD_UNSTRUCTURED_PLUGIN}",
),
Expand Down Expand Up @@ -422,7 +456,7 @@ def install_python_stack() -> int:
stderr = subprocess.DEVNULL,
)

print(_green("✅ Python dependencies installed"))
_safe_print(_green("✅ Python dependencies installed"))
return 0


Expand Down