From d838353e9f5d16f304add1813ec6443736dbc0f8 Mon Sep 17 00:00:00 2001 From: unslothai Date: Wed, 29 Jul 2026 03:38:40 +0000 Subject: [PATCH] Tighten the comments on the PyPI metadata guards --- pyproject.toml | 36 ++++++--------- .../python/test_pyproject_pypi_installable.py | 44 +++++++------------ tests/python/test_torchcodec_torch_compat.py | 5 +-- 3 files changed, 32 insertions(+), 53 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3bdb64f566..46167639da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,8 +59,7 @@ 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 + # Imported at module level on the CLI entry path. typer still pulls rich, but it # pulled click too until 0.26.0 dropped it: same shape, so declare it. "rich", ] @@ -157,12 +156,9 @@ huggingface = [ "pyyaml", "nest-asyncio", ] -# "notorch" means this list declares no torch of its own, not that installing it -# leaves you torch-free: extras are additive to [project].dependencies, which on -# this branch deliberately carry the full runtime so a bare `pip install unsloth` -# works, and accelerate/peft/sentence-transformers each require torch anyway. The -# genuinely torch-free path is the installer's --no-deps pass over -# studio/backend/requirements/no-torch-runtime.txt, not plain pip resolution. +# "notorch" means this list declares no torch, not that you end up torch-free: extras +# are additive to [project].dependencies, which carry the full runtime here. The +# torch-free path is the installer's --no-deps pass over no-torch-runtime.txt, not pip. # Kept byte-identical to main so the two branches can be diffed for drift. huggingfacenotorch = [ "unsloth_zoo>=2026.7.6", @@ -184,17 +180,13 @@ huggingfacenotorch = [ "trl>=0.18.2,!=0.19.0,<=0.24.0", "sentence-transformers", ] -# What this extra does on this branch is raise the bitsandbytes floor; it does not -# hand you a ROCm stack. The base dependencies still apply, so ROCm torch has to -# come from the AMD index or already be installed (pip leaves an installed -# +rocm torch alone, since it satisfies the base specifier). Use install.sh or -# `unsloth studio install` for a torch-free AMD install. +# Raises the bitsandbytes floor; it does not hand you a ROCm stack. The base deps still +# apply, so ROCm torch must come from the AMD index or be installed already (pip leaves +# a +rocm torch alone). Use install.sh or `unsloth studio install` for a torch-free AMD install. amd = [ "unsloth[huggingfacenotorch]", - # 4-bit decode is unreliable on ROCm before 0.50.0, the first PyPI release - # carrying the full path: blocksize/warp decoupling (bnb #1887), fused SIMT - # GEMM on RDNA (#1979), RDNA3/4 workgroup fix (#2012). Keep in step with - # install.sh and studio/install_python_stack.py. + # 4-bit decode is unreliable on ROCm before 0.50.0 (bnb #1887, #1979, #2012). + # Keep in step with install.sh and studio/install_python_stack.py. "bitsandbytes>=0.50.0 ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64' or platform_machine == 'aarch64')", "bitsandbytes>=0.50.0 ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] @@ -556,12 +548,10 @@ 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). It publishes no sdist and only -# manylinux_2_28_x86_64, macosx_*_arm64 and win_amd64 wheels, so Linux aarch64, -# Windows ARM64 and Intel Mac have nothing to resolve and pip would hard-fail the -# whole install. Gate on the platforms that have a wheel, matching -# PLATFORM_LACKS_TORCHCODEC_WHEEL in studio/install_python_stack.py. +# torchcodec releases track torch minor versions, so each torch minor gets its own +# window (#7225). It ships no sdist and only x86_64 linux/windows and arm64 macOS +# wheels, so elsewhere pip would hard-fail the whole install: gate on the platforms +# with a wheel, matching PLATFORM_LACKS_TORCHCODEC_WHEEL in install_python_stack.py. audio-torch280 = [ "torchcodec>=0.6.0,<0.8.0 ; python_version >= '3.9' and (((sys_platform == 'linux' or sys_platform == 'win32') and (platform_machine == 'x86_64' or platform_machine == 'AMD64')) or (sys_platform == 'darwin' and platform_machine == 'arm64'))", ] diff --git a/tests/python/test_pyproject_pypi_installable.py b/tests/python/test_pyproject_pypi_installable.py index 916aa2ce29..f760886f34 100644 --- a/tests/python/test_pyproject_pypi_installable.py +++ b/tests/python/test_pyproject_pypi_installable.py @@ -3,14 +3,10 @@ """This branch is what gets uploaded to PyPI, so its metadata has to satisfy PyPI's rules. -PyPI rejects any PEP 508 direct reference in ``Requires-Dist`` with -``Invalid value for requires_dist. Error: Can't have direct dependency`` -(pypi/warehouse#7136), and ``twine check`` does not catch it beforehand -(pypa/twine#726) -- the upload just 400s. ``main`` carries hundreds of direct URL -requirements for the CUDA/XPU/ROCm wheel indexes, so every merge from ``main`` is a -chance to import one here and break publishing. Nothing else guards that. - -Offline by design: structural checks only, no network. +PyPI rejects any PEP 508 direct reference in ``Requires-Dist`` (pypi/warehouse#7136) and +``twine check`` does not catch it (pypa/twine#726): the upload just 400s. ``main`` carries +hundreds of direct URL requirements for the wheel indexes, so every merge from it can +break publishing, and nothing else guards that. Structural checks only, no network. """ from __future__ import annotations @@ -43,13 +39,10 @@ def _all_requirements() -> list[tuple[str, str]]: class TestNoDirectUrlRequirements: def test_no_requirement_uses_a_direct_url(self): - """Ask packaging, not the spelling. - - PEP 508 allows any whitespace around the `@`, and the scheme is case - insensitive, so `flash-attn@https://...` and `flash-attn @ HTTPS://...` are both - valid direct references that a substring match on " @ https://" would wave - through - the exact upload failure this guards. `Requirement.url` is set for - every form of them. + """Parse rather than substring-match: PEP 508 allows any whitespace around the + `@` and a case-insensitive scheme, so `flash-attn@https://...` and + `flash-attn @ HTTPS://...` both slip past a " @ https://" match. + `Requirement.url` is set for every form. """ packaging_requirements = pytest.importorskip("packaging.requirements") offenders = [] @@ -78,12 +71,11 @@ class TestNoDirectUrlRequirements: class TestExtraReferencesResolve: def test_every_unsloth_extra_reference_exists(self): - """A `unsloth[foo]` pointing at an extra this branch does not define installs - nothing and fails silently, which is how a partially ported extras block breaks. + """A `unsloth[foo]` naming an undefined extra installs nothing and fails silently. - Parsed and canonicalized rather than pattern-matched: project names and extra - names are both case and separator insensitive (PEP 503, PEP 685), so pip honours - `Unsloth[Rocm72_Torch2100]` while a lowercase regex would never look at it. + Canonicalized on both sides: project and extra names are case and separator + insensitive (PEP 503, PEP 685), so pip honours `Unsloth[Rocm72_Torch2100]` + while a lowercase regex would never look at it. """ packaging_requirements = pytest.importorskip("packaging.requirements") packaging_utils = pytest.importorskip("packaging.utils") @@ -108,7 +100,7 @@ class TestExtraReferencesResolve: class TestAmdExtraIsInstallableFromPyPI: """`pip install unsloth[amd]` is the supported AMD entry point, so the extra has to - exist here and stay a version floor -- a URL pin would be unpublishable.""" + exist here and stay a version floor: a URL pin would be unpublishable.""" def test_amd_extra_exists_and_floors_bitsandbytes(self): extras = _load()["project"].get("optional-dependencies", {}) @@ -127,9 +119,8 @@ class TestAmdExtraIsInstallableFromPyPI: 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.""" + """The base install must cover every module-scope import on the CLI entry path: + typer supplied click until 0.26.0 dropped it (#7504), and supplies rich by chance.""" ENTRY_PATH_IMPORTS = ("click", "rich", "structlog", "typer") @@ -147,9 +138,8 @@ class TestRuntimeImportsAreDeclared: 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.""" + """`-ampere-` variants add flash-attn and torch 2.10 variants add the torchcodec + audio path; dropping one installs a quietly weaker stack than its siblings.""" def test_torch2100_extras_pull_the_audio_path(self): extras = _load()["project"].get("optional-dependencies", {}) diff --git a/tests/python/test_torchcodec_torch_compat.py b/tests/python/test_torchcodec_torch_compat.py index b90f4ccba8..093b7afedb 100644 --- a/tests/python/test_torchcodec_torch_compat.py +++ b/tests/python/test_torchcodec_torch_compat.py @@ -45,9 +45,8 @@ 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.""" + """ROCm extras pin torch by direct URL, which PyPI rejects, so this branch omits + them: check whichever torch 2.10 extras it defines, and require at least one.""" text = PYPROJECT.read_text(encoding = "utf-8") checked = 0 for extra in (