From e14e2d89bf489bf43f15743dd04d7fb4cb2ca7a9 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 29 Jul 2026 02:43:18 +0000 Subject: [PATCH] pip: restore the rich, audio and flash-attn dependencies main declares --- pyproject.toml | 22 +++++++++ .../python/test_pyproject_pypi_installable.py | 49 +++++++++++++++++++ tests/python/test_torchcodec_torch_compat.py | 11 ++++- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 64703a0ac8..753c5a63d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,10 @@ dependencies = [ # imports that, so every command needs it. typer supplied it until 0.27 # dropped the dependency, which left this satisfied only by chance. "click>=8.0", + # unsloth_cli/__init__.py imports commands/chat.py, which imports rich at + # module level, so every command needs it. typer still pulls rich, but it + # pulled click too until 0.26.0 dropped it: same shape, so declare it. + "rich", ] [project.scripts] @@ -506,16 +510,19 @@ cu126-torch2100 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu126onlytorch2100]", + "unsloth[audio-torch210]", ] cu128-torch2100 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu128onlytorch2100]", + "unsloth[audio-torch210]", ] cu130-torch2100 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu130onlytorch2100]", + "unsloth[audio-torch210]", ] kaggle = [ "unsloth[huggingface]", @@ -537,6 +544,17 @@ flashattention = [ "ninja ; ('linux' in sys_platform)", "flash-attn>=2.6.3 ; ('linux' in sys_platform)", ] +# torchcodec is the audio decode path; its releases track torch minor versions, +# so each torch minor gets its own window (#7225). +audio-torch280 = [ + "torchcodec>=0.6.0,<0.8.0 ; python_version >= '3.9'", +] +audio-torch290 = [ + "torchcodec>=0.8.0,<0.10.0 ; python_version >= '3.10'", +] +audio-torch210 = [ + "torchcodec>=0.10.0,<0.11.0 ; python_version >= '3.10'", +] colab-ampere-torch211 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", @@ -769,6 +787,7 @@ cu130-ampere-torch280 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu130onlytorch280]", + "unsloth[flashattention]", ] cu126-ampere-torch290 = [ "unsloth[huggingface]", @@ -804,16 +823,19 @@ cu126-ampere-torch2100 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu126onlytorch2100]", + "unsloth[audio-torch210]", ] cu128-ampere-torch2100 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu128onlytorch2100]", + "unsloth[audio-torch210]", ] cu130-ampere-torch2100 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu130onlytorch2100]", + "unsloth[audio-torch210]", ] [project.urls] diff --git a/tests/python/test_pyproject_pypi_installable.py b/tests/python/test_pyproject_pypi_installable.py index 911b40c316..dbfe2d0d77 100644 --- a/tests/python/test_pyproject_pypi_installable.py +++ b/tests/python/test_pyproject_pypi_installable.py @@ -101,3 +101,52 @@ class TestAmdExtraIsInstallableFromPyPI: extras = _load()["project"].get("optional-dependencies", {}) assert any("huggingfacenotorch" in d for d in extras["amd"]) assert "huggingfacenotorch" in extras + + +class TestRuntimeImportsAreDeclared: + """This branch's base install has to satisfy every module-scope import on the CLI + entry path. typer supplied click until 0.26.0 dropped it (#7504); it still supplies + rich, so rich is satisfied only by chance unless we declare it ourselves.""" + + ENTRY_PATH_IMPORTS = ("click", "rich", "structlog", "typer") + + def test_cli_entry_path_imports_are_base_dependencies(self): + packaging_requirements = pytest.importorskip("packaging.requirements") + base = _load()["project"].get("dependencies", []) + declared = { + packaging_requirements.Requirement(r).name.lower().replace("_", "-") + for r in base + } + missing = [p for p in self.ENTRY_PATH_IMPORTS if p not in declared] + assert missing == [], ( + "imported at module scope by unsloth_cli/__init__.py's import chain but not " + f"declared in base dependencies: {missing}" + ) + + +class TestAcceleratorExtrasCarryTheirCompanions: + """Each accelerator extra composes a stack: `-ampere-` variants add flash-attn and + torch 2.10 variants add the torchcodec audio path. A variant that silently drops one + installs a quietly weaker environment than its siblings.""" + + def test_torch2100_extras_pull_the_audio_path(self): + extras = _load()["project"].get("optional-dependencies", {}) + targets = [ + n + for n in extras + if n.endswith("torch2100") and "only" not in n and n.startswith("cu") + ] + assert targets, "expected cu*-torch2100 extras to exist" + missing = [n for n in targets if not any("audio-torch" in d for d in extras[n])] + assert missing == [], f"torch 2.10 extras missing the audio extra: {missing}" + + def test_ampere_torch280_extras_pull_flash_attention(self): + extras = _load()["project"].get("optional-dependencies", {}) + targets = [n for n in extras if n.endswith("-ampere-torch280")] + assert targets, "expected *-ampere-torch280 extras to exist" + missing = [ + n + for n in targets + if not any("flashattention" in d or "flash-attn" in d for d in extras[n]) + ] + assert missing == [], f"ampere torch 2.8 extras missing flash-attn: {missing}" diff --git a/tests/python/test_torchcodec_torch_compat.py b/tests/python/test_torchcodec_torch_compat.py index 6ad16a73f4..b90f4ccba8 100644 --- a/tests/python/test_torchcodec_torch_compat.py +++ b/tests/python/test_torchcodec_torch_compat.py @@ -45,15 +45,22 @@ def _stub_torch(monkeypatch, version: str): def test_torch210_extras_bundle_audio_torch210(): + """The ROCm extras pin torch from the AMD wheel index, which PyPI rejects as a + direct reference, so this branch does not carry them. Check whichever torch 2.10 + extras it does define, and require at least one.""" text = PYPROJECT.read_text(encoding = "utf-8") + checked = 0 for extra in ( "cu128-torch2100", "cu126-ampere-torch2100", "rocm72-torch2100", ): match = re.search(rf"^{extra} = \[(.*?)^\]", text, re.MULTILINE | re.DOTALL) - assert match is not None, extra - assert "unsloth[audio-torch210]" in match.group(1) + if match is None: + continue + assert "unsloth[audio-torch210]" in match.group(1), extra + checked += 1 + assert checked, "no torch 2.10 extra found to check" def test_torchcodec_matrix_matches_notebook_validator():