unsloth/studio/backend/utils/studio_version.py
Michael Han 6d8c18cd1a
Replace standalone Studio wording with Unsloth (#7221)
* Replace standalone Studio wording with Unsloth

Replace the single word Studio with Unsloth wherever it is used as
shorthand for Unsloth Studio in docs, CLI output, UI strings, i18n
locales, workflow display names, comments and docstrings.

Kept unchanged: the full name Unsloth Studio, third party product
names (LM Studio, Visual Studio, Mac Studio), feature names
(Recipe Studio, Fine-tuning Studio and its translations), and all
identifiers such as env vars, commands, paths and filenames.

* Address review feedback on the Studio wording rename

Use "an" before Unsloth where the rename left the article as "a".
Restore the split brand where Unsloth and Studio render as two halves
of the full product name: the onboarding sidebar subtitle and the
IPv6 localhost warning. Scope two messages to the full name Unsloth
Studio where plain Unsloth was misleading: the AMD README bullet and
the CLI studio setup error.
2026-07-19 00:47:04 -07:00

123 lines
3.7 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Network-free Unsloth release version resolution for display-only UI."""
from __future__ import annotations
import re
import subprocess
from pathlib import Path
from utils import _studio_release_build
_DEV_VERSION = "dev"
_GIT_TIMEOUT_SECONDS = 1.0
_STUDIO_TAG_RE = re.compile(r"^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.][0-9A-Za-z.-]*)?$")
_GIT_DESCRIBE_SUFFIX_RE = re.compile(r"-\d+-g[0-9A-Fa-f]+(?:-dirty)?$")
_GIT_BRANCH_RE = re.compile(r"^[0-9A-Za-z._/-]+$")
_MAX_VERSION_LENGTH = 64
def is_valid_studio_release_version(value: object) -> bool:
"""Return True for Unsloth release tags such as ``v0.1.39-beta``."""
if not isinstance(value, str):
return False
version = value.strip()
if not version or len(version) > _MAX_VERSION_LENGTH:
return False
if version.endswith("-dirty") or _GIT_DESCRIBE_SUFFIX_RE.search(version):
return False
return _STUDIO_TAG_RE.fullmatch(version) is not None
def _repo_root() -> Path:
return Path(__file__).resolve().parents[3]
def _path_is_in_site_packages(path: Path) -> bool:
return any(part in {"site-packages", "dist-packages"} for part in path.parts)
def _is_source_checkout(repo_root: Path) -> bool:
return (repo_root / ".git").exists() and not _path_is_in_site_packages(Path(__file__).resolve())
def _exact_git_studio_tag(repo_root: Path) -> str | None:
try:
result = subprocess.run(
[
"git",
"describe",
"--tags",
"--exact-match",
"--match",
"v[0-9]*",
"HEAD",
],
cwd = repo_root,
check = False,
stdout = subprocess.PIPE,
stderr = subprocess.DEVNULL,
text = True,
timeout = _GIT_TIMEOUT_SECONDS,
)
except (OSError, subprocess.TimeoutExpired):
return None
if result.returncode != 0:
return None
tag = result.stdout.strip()
return tag if is_valid_studio_release_version(tag) else None
def _git_branch(repo_root: Path) -> str | None:
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd = repo_root,
check = False,
stdout = subprocess.PIPE,
stderr = subprocess.DEVNULL,
text = True,
timeout = _GIT_TIMEOUT_SECONDS,
)
except (OSError, subprocess.TimeoutExpired):
return None
if result.returncode != 0:
return None
branch = result.stdout.strip()
# "HEAD" means detached, e.g. a tag or commit checkout.
if (
not branch
or branch == "HEAD"
or len(branch) > _MAX_VERSION_LENGTH
or _GIT_BRANCH_RE.fullmatch(branch) is None
):
return None
return branch
def get_studio_version(repo_root: Path | None = None) -> str:
"""Return the installed Unsloth release tag for display, or ``dev``.
Intentionally separate from the PyPI ``unsloth`` package version used by
update checks. Never performs network requests.
"""
resolved_repo_root = repo_root or _repo_root()
if _is_source_checkout(resolved_repo_root):
git_tag = _exact_git_studio_tag(resolved_repo_root)
if git_tag is not None:
return git_tag
branch = _git_branch(resolved_repo_root)
return f"GitHub {branch}" if branch is not None else _DEV_VERSION
stamped_version = _studio_release_build.STUDIO_RELEASE_VERSION
if is_valid_studio_release_version(stamped_version):
return stamped_version.strip()
return _DEV_VERSION