Tighten the comments on the PyPI metadata guards

This commit is contained in:
unslothai 2026-07-29 03:38:40 +00:00
commit d838353e9f
3 changed files with 32 additions and 53 deletions

View file

@ -59,8 +59,7 @@ dependencies = [
# imports that, so every command needs it. typer supplied it until 0.27 # imports that, so every command needs it. typer supplied it until 0.27
# dropped the dependency, which left this satisfied only by chance. # dropped the dependency, which left this satisfied only by chance.
"click>=8.0", "click>=8.0",
# unsloth_cli/__init__.py imports commands/chat.py, which imports rich at # Imported at module level on the CLI entry path. typer still pulls rich, but it
# 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. # pulled click too until 0.26.0 dropped it: same shape, so declare it.
"rich", "rich",
] ]
@ -157,12 +156,9 @@ huggingface = [
"pyyaml", "pyyaml",
"nest-asyncio", "nest-asyncio",
] ]
# "notorch" means this list declares no torch of its own, not that installing it # "notorch" means this list declares no torch, not that you end up torch-free: extras
# leaves you torch-free: extras are additive to [project].dependencies, which on # are additive to [project].dependencies, which carry the full runtime here. The
# this branch deliberately carry the full runtime so a bare `pip install unsloth` # torch-free path is the installer's --no-deps pass over no-torch-runtime.txt, not pip.
# 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.
# Kept byte-identical to main so the two branches can be diffed for drift. # Kept byte-identical to main so the two branches can be diffed for drift.
huggingfacenotorch = [ huggingfacenotorch = [
"unsloth_zoo>=2026.7.6", "unsloth_zoo>=2026.7.6",
@ -184,17 +180,13 @@ huggingfacenotorch = [
"trl>=0.18.2,!=0.19.0,<=0.24.0", "trl>=0.18.2,!=0.19.0,<=0.24.0",
"sentence-transformers", "sentence-transformers",
] ]
# What this extra does on this branch is raise the bitsandbytes floor; it does not # Raises the bitsandbytes floor; it does not hand you a ROCm stack. The base deps still
# hand you a ROCm stack. The base dependencies still apply, so ROCm torch has to # apply, so ROCm torch must come from the AMD index or be installed already (pip leaves
# come from the AMD index or already be installed (pip leaves an installed # a +rocm torch alone). Use install.sh or `unsloth studio install` for a torch-free AMD install.
# +rocm torch alone, since it satisfies the base specifier). Use install.sh or
# `unsloth studio install` for a torch-free AMD install.
amd = [ amd = [
"unsloth[huggingfacenotorch]", "unsloth[huggingfacenotorch]",
# 4-bit decode is unreliable on ROCm before 0.50.0, the first PyPI release # 4-bit decode is unreliable on ROCm before 0.50.0 (bnb #1887, #1979, #2012).
# carrying the full path: blocksize/warp decoupling (bnb #1887), fused SIMT # Keep in step with install.sh and studio/install_python_stack.py.
# GEMM on RDNA (#1979), RDNA3/4 workgroup fix (#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 ; ('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')", "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)", "ninja ; ('linux' in sys_platform)",
"flash-attn>=2.6.3 ; ('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, # torchcodec releases track torch minor versions, so each torch minor gets its own
# so each torch minor gets its own window (#7225). It publishes no sdist and only # window (#7225). It ships no sdist and only x86_64 linux/windows and arm64 macOS
# manylinux_2_28_x86_64, macosx_*_arm64 and win_amd64 wheels, so Linux aarch64, # wheels, so elsewhere pip would hard-fail the whole install: gate on the platforms
# Windows ARM64 and Intel Mac have nothing to resolve and pip would hard-fail the # with a wheel, matching PLATFORM_LACKS_TORCHCODEC_WHEEL in install_python_stack.py.
# whole install. Gate on the platforms that have a wheel, matching
# PLATFORM_LACKS_TORCHCODEC_WHEEL in studio/install_python_stack.py.
audio-torch280 = [ 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'))", "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'))",
] ]

View file

@ -3,14 +3,10 @@
"""This branch is what gets uploaded to PyPI, so its metadata has to satisfy PyPI's rules. """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 PyPI rejects any PEP 508 direct reference in ``Requires-Dist`` (pypi/warehouse#7136) and
``Invalid value for requires_dist. Error: Can't have direct dependency`` ``twine check`` does not catch it (pypa/twine#726): the upload just 400s. ``main`` carries
(pypi/warehouse#7136), and ``twine check`` does not catch it beforehand hundreds of direct URL requirements for the wheel indexes, so every merge from it can
(pypa/twine#726) -- the upload just 400s. ``main`` carries hundreds of direct URL break publishing, and nothing else guards that. Structural checks only, no network.
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.
""" """
from __future__ import annotations from __future__ import annotations
@ -43,13 +39,10 @@ def _all_requirements() -> list[tuple[str, str]]:
class TestNoDirectUrlRequirements: class TestNoDirectUrlRequirements:
def test_no_requirement_uses_a_direct_url(self): def test_no_requirement_uses_a_direct_url(self):
"""Ask packaging, not the spelling. """Parse rather than substring-match: PEP 508 allows any whitespace around the
`@` and a case-insensitive scheme, so `flash-attn@https://...` and
PEP 508 allows any whitespace around the `@`, and the scheme is case `flash-attn @ HTTPS://...` both slip past a " @ https://" match.
insensitive, so `flash-attn@https://...` and `flash-attn @ HTTPS://...` are both `Requirement.url` is set for every form.
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.
""" """
packaging_requirements = pytest.importorskip("packaging.requirements") packaging_requirements = pytest.importorskip("packaging.requirements")
offenders = [] offenders = []
@ -78,12 +71,11 @@ class TestNoDirectUrlRequirements:
class TestExtraReferencesResolve: class TestExtraReferencesResolve:
def test_every_unsloth_extra_reference_exists(self): def test_every_unsloth_extra_reference_exists(self):
"""A `unsloth[foo]` pointing at an extra this branch does not define installs """A `unsloth[foo]` naming an undefined extra installs nothing and fails silently.
nothing and fails silently, which is how a partially ported extras block breaks.
Parsed and canonicalized rather than pattern-matched: project names and extra Canonicalized on both sides: project and extra names are case and separator
names are both case and separator insensitive (PEP 503, PEP 685), so pip honours insensitive (PEP 503, PEP 685), so pip honours `Unsloth[Rocm72_Torch2100]`
`Unsloth[Rocm72_Torch2100]` while a lowercase regex would never look at it. while a lowercase regex would never look at it.
""" """
packaging_requirements = pytest.importorskip("packaging.requirements") packaging_requirements = pytest.importorskip("packaging.requirements")
packaging_utils = pytest.importorskip("packaging.utils") packaging_utils = pytest.importorskip("packaging.utils")
@ -108,7 +100,7 @@ class TestExtraReferencesResolve:
class TestAmdExtraIsInstallableFromPyPI: class TestAmdExtraIsInstallableFromPyPI:
"""`pip install unsloth[amd]` is the supported AMD entry point, so the extra has to """`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): def test_amd_extra_exists_and_floors_bitsandbytes(self):
extras = _load()["project"].get("optional-dependencies", {}) extras = _load()["project"].get("optional-dependencies", {})
@ -127,9 +119,8 @@ class TestAmdExtraIsInstallableFromPyPI:
class TestRuntimeImportsAreDeclared: class TestRuntimeImportsAreDeclared:
"""This branch's base install has to satisfy every module-scope import on the CLI """The base install must cover every module-scope import on the CLI entry path:
entry path. typer supplied click until 0.26.0 dropped it (#7504); it still supplies typer supplied click until 0.26.0 dropped it (#7504), and supplies rich by chance."""
rich, so rich is satisfied only by chance unless we declare it ourselves."""
ENTRY_PATH_IMPORTS = ("click", "rich", "structlog", "typer") ENTRY_PATH_IMPORTS = ("click", "rich", "structlog", "typer")
@ -147,9 +138,8 @@ class TestRuntimeImportsAreDeclared:
class TestAcceleratorExtrasCarryTheirCompanions: class TestAcceleratorExtrasCarryTheirCompanions:
"""Each accelerator extra composes a stack: `-ampere-` variants add flash-attn and """`-ampere-` variants add flash-attn and torch 2.10 variants add the torchcodec
torch 2.10 variants add the torchcodec audio path. A variant that silently drops one audio path; dropping one installs a quietly weaker stack than its siblings."""
installs a quietly weaker environment than its siblings."""
def test_torch2100_extras_pull_the_audio_path(self): def test_torch2100_extras_pull_the_audio_path(self):
extras = _load()["project"].get("optional-dependencies", {}) extras = _load()["project"].get("optional-dependencies", {})

View file

@ -45,9 +45,8 @@ def _stub_torch(monkeypatch, version: str):
def test_torch210_extras_bundle_audio_torch210(): def test_torch210_extras_bundle_audio_torch210():
"""The ROCm extras pin torch from the AMD wheel index, which PyPI rejects as a """ROCm extras pin torch by direct URL, which PyPI rejects, so this branch omits
direct reference, so this branch does not carry them. Check whichever torch 2.10 them: check whichever torch 2.10 extras it defines, and require at least one."""
extras it does define, and require at least one."""
text = PYPROJECT.read_text(encoding = "utf-8") text = PYPROJECT.read_text(encoding = "utf-8")
checked = 0 checked = 0
for extra in ( for extra in (