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
# 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'))",
]

View file

@ -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", {})

View file

@ -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 (