"""Tests for the ``repo:variant`` shorthand parser used by ``unsloth studio run``. Loads studio.py via importlib with a minimal typer stub to avoid importing the unsloth training stack. """ from __future__ import annotations import importlib.util import sys import types from pathlib import Path import pytest def _load_split_repo_variant(): """Load ``_split_repo_variant`` from studio.py with typer stubbed (discards decorator calls).""" if "typer" not in sys.modules: typer_stub = types.ModuleType("typer") class _Typer: def __init__(self, **kwargs): pass def callback(self, *args, **kwargs): return lambda fn: fn def command(self, *args, **kwargs): return lambda fn: fn typer_stub.Typer = _Typer typer_stub.Option = lambda *args, **kwargs: (args[0] if args else None) typer_stub.Context = type("Context", (), {}) typer_stub.Exit = type("Exit", (Exception,), {}) typer_stub.echo = lambda *args, **kwargs: None sys.modules["typer"] = typer_stub studio_py = Path(__file__).resolve().parents[2] / "unsloth_cli" / "commands" / "studio.py" spec = importlib.util.spec_from_file_location("_studio_for_repo_variant_test", studio_py) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module._split_repo_variant _split = _load_split_repo_variant() # ── HF-style repo:variant inputs ------------------------------------- @pytest.mark.parametrize( "model_arg, expected", [ ( "unsloth/gpt-oss-20b-GGUF:UD-Q4_K_XL", ("unsloth/gpt-oss-20b-GGUF", "UD-Q4_K_XL"), ), ("unsloth/gpt-oss-120b-GGUF:Q4_K_XL", ("unsloth/gpt-oss-120b-GGUF", "Q4_K_XL")), ("unsloth/Qwen3-0.6B-GGUF:Q4_K_M", ("unsloth/Qwen3-0.6B-GGUF", "Q4_K_M")), # Variants commonly contain dashes, dots, and underscores. ("org/repo:UD-Q5_K_M", ("org/repo", "UD-Q5_K_M")), ("org/repo:F16", ("org/repo", "F16")), ], ) def test_repo_variant_split(model_arg, expected): assert _split(model_arg) == expected # ── No variant suffix ------------------------------------------------ @pytest.mark.parametrize( "model_arg", [ "unsloth/gpt-oss-20b-GGUF", "unsloth/Qwen3-0.6B-GGUF", "shorthand-no-org-no-colon", ], ) def test_no_colon_returns_none_variant(model_arg): repo, variant = _split(model_arg) assert repo == model_arg assert variant is None # ── Local paths must NOT be split ------------------------------------ @pytest.mark.parametrize( "local_path", [ "/abs/path/to/model.gguf", "/abs/path:with-colon-in-name", "./relative/model", "../parent/model", "~/home/model", ".", "C:\\Users\\me\\model.gguf", "C:/Users/me/model.gguf", "D:/data/model:Q4", # Windows drive + colon-suffixed filename: drive wins ], ) def test_local_path_passthrough(local_path): repo, variant = _split(local_path) assert repo == local_path assert variant is None # ── Edge cases ------------------------------------------------------- def test_empty_string(): assert _split("") == ("", None) def test_trailing_colon_no_variant(): # "org/repo:" has no quant label; pass through unchanged so backend validation gives a clearer error. repo, variant = _split("org/repo:") assert repo == "org/repo:" assert variant is None def test_slash_in_variant_disqualifies_split(): # "foo:bar/baz" suffix has a slash, so it's not a quant label; treat as opaque. repo, variant = _split("foo:bar/baz") assert repo == "foo:bar/baz" assert variant is None def test_whitespace_stripped(): assert _split(" org/repo:Q4 ") == ("org/repo", "Q4")