* Fix Studio crash on Anaconda Python due to platform._sys_version() parse failure
Anaconda and conda-forge modify sys.version to include distributor
metadata between pipe characters, e.g.:
3.12.4 | packaged by Anaconda, Inc. | (main, ...) [MSC v.1929 ...]
Python's platform._sys_version() has a hardcoded regex that cannot
parse this format, raising ValueError. CPython closed this as "not
planned" (cpython#102396) since Anaconda modified the binary.
This breaks the import chain: run.py -> structlog -> rich -> attrs,
which calls platform.python_implementation() at module scope.
Fix: before any library imports, strip the pipe segments, parse the
cleaned version string via the standard parser, and cache the result
under the original sys.version key so all subsequent platform calls
hit the cache.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Add defensive fallback for unpaired pipe edge cases in version patch
Address Gemini review suggestion: if the paired-pipe regex leaves
residual pipes (hypothetical single-pipe distributor metadata), fall
back to extracting the version number and the parenthesized build
info directly. Wrap the entire patch in try/except so unexpected
version string formats degrade gracefully instead of crashing the
patch itself.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Refactor into shared _platform_compat module, cover colab.py entrypoint
Address reviewer feedback:
1. Extract the Anaconda/conda-forge sys.version fix into a shared
_platform_compat.py module that wraps platform._sys_version() with
a retry-on-ValueError fallback. This is more robust than cache-seeding
because it handles all future platform._sys_version() calls, not just
the first one.
2. Import the fix from both run.py and colab.py entrypoints, so Studio
no longer crashes on Anaconda Python regardless of the launch path.
3. The wrapper is idempotent (guarded by a flag) and handles edge cases:
paired pipes (Anaconda, conda-forge), unpaired pipes (hypothetical),
and standard CPython strings (no-op since ValueError is never raised).
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Replace monkey-patch with cache-prime, fix colab.py duplicate sys.path, cover main.py
- Rewrite _platform_compat.py: replace function-wrapping monkey-patch with
one-shot cache seed (_seed_sys_version_cache). Parses cleaned sys.version
once and seeds platform._sys_version_cache so the stdlib parser never sees
the problematic Anaconda/conda-forge pipe-delimited string. No function
replacement, no idempotency flag, no reload edge cases.
- colab.py: remove duplicate backend_path sys.path insertion after
_bootstrap_studio_venv(). The early insertion (before _platform_compat
import) already covers it. This also fixes backend/ ending up behind
venv site-packages in sys.path ordering.
- run.py: move PYTHONWARNINGS=ignore before _platform_compat import to
preserve original intent of suppressing warnings early.
- main.py: add sys.path + _platform_compat import before route imports,
covering the direct `uvicorn main:app` launch path.
- Add test_platform_compat.py with 7 tests covering Anaconda, conda-forge,
and standard CPython version strings, plus the loggers import chain.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Remove test_platform_compat.py from PR
* Handle Format B conda-forge version strings with duplicate paren groups
Some conda-forge builds produce sys.version with the build info both
before and after the pipe label (e.g. "3.9.7 (default, ...) | packaged
by conda-forge | (default, ...) \n[GCC 7.5.0]"). After stripping the
pipe segment, two consecutive (...) groups remain, which still fails
platform._sys_version(). Add a second regex pass to drop the duplicate
paren group.
* Guard _sys_version call with try/except to avoid making things worse
If the cleaned version string is still unparseable by the stdlib regex
(e.g. nested parens, exotic multi-pipe formats), silently give up
instead of letting ValueError propagate at import time -- which would
be a worse crash than the original deferred one.
---------
Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
59 lines
2.1 KiB
Python
59 lines
2.1 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
|
|
|
|
"""
|
|
Compatibility shim for Anaconda/conda-forge Python builds.
|
|
|
|
Anaconda modifies sys.version to include distributor metadata between pipe
|
|
characters, e.g. '3.12.4 | packaged by Anaconda, Inc. | (main, ...) [MSC ...]'.
|
|
Python's platform._sys_version() has a hardcoded regex that cannot parse this,
|
|
raising ValueError. CPython closed this as "not planned" (cpython#102396).
|
|
|
|
This module seeds platform._sys_version_cache so the stdlib parser never sees
|
|
the problematic string, fixing the import chain:
|
|
structlog -> rich.pretty -> attrs._compat -> platform.python_implementation()
|
|
|
|
Import this module before any library imports that may trigger the above chain.
|
|
Safe to import multiple times (no-op if cache is already seeded or no pipes).
|
|
"""
|
|
|
|
import platform
|
|
import re
|
|
import sys
|
|
|
|
|
|
def _seed_sys_version_cache() -> None:
|
|
"""One-shot cache prime: parse a cleaned sys.version and seed the cache."""
|
|
raw = sys.version
|
|
|
|
# Strip paired |...| segments (Anaconda, conda-forge metadata)
|
|
cleaned = re.sub(r"\s*\|[^|]*\|\s*", " ", raw).strip()
|
|
|
|
# Format B: "ver (build) | label | (build_dup) \n[compiler]"
|
|
# After pipe-strip, two consecutive (...) groups remain; drop the second.
|
|
cleaned = re.sub(r"(\([^)]*\))\s+\([^)]*\)", r"\1", cleaned)
|
|
|
|
if "|" in cleaned:
|
|
# Unpaired pipe remaining -- keep version + everything from "(" onward
|
|
m = re.match(r"([\w.+]+)\s*", cleaned)
|
|
p = cleaned.find("(")
|
|
if m and p > 0:
|
|
cleaned = m.group(0) + cleaned[p:]
|
|
|
|
if cleaned == raw:
|
|
return # Nothing to fix
|
|
|
|
# Parse the cleaned string through the real stdlib parser
|
|
try:
|
|
result = platform._sys_version(cleaned)
|
|
except ValueError:
|
|
return # Cleaning didn't produce a parseable string; don't make things worse
|
|
|
|
# Seed the cache so future calls with the raw string skip parsing entirely
|
|
cache = getattr(platform, "_sys_version_cache", None)
|
|
if isinstance(cache, dict):
|
|
cache[raw] = result
|
|
|
|
|
|
if "|" in sys.version:
|
|
_seed_sys_version_cache()
|