-
-
Notifications
You must be signed in to change notification settings - Fork 5.9k
studio: setup log styling #4494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
08b3dae
234a94d
b4e6ba3
de93634
38516de
9a691e4
d11dc31
67a997f
67fda0c
7ed5b69
28981e5
86490e7
630d6a9
83cab0c
331edae
5daeba5
07850d7
f3f7dbf
35ef245
81a963a
3613325
e0de157
7dcd7d8
2a7c581
b15dc8f
3e05a2a
618b63c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 | ||
|
|
||
| """Terminal banner for Studio startup. | ||
|
|
||
| Stdlib only — safe to import without the rest of the backend (no structlog/uvicorn). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import sys | ||
|
|
||
|
|
||
| def stdout_supports_color() -> bool: | ||
| """True if we should emit ANSI colors.""" | ||
| if os.environ.get("NO_COLOR", "").strip(): | ||
| return False | ||
| if os.environ.get("FORCE_COLOR", "").strip(): | ||
| return True | ||
| try: | ||
| return sys.stdout.isatty() | ||
| except Exception: | ||
| return False | ||
|
|
||
|
|
||
| def print_port_in_use_notice(original_port: int, new_port: int) -> None: | ||
| """Message when the requested port is taken and another is chosen.""" | ||
| msg = f"Port {original_port} is in use, using port {new_port} instead." | ||
| if stdout_supports_color(): | ||
| print(f"\033[38;5;245m{msg}\033[0m") | ||
| else: | ||
| print(msg) | ||
|
|
||
|
|
||
| def print_studio_access_banner( | ||
| *, | ||
| port: int, | ||
| bind_host: str, | ||
| display_host: str, | ||
| ) -> None: | ||
| """Pretty-print URLs after the server is listening (beginner-friendly).""" | ||
|
Comment on lines
+15
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency with def stdout_supports_color() -> bool:
"""True if we should emit ANSI colors."""
if os.environ.get("NO_COLOR", "").strip():
return False
if os.environ.get("FORCE_COLOR", "").strip():
return True
try:
if not sys.stdout.isatty():
return False
except Exception as e:
# Log the exception for debugging purposes as per repository rule.
import logging
logging.debug(f"stdout.isatty() check failed: {e}")
return False
if sys.platform == "win32":
try:
import ctypes
kernel32 = ctypes.windll.kernel32
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
mode = ctypes.c_ulong()
kernel32.GetConsoleMode(handle, ctypes.byref(mode))
kernel32.SetConsoleMode(handle, mode.value | 0x0004)
except Exception as e:
# Log the exception for debugging purposes as per repository rule.
import logging
logging.debug(f"Failed to enable VT processing on Windows: {e}")
return False
return TrueReferences
|
||
| use_color = stdout_supports_color() | ||
| dim = "\033[38;5;245m" | ||
| title = "\033[38;5;150m" | ||
| local_url_style = "\033[38;5;108;1m" | ||
| secondary = "\033[38;5;109m" | ||
| reset = "\033[0m" | ||
|
|
||
| def style(text: str, code: str) -> str: | ||
| return f"{code}{text}{reset}" if use_color else text | ||
|
|
||
| ipv6_bind = bind_host in ("::", "::1") | ||
| if ipv6_bind: | ||
| local_url = f"http://[::1]:{port}" | ||
| alt_local = f"http://localhost:{port}" | ||
| else: | ||
| local_url = f"http://127.0.0.1:{port}" | ||
| alt_local = f"http://localhost:{port}" | ||
|
Imagineer99 marked this conversation as resolved.
|
||
| if ":" in display_host: | ||
| external_url = f"http://[{display_host}]:{port}" | ||
| else: | ||
| external_url = f"http://{display_host}:{port}" | ||
| listen_all = bind_host in ("0.0.0.0", "::") | ||
| loopback_bind = bind_host in ("127.0.0.1", "localhost", "::1") | ||
| api_base = local_url if listen_all or loopback_bind else external_url | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
|
|
||
| lines: list[str] = [ | ||
| "", | ||
| style("🦥 Unsloth Studio is running", title), | ||
| style("─" * 52, dim), | ||
| style(" On this machine — open this in your browser:", dim), | ||
| style(f" {local_url}", local_url_style), | ||
| style(f" (same as {alt_local})", dim), | ||
| ] | ||
|
|
||
| if listen_all and display_host not in ( | ||
| "127.0.0.1", | ||
| "localhost", | ||
| "::1", | ||
| "0.0.0.0", | ||
| "::", | ||
| ): | ||
| lines.extend( | ||
| [ | ||
| "", | ||
| style(" From another device on your network / to share:", dim), | ||
| style(f" {external_url}", secondary), | ||
| ] | ||
| ) | ||
| elif not listen_all and bind_host not in ("127.0.0.1", "localhost", "::1"): | ||
| lines.extend( | ||
| [ | ||
| "", | ||
| style(" Bound address:", dim), | ||
| style(f" {external_url}", secondary), | ||
| ] | ||
| ) | ||
|
|
||
| lines.extend( | ||
| [ | ||
| "", | ||
| style(" API & health:", dim), | ||
| style(f" {api_base}/api", secondary), | ||
| style(f" {api_base}/api/health", secondary), | ||
| style("─" * 52, dim), | ||
| style( | ||
| " Tip: if you are on the same computer, use the Local link above.", | ||
| dim, | ||
| ), | ||
| "", | ||
| ] | ||
| ) | ||
|
|
||
| print("\n".join(lines)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,7 @@ def _infer_no_torch() -> bool: | |
| # -- Verbosity control ---------------------------------------------------------- | ||
| # By default the installer shows a minimal progress bar (one line, in-place). | ||
| # Set UNSLOTH_VERBOSE=1 in the environment to restore full per-step output: | ||
| # CLI: unsloth studio setup --verbose | ||
| # Linux/Mac: UNSLOTH_VERBOSE=1 ./studio/setup.sh | ||
| # Windows: $env:UNSLOTH_VERBOSE="1" ; .\studio\setup.ps1 | ||
| VERBOSE: bool = os.environ.get("UNSLOTH_VERBOSE", "0") == "1" | ||
|
|
@@ -96,15 +97,18 @@ def _safe_print(*args: object, **kwargs: object) -> None: | |
| ) | ||
|
|
||
|
|
||
| # -- Color support ------------------------------------------------------ | ||
| # ── Color support ────────────────────────────────────────────────────── | ||
| # Same logic as startup_banner: NO_COLOR disables, FORCE_COLOR or TTY enables. | ||
|
|
||
|
|
||
| def _enable_colors() -> bool: | ||
| """Try to enable ANSI color support. Returns True if available.""" | ||
| if not hasattr(sys.stdout, "fileno"): | ||
| def _stdout_supports_color() -> bool: | ||
| """True if we should emit ANSI colors (matches startup_banner).""" | ||
| if os.environ.get("NO_COLOR", "").strip(): | ||
| return False | ||
| if os.environ.get("FORCE_COLOR", "").strip(): | ||
| return True | ||
| try: | ||
| if not os.isatty(sys.stdout.fileno()): | ||
| if not sys.stdout.isatty(): | ||
| return False | ||
| except Exception: | ||
| return False | ||
|
Comment on lines
113
to
114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The References
|
||
|
|
@@ -113,24 +117,26 @@ def _enable_colors() -> bool: | |
| import ctypes | ||
|
|
||
| kernel32 = ctypes.windll.kernel32 | ||
| # Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x0004) on stdout | ||
| handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE | ||
| handle = kernel32.GetStdHandle(-11) | ||
| mode = ctypes.c_ulong() | ||
| kernel32.GetConsoleMode(handle, ctypes.byref(mode)) | ||
| kernel32.SetConsoleMode(handle, mode.value | 0x0004) | ||
| return True | ||
| except Exception: | ||
| return False | ||
|
Comment on lines
124
to
125
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The References
|
||
| return True # Unix terminals support ANSI by default | ||
| return True | ||
|
|
||
|
|
||
| _HAS_COLOR = _stdout_supports_color() | ||
|
|
||
|
|
||
| # Colors disabled -- Colab and most CI runners render ANSI fine, but plain output | ||
| # is cleaner in the notebook cell. Re-enable by setting _HAS_COLOR = _enable_colors() | ||
| _HAS_COLOR = False | ||
| # Column layout — matches setup.sh step() helper: | ||
| # 2-space indent, 15-char label (dim), then value. | ||
| _LABEL = "deps" | ||
| _COL = 15 | ||
|
|
||
|
|
||
| def _green(msg: str) -> str: | ||
| return f"\033[92m{msg}\033[0m" if _HAS_COLOR else msg | ||
| return f"\033[38;5;108m{msg}\033[0m" if _HAS_COLOR else msg | ||
|
|
||
|
|
||
| def _cyan(msg: str) -> str: | ||
|
|
@@ -141,21 +147,39 @@ def _red(msg: str) -> str: | |
| return f"\033[91m{msg}\033[0m" if _HAS_COLOR else msg | ||
|
|
||
|
|
||
| def _progress(label: str) -> None: | ||
| """Print an in-place progress bar for the current install step. | ||
| def _dim(msg: str) -> str: | ||
| return f"\033[38;5;245m{msg}\033[0m" if _HAS_COLOR else msg | ||
|
|
||
| Uses only stdlib (sys.stdout) -- no extra packages required. | ||
| In VERBOSE mode this is a no-op; per-step labels are printed by run() instead. | ||
| """ | ||
|
|
||
| def _title(msg: str) -> str: | ||
| return f"\033[38;5;150m{msg}\033[0m" if _HAS_COLOR else msg | ||
|
|
||
|
|
||
| _RULE = "\u2500" * 52 | ||
|
|
||
|
|
||
| def _step(label: str, value: str, color_fn = None) -> None: | ||
| """Print a single step line in the column format.""" | ||
| if color_fn is None: | ||
| color_fn = _green | ||
| padded = label[:_COL] | ||
| print(f" {_dim(padded)}{' ' * (_COL - len(padded))}{color_fn(value)}") | ||
|
|
||
|
|
||
| def _progress(label: str) -> None: | ||
| """Print an in-place progress bar aligned to the step column layout.""" | ||
| global _STEP | ||
| _STEP += 1 | ||
| if VERBOSE: | ||
| return # verbose mode: run() already printed the label | ||
| return | ||
| width = 20 | ||
| filled = int(width * _STEP / _TOTAL) | ||
| bar = "=" * filled + "-" * (width - filled) | ||
| end = "\n" if _STEP >= _TOTAL else "" # newline only on the final step | ||
| sys.stdout.write(f"\r[{bar}] {_STEP:2}/{_TOTAL} {label:<40}{end}") | ||
| pad = " " * (_COL - len(_LABEL)) | ||
| end = "\n" if _STEP >= _TOTAL else "" | ||
| sys.stdout.write( | ||
| f"\r {_dim(_LABEL)}{pad}[{bar}] {_STEP:2}/{_TOTAL} {label:<20}{end}" | ||
| ) | ||
| sys.stdout.flush() | ||
|
|
||
|
|
||
|
|
@@ -164,14 +188,14 @@ def run( | |
| ) -> subprocess.CompletedProcess[bytes]: | ||
| """Run a command; on failure print output and exit.""" | ||
| if VERBOSE: | ||
| print(f" {label}...") | ||
| _step(_LABEL, f"{label}...", _dim) | ||
| result = subprocess.run( | ||
| cmd, | ||
| stdout = subprocess.PIPE if quiet else None, | ||
| stderr = subprocess.STDOUT if quiet else None, | ||
| ) | ||
| if result.returncode != 0: | ||
| _safe_print(_red(f"❌ {label} failed (exit code {result.returncode}):")) | ||
| _step("error", f"{label} failed (exit code {result.returncode})", _red) | ||
| if result.stdout: | ||
| print(result.stdout.decode(errors = "replace")) | ||
| sys.exit(result.returncode) | ||
|
|
@@ -353,9 +377,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: | |
| text = True, | ||
| ) | ||
| if result.returncode != 0: | ||
| _safe_print( | ||
| _red(f" ⚠️ Could not find package {package_name}, skipping patch") | ||
| ) | ||
| _step(_LABEL, f"package {package_name} not found, skipping patch", _red) | ||
| return | ||
|
|
||
| location = None | ||
|
|
@@ -365,11 +387,11 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: | |
| break | ||
|
|
||
| if not location: | ||
| _safe_print(_red(f" ⚠️ Could not determine location of {package_name}")) | ||
| _step(_LABEL, f"could not locate {package_name}", _red) | ||
| return | ||
|
|
||
| dest = Path(location) / relative_path | ||
| print(_cyan(f" Patching {dest.name} in {package_name}...")) | ||
| _step(_LABEL, f"patching {dest.name} in {package_name}...", _dim) | ||
| download_file(url, dest) | ||
|
|
||
|
|
||
|
|
@@ -633,7 +655,7 @@ def install_python_stack() -> int: | |
| stderr = subprocess.DEVNULL, | ||
| ) | ||
|
|
||
| _safe_print(_green("✅ Python dependencies installed")) | ||
| _step(_LABEL, "installed") | ||
| return 0 | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
except Exceptionblock is too broad and silently catches all exceptions. This can hide potential issues and make debugging difficult. It's best practice to catch specific exceptions or at least log the exception for future debugging, even if at a debug level. This aligns with the general rule: "Avoid using broad, silent exception handlers likeexcept Exception: pass. Instead, log the exception, even if at a debug level, to aid in future debugging."References
except Exception: pass. Instead, log the exception, even if at a debug level, to aid in future debugging.