unsloth/tests/python/test_studio_import_no_torch.py
Sanat Bhargava 1fc8bf53c7
Add Hugging Face dataset streaming mode to Studio (#4946)
* Add HF dataset streaming mode to Studio

* Added default value for datasetStreaming in training-config-store.ts

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Handle None max_steps for streaming validation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: fast-fail streaming validation and guard incompatible modes

Reject dataset_streaming at the API boundary when hf_dataset is empty,
the dataset is vision/audio, or max_steps is not set. Probe eval split
with get_dataset_split_names before the streaming load so typos fail
immediately instead of mid-training. Guard column_names=None after map
on iterables. Hide the UI toggle for non-text configurations and clear
the stale flag when config becomes incompatible.

* studio: add streaming dataset tests, iterable helper, and streaming template/format support (WIP)

Work-in-progress on top of feat/studio-dataset-streaming-mode (PR #4946):
- new test_training_streaming.py and iterable.py dataset helper
- streaming support in chat_templates.py and format_conversion.py
- additional streaming guards in trainer.py / models / routes
- frontend streaming wiring in params-section and training-config-store

Committed to preserve uncommitted work before merging latest main.

* studio: fix review-team findings for streaming + main merge

BLOCKER: streaming + raw-text/CPT crashed on len(IterableDataset). Guard it in the
start route (reject format_type=="raw" or training_type=="Continued Pretraining")
and in isStreamingSupported (datasetFormat !== "raw").

Also:
- models/training.py: validate hf_dataset/subset/split (charset+length, block ..//);
  cap dataset slice indices (le=1e9); note validator ordering
- chat_templates.py: guard _apply_custom_mapping .map() for streaming
- trainer.py: warn when packing+streaming
- training-config-store.ts: persist-migration bump to v11 (standalone datasetStreaming
  backfill); add isVisionModel to NON_PERSISTED; toast on silent streamingCompatiblePatch
  mutations in the 4 indirect setters
- tests: route rejections (max_steps, raw/cpt), slice cap, unsafe hf_dataset

* studio: enable raw-text/CPT dataset streaming + streaming UX polish

- raw_text: keep the lazy filter but skip len()-based row counting for
  IterableDatasets so raw-text / CPT can stream; guard the eval-size log
- routes/trainer: drop the raw/CPT streaming block; add a defensive
  not-streaming guard on the eval auto-split (train_test_split)
- dataset-section: streaming toggle is visible-but-disabled and lists the
  exact unmet requirement(s) in its tooltip; block embedding models
- training-start-overlay: show "streaming (no full download)" instead of a
  stuck download bar for streaming runs
- trim the streaming test suite to the high-value cases

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: address streaming review (MLX/embedding guards, sliced eval split, rehydrate timing)

- routes: reject dataset_streaming for embedding training and on Apple Silicon
  (MLX); both loaders materialize the full dataset instead of streaming
- trainer: validate the base eval split name so streaming eval accepts HF slice
  syntax such as "validation[:1000]"
- training-config-store: defer the onRehydrateStorage setState to a microtask so
  it doesn't hit the store's TDZ during synchronous hydration
- test: streaming start rejects embedding models

* studio: harden HF dataset streaming (column_names, split slicing, empty/eval bounds, gating)

Address a deeper streaming review:
- raw_text: resolve_column_names() guards IterableDataset.column_names=None
  (from_generator / unresolved features) so raw-text and CPT streaming no longer
  raise TypeError before training
- models/routes: reject HF slice syntax in train_split/eval_split when streaming
  (load_dataset(streaming=True) raises "Bad split"); reject mixed sources
  (local/S3) and embedding/MLX streaming at the API, not just in the UI
- trainer: an empty post-slice/filter stream fails preflight with a clear message;
  streaming eval is capped (STREAMING_EVAL_MAX_SAMPLES) so each eval terminates;
  the manual-slice shortcut falls back to a regular load when train_split is sliced
- format_conversion: streaming conversions preflight the first mapped row so
  format errors surface before training, not mid-iteration
- frontend: block streaming on Apple Silicon; clear datasetStreaming when a
  dataset is detected as image/audio at start

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: fix CI for streaming PR (lint blocker + no-torch sandbox + preflight test)

- trainer.py: drop unused `IterableDataset` import (hoist safety-net blocker).
- test_training_streaming.py: only select real classes (isinstance type) when
  locating the trainer class, so a MagicMock-stubbed global is never passed to
  object.__new__ (fixes TypeError on the Python 3.10-3.13 jobs).
- no-torch import sandboxes (test_e2e_no_torch_sandbox.py,
  test_studio_import_no_torch.py): teach the chat_templates/format_conversion
  exec stubs and the full-import-chain copy list about the new `.iterable`
  module so the AFTER/runtime cases import without torch again.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: Roland Tannous <rolandtannous@gravityq.ai>
Co-authored-by: Etherll <61019402+Etherll@users.noreply.github.com>
2026-06-22 17:48:18 +03:00

569 lines
24 KiB
Python

"""Sandbox tests: Studio dataset modules load/run in isolated no-torch venvs."""
from __future__ import annotations
import ast
import os
import shutil
import subprocess
import sys
import tempfile
import textwrap
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
DATA_COLLATORS = REPO_ROOT / "studio" / "backend" / "utils" / "datasets" / "data_collators.py"
CHAT_TEMPLATES = REPO_ROOT / "studio" / "backend" / "utils" / "datasets" / "chat_templates.py"
FORMAT_CONVERSION = REPO_ROOT / "studio" / "backend" / "utils" / "datasets" / "format_conversion.py"
def _has_uv() -> bool:
return shutil.which("uv") is not None
def _create_venv(venv_dir: Path, python_version: str) -> Path | None:
"""Create a uv venv at the given Python version. Returns python path or None."""
result = subprocess.run(
["uv", "venv", str(venv_dir), "--python", python_version],
capture_output = True,
)
if result.returncode != 0:
return None
venv_python = venv_dir / "bin" / "python"
if not venv_python.exists():
venv_python = venv_dir / "Scripts" / "python.exe"
return venv_python if venv_python.exists() else None
@pytest.fixture(params = ["3.12", "3.13"], scope = "module")
def no_torch_venv(request, tmp_path_factory):
"""Temp no-torch venv, parametrized for 3.12 (Intel Mac) and 3.13 (Apple Silicon / Linux)."""
if not _has_uv():
pytest.skip("uv not available")
py_version = request.param
venv_dir = tmp_path_factory.mktemp(f"no_torch_venv_{py_version}")
venv_python = _create_venv(venv_dir, py_version)
if venv_python is None:
pytest.skip(f"Could not create Python {py_version} venv")
check = subprocess.run(
[str(venv_python), "-c", "import torch"],
capture_output = True,
)
assert check.returncode != 0, f"torch should NOT be importable in fresh {py_version} venv"
return str(venv_python)
# ── AST structural checks ─────────────────────────────────────────────
class TestDataCollatorsAST:
"""Static analysis: data_collators.py has no top-level torch imports."""
def test_ast_parse(self):
"""data_collators.py must be valid Python syntax."""
source = DATA_COLLATORS.read_text(encoding = "utf-8")
tree = ast.parse(source, filename = str(DATA_COLLATORS))
assert tree is not None
def test_no_top_level_torch_import(self):
"""No top-level 'import torch' or 'from torch' statements."""
source = DATA_COLLATORS.read_text(encoding = "utf-8")
tree = ast.parse(source)
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Import):
for alias in node.names:
assert not alias.name.startswith(
"torch"
), f"Top-level 'import {alias.name}' found at line {node.lineno}"
elif isinstance(node, ast.ImportFrom):
if node.module:
assert not node.module.startswith(
"torch"
), f"Top-level 'from {node.module}' found at line {node.lineno}"
class TestChatTemplatesAST:
"""Static analysis: chat_templates.py has no top-level torch imports."""
def test_ast_parse(self):
"""chat_templates.py must be valid Python syntax."""
source = CHAT_TEMPLATES.read_text(encoding = "utf-8")
tree = ast.parse(source, filename = str(CHAT_TEMPLATES))
assert tree is not None
def test_no_top_level_torch_import(self):
"""No top-level 'import torch' or 'from torch' at module level."""
source = CHAT_TEMPLATES.read_text(encoding = "utf-8")
tree = ast.parse(source)
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Import):
for alias in node.names:
assert not alias.name.startswith(
"torch"
), f"Top-level 'import {alias.name}' found at line {node.lineno}"
elif isinstance(node, ast.ImportFrom):
if node.module:
assert not node.module.startswith(
"torch"
), f"Top-level 'from {node.module}' found at line {node.lineno}"
def test_torch_imports_only_inside_functions(self):
"""All 'from torch' imports must be inside function/method bodies."""
source = CHAT_TEMPLATES.read_text(encoding = "utf-8")
tree = ast.parse(source)
torch_imports = []
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
module = None
if isinstance(node, ast.ImportFrom):
module = node.module
elif isinstance(node, ast.Import):
module = node.names[0].name if node.names else None
if module and module.startswith("torch"):
torch_imports.append(node)
top_level = set(id(n) for n in ast.iter_child_nodes(tree))
for imp in torch_imports:
assert id(imp) not in top_level, (
f"torch import at line {imp.lineno} is at top level"
" (should be inside a function)"
)
# ── data_collators.py: exec + dataclass instantiation in no-torch venv ──
class TestDataCollatorsNoTorchVenv:
"""Run data_collators.py in an isolated no-torch venv, verify classes load."""
def test_exec_in_no_torch_venv(self, no_torch_venv):
"""data_collators.py executes in a venv without torch (with loggers stub)."""
code = textwrap.dedent(f"""\
import sys, types
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: None
sys.modules['loggers'] = loggers
exec(open({str(DATA_COLLATORS)!r}).read())
print("OK: exec succeeded")
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert (
result.returncode == 0
), f"data_collators.py failed in no-torch venv:\n{result.stderr.decode()}"
assert b"OK: exec succeeded" in result.stdout
def test_dataclass_speech_collator_instantiable(self, no_torch_venv):
"""DataCollatorSpeechSeq2SeqWithPadding can be instantiated with processor=None."""
code = textwrap.dedent(f"""\
import sys, types
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: None
sys.modules['loggers'] = loggers
exec(open({str(DATA_COLLATORS)!r}).read())
obj = DataCollatorSpeechSeq2SeqWithPadding(processor=None)
assert obj.processor is None, "processor should be None"
print("OK: DataCollatorSpeechSeq2SeqWithPadding instantiated")
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert (
result.returncode == 0
), f"DataCollatorSpeechSeq2SeqWithPadding failed:\n{result.stderr.decode()}"
assert b"OK: DataCollatorSpeechSeq2SeqWithPadding instantiated" in result.stdout
def test_dataclass_deepseek_collator_instantiable(self, no_torch_venv):
"""DeepSeekOCRDataCollator can be instantiated with processor=None."""
code = textwrap.dedent(f"""\
import sys, types
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: None
sys.modules['loggers'] = loggers
exec(open({str(DATA_COLLATORS)!r}).read())
obj = DeepSeekOCRDataCollator(processor=None)
assert obj.processor is None, "processor should be None"
assert obj.max_length == 2048, "default max_length should be 2048"
assert obj.ignore_index == -100, "default ignore_index should be -100"
print("OK: DeepSeekOCRDataCollator instantiated")
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert result.returncode == 0, f"DeepSeekOCRDataCollator failed:\n{result.stderr.decode()}"
assert b"OK: DeepSeekOCRDataCollator instantiated" in result.stdout
def test_dataclass_vlm_collator_instantiable(self, no_torch_venv):
"""VLMDataCollator can be instantiated with processor=None."""
code = textwrap.dedent(f"""\
import sys, types
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: None
sys.modules['loggers'] = loggers
exec(open({str(DATA_COLLATORS)!r}).read())
obj = VLMDataCollator(processor=None)
assert obj.processor is None
assert obj.mask_input_tokens is True, "default mask_input_tokens should be True"
print("OK: VLMDataCollator instantiated")
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert result.returncode == 0, f"VLMDataCollator failed:\n{result.stderr.decode()}"
assert b"OK: VLMDataCollator instantiated" in result.stdout
# ── chat_templates.py: exec in no-torch venv ─────────────────────────
class TestChatTemplatesNoTorchVenv:
"""Run chat_templates.py in an isolated no-torch venv with stubs."""
def test_exec_with_stubs(self, no_torch_venv):
"""chat_templates.py top-level exec works with stubs for relative imports."""
code = textwrap.dedent(f"""\
import sys, types
# Stub loggers
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: type('L', (), {{'info': lambda s, m: None, 'warning': lambda s, m: None, 'debug': lambda s, m: None}})()
sys.modules['loggers'] = loggers
# Stub relative imports (.format_detection, .model_mappings)
format_detection = types.ModuleType('format_detection')
format_detection.detect_dataset_format = lambda *a, **k: None
format_detection.detect_multimodal_dataset = lambda *a, **k: None
format_detection.detect_custom_format_heuristic = lambda *a, **k: None
sys.modules['format_detection'] = format_detection
model_mappings = types.ModuleType('model_mappings')
model_mappings.MODEL_TO_TEMPLATE_MAPPER = {{}}
sys.modules['model_mappings'] = model_mappings
iterable = types.ModuleType('iterable')
iterable.is_streaming_dataset = lambda *a, **k: False
sys.modules['iterable'] = iterable
# Read and transform the source: replace relative imports with absolute
source = open({str(CHAT_TEMPLATES)!r}).read()
source = source.replace('from .format_detection import', 'from format_detection import')
source = source.replace('from .model_mappings import', 'from model_mappings import')
source = source.replace('from .iterable import', 'from iterable import')
exec(source)
# Verify module-level constants are defined
ns = dict(locals())
assert 'DEFAULT_ALPACA_TEMPLATE' in ns, "DEFAULT_ALPACA_TEMPLATE not defined after exec"
print("OK: chat_templates.py exec succeeded")
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert (
result.returncode == 0
), f"chat_templates.py failed in no-torch venv:\n{result.stderr.decode()}"
assert b"OK: chat_templates.py exec succeeded" in result.stdout
def test_default_alpaca_template_defined(self, no_torch_venv):
"""DEFAULT_ALPACA_TEMPLATE constant is accessible after exec."""
code = textwrap.dedent(f"""\
import sys, types
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: type('L', (), {{'info': lambda s, m: None, 'warning': lambda s, m: None, 'debug': lambda s, m: None}})()
sys.modules['loggers'] = loggers
format_detection = types.ModuleType('format_detection')
format_detection.detect_dataset_format = lambda *a, **k: None
format_detection.detect_multimodal_dataset = lambda *a, **k: None
format_detection.detect_custom_format_heuristic = lambda *a, **k: None
sys.modules['format_detection'] = format_detection
model_mappings = types.ModuleType('model_mappings')
model_mappings.MODEL_TO_TEMPLATE_MAPPER = {{}}
sys.modules['model_mappings'] = model_mappings
iterable = types.ModuleType('iterable')
iterable.is_streaming_dataset = lambda *a, **k: False
sys.modules['iterable'] = iterable
ns = {{}}
source = open({str(CHAT_TEMPLATES)!r}).read()
source = source.replace('from .format_detection import', 'from format_detection import')
source = source.replace('from .model_mappings import', 'from model_mappings import')
source = source.replace('from .iterable import', 'from iterable import')
exec(source, ns)
assert 'DEFAULT_ALPACA_TEMPLATE' in ns, "DEFAULT_ALPACA_TEMPLATE not defined"
assert 'Instruction' in ns['DEFAULT_ALPACA_TEMPLATE'], "Template content unexpected"
print("OK: DEFAULT_ALPACA_TEMPLATE defined and valid")
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert (
result.returncode == 0
), f"DEFAULT_ALPACA_TEMPLATE check failed:\n{result.stderr.decode()}"
assert b"OK: DEFAULT_ALPACA_TEMPLATE defined and valid" in result.stdout
# ── format_conversion.py: AST + runtime tests ────────────────────────
class TestFormatConversionAST:
"""Static analysis: format_conversion.py torch imports are guarded."""
def test_ast_parse(self):
"""format_conversion.py must be valid Python syntax."""
source = FORMAT_CONVERSION.read_text(encoding = "utf-8")
tree = ast.parse(source, filename = str(FORMAT_CONVERSION))
assert tree is not None
def test_no_bare_torch_import_in_functions(self):
"""All 'from torch' imports in function bodies must be inside try/except."""
source = FORMAT_CONVERSION.read_text(encoding = "utf-8")
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
for child in ast.walk(node):
if (
isinstance(child, ast.ImportFrom)
and child.module
and child.module.startswith("torch")
):
# This torch import must be inside a Try node.
found_in_try = False
for try_node in ast.walk(node):
if isinstance(try_node, ast.Try):
for try_child in ast.walk(try_node):
if try_child is child:
found_in_try = True
break
if found_in_try:
break
assert found_in_try, (
f"torch import at line {child.lineno} in {node.name}() "
"is not inside a try/except block"
)
class TestFormatConversionNoTorchVenv:
"""Run format_conversion.py functions in a no-torch venv."""
def test_convert_chatml_to_alpaca_no_torch(self, no_torch_venv):
"""convert_chatml_to_alpaca works without torch (via try/except ImportError)."""
code = textwrap.dedent(f"""\
import sys, types
# Stub loggers
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: type('L', (), {{
'info': lambda s, m: None,
'warning': lambda s, m: None,
'debug': lambda s, m: None,
}})()
sys.modules['loggers'] = loggers
# Stub datasets.IterableDataset (HF datasets, not torch)
datasets_mod = types.ModuleType('datasets')
datasets_mod.IterableDataset = type('IterableDataset', (), {{}})
sys.modules['datasets'] = datasets_mod
iterable_mod = types.ModuleType('iterable')
iterable_mod.is_streaming_dataset = lambda *a, **k: False
sys.modules['iterable'] = iterable_mod
# Stub utils.hardware
utils_mod = types.ModuleType('utils')
hardware_mod = types.ModuleType('utils.hardware')
hardware_mod.dataset_map_num_proc = lambda n=None: 1
utils_mod.hardware = hardware_mod
sys.modules['utils'] = utils_mod
sys.modules['utils.hardware'] = hardware_mod
# Read and exec format_conversion.py
source = open({str(FORMAT_CONVERSION)!r}).read()
source = source.replace('from .format_detection import', 'from format_detection import')
source = source.replace('from .iterable import', 'from iterable import')
ns = {{'__name__': '__test__'}}
exec(source, ns)
# Test convert_chatml_to_alpaca with a simple dataset
class FakeDataset:
def map(self, fn, **kw):
result = fn({{
'messages': [[
{{'role': 'user', 'content': 'Hello'}},
{{'role': 'assistant', 'content': 'Hi there'}},
]]
}})
return result
result = ns['convert_chatml_to_alpaca'](FakeDataset())
assert 'instruction' in result, f"Expected 'instruction' in result, got {{result.keys()}}"
assert result['instruction'] == ['Hello']
assert result['output'] == ['Hi there']
print("OK: convert_chatml_to_alpaca works without torch")
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert (
result.returncode == 0
), f"convert_chatml_to_alpaca failed without torch:\n{result.stderr.decode()}"
assert b"OK: convert_chatml_to_alpaca works without torch" in result.stdout
def test_convert_alpaca_to_chatml_no_torch(self, no_torch_venv):
"""convert_alpaca_to_chatml works without torch (via try/except ImportError)."""
code = textwrap.dedent(f"""\
import sys, types
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: type('L', (), {{
'info': lambda s, m: None,
'warning': lambda s, m: None,
'debug': lambda s, m: None,
}})()
sys.modules['loggers'] = loggers
datasets_mod = types.ModuleType('datasets')
datasets_mod.IterableDataset = type('IterableDataset', (), {{}})
sys.modules['datasets'] = datasets_mod
iterable_mod = types.ModuleType('iterable')
iterable_mod.is_streaming_dataset = lambda *a, **k: False
sys.modules['iterable'] = iterable_mod
utils_mod = types.ModuleType('utils')
hardware_mod = types.ModuleType('utils.hardware')
hardware_mod.dataset_map_num_proc = lambda n=None: 1
utils_mod.hardware = hardware_mod
sys.modules['utils'] = utils_mod
sys.modules['utils.hardware'] = hardware_mod
source = open({str(FORMAT_CONVERSION)!r}).read()
source = source.replace('from .format_detection import', 'from format_detection import')
source = source.replace('from .iterable import', 'from iterable import')
ns = {{'__name__': '__test__'}}
exec(source, ns)
class FakeDataset:
def map(self, fn, **kw):
result = fn({{
'instruction': ['Write a poem'],
'input': [''],
'output': ['Roses are red'],
}})
return result
result = ns['convert_alpaca_to_chatml'](FakeDataset())
assert 'conversations' in result
convo = result['conversations'][0]
assert convo[0]['role'] == 'user'
assert convo[1]['role'] == 'assistant'
print("OK: convert_alpaca_to_chatml works without torch")
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert (
result.returncode == 0
), f"convert_alpaca_to_chatml failed without torch:\n{result.stderr.decode()}"
assert b"OK: convert_alpaca_to_chatml works without torch" in result.stdout
# ── Negative controls ─────────────────────────────────────────────────
class TestNegativeControls:
"""Prove the fix is necessary by showing what fails WITHOUT it."""
def test_import_torch_prepended_fails(self, no_torch_venv):
"""Prepending 'import torch' to data_collators.py causes ModuleNotFoundError."""
with tempfile.NamedTemporaryFile(
mode = "w", suffix = ".py", delete = False, encoding = "utf-8"
) as f:
f.write("import torch\n")
f.write(DATA_COLLATORS.read_text(encoding = "utf-8"))
temp_file = f.name
try:
code = textwrap.dedent(f"""\
import sys, types
loggers = types.ModuleType('loggers')
loggers.get_logger = lambda n: None
sys.modules['loggers'] = loggers
exec(open({temp_file!r}).read())
""")
result = subprocess.run(
[no_torch_venv, "-c", code],
capture_output = True,
timeout = 30,
)
assert result.returncode != 0, "Expected failure when 'import torch' is prepended"
assert (
b"ModuleNotFoundError" in result.stderr or b"ImportError" in result.stderr
), f"Expected ImportError, got:\n{result.stderr.decode()}"
finally:
os.unlink(temp_file)
def test_torchao_install_fails_no_torch_venv(self, no_torch_venv):
"""torchao install fails in a no-torch venv: proves the overrides.txt skip is needed."""
result = subprocess.run(
[
no_torch_venv,
"-m",
"pip",
"install",
"torchao==0.14.0",
"--dry-run",
],
capture_output = True,
timeout = 60,
)
if result.returncode != 0:
# torchao install/resolution failed as expected.
pass
else:
# dry-run may miss dep issues; verify torch is absent instead.
check = subprocess.run(
[no_torch_venv, "-c", "import torch"],
capture_output = True,
)
assert (
check.returncode != 0
), "torch should not be importable -- torchao would fail at runtime"
def test_direct_torch_import_fails(self, no_torch_venv):
"""Direct 'import torch' fails in the no-torch venv."""
result = subprocess.run(
[no_torch_venv, "-c", "import torch; print('torch loaded')"],
capture_output = True,
timeout = 30,
)
assert result.returncode != 0, "import torch should fail in no-torch venv"
assert b"ModuleNotFoundError" in result.stderr or b"ImportError" in result.stderr