pip: add the amd and huggingfacenotorch extras, and guard against unpublishable metadata
This commit is contained in:
parent
0d320c5b56
commit
fa5bb096cc
2 changed files with 132 additions and 0 deletions
|
|
@ -153,6 +153,35 @@ huggingface = [
|
|||
"pyyaml",
|
||||
"nest-asyncio",
|
||||
]
|
||||
huggingfacenotorch = [
|
||||
"unsloth_zoo>=2026.7.6",
|
||||
"wheel>=0.42.0",
|
||||
"packaging",
|
||||
"numpy",
|
||||
"tqdm",
|
||||
"psutil",
|
||||
"tyro",
|
||||
"protobuf",
|
||||
"sentencepiece>=0.2.0",
|
||||
"datasets>=3.4.1,!=4.0.*,!=4.1.0,<4.4.0",
|
||||
"accelerate>=0.34.1",
|
||||
"peft>=0.18.0,!=0.11.0",
|
||||
"huggingface_hub>=0.34.0",
|
||||
"hf_transfer",
|
||||
"diffusers",
|
||||
"transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,!=5.0.0,!=5.1.0,<=5.5.0",
|
||||
"trl>=0.18.2,!=0.19.0,<=0.24.0",
|
||||
"sentence-transformers",
|
||||
]
|
||||
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.
|
||||
"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')",
|
||||
]
|
||||
windows = [
|
||||
"unsloth[huggingface]",
|
||||
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0 ; (sys_platform == 'win32')",
|
||||
|
|
|
|||
103
tests/python/test_pyproject_pypi_installable.py
Normal file
103
tests/python/test_pyproject_pypi_installable.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
PYPROJECT = REPO_ROOT / "pyproject.toml"
|
||||
|
||||
_EXTRA_REF = re.compile(r"unsloth\[([\w\-\.,\s]+)\]")
|
||||
|
||||
|
||||
def _load() -> dict:
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
tomllib = pytest.importorskip("tomli")
|
||||
return tomllib.loads(PYPROJECT.read_text(encoding = "utf-8"))
|
||||
|
||||
|
||||
def _all_requirements() -> list[tuple[str, str]]:
|
||||
"""(extra name, requirement string) for the base deps and every extra."""
|
||||
project = _load()["project"]
|
||||
items = [("<base>", r) for r in project.get("dependencies", [])]
|
||||
for extra, deps in project.get("optional-dependencies", {}).items():
|
||||
items += [(extra, r) for r in deps]
|
||||
return items
|
||||
|
||||
|
||||
class TestNoDirectUrlRequirements:
|
||||
def test_no_requirement_uses_a_direct_url(self):
|
||||
offenders = [
|
||||
(extra, req)
|
||||
for extra, req in _all_requirements()
|
||||
if "@ http://" in req or "@ https://" in req or "@ git+" in req
|
||||
]
|
||||
assert offenders == [], (
|
||||
"PyPI rejects direct references in Requires-Dist, so these would fail the "
|
||||
f"upload (only found at publish time): {offenders[:5]}"
|
||||
)
|
||||
|
||||
def test_every_requirement_parses(self):
|
||||
packaging_requirements = pytest.importorskip("packaging.requirements")
|
||||
bad = []
|
||||
for extra, req in _all_requirements():
|
||||
try:
|
||||
packaging_requirements.Requirement(req)
|
||||
except Exception as exc: # noqa: BLE001 - report every malformed spec at once
|
||||
bad.append((extra, req, str(exc)))
|
||||
assert bad == [], f"unparseable requirements: {bad}"
|
||||
|
||||
|
||||
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."""
|
||||
extras = _load()["project"].get("optional-dependencies", {})
|
||||
dangling = []
|
||||
for name, deps in extras.items():
|
||||
for dep in deps:
|
||||
for match in _EXTRA_REF.finditer(dep):
|
||||
for ref in match.group(1).split(","):
|
||||
ref = ref.strip()
|
||||
if ref and ref not in extras:
|
||||
dangling.append((name, ref))
|
||||
assert dangling == [], f"extras referencing undefined extras: {dangling}"
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
def test_amd_extra_exists_and_floors_bitsandbytes(self):
|
||||
extras = _load()["project"].get("optional-dependencies", {})
|
||||
assert "amd" in extras, "pyproject.toml must define an `amd` extra"
|
||||
specs = [d for d in extras["amd"] if d.lower().startswith("bitsandbytes")]
|
||||
assert specs, "the amd extra must constrain bitsandbytes"
|
||||
for spec in specs:
|
||||
assert spec.startswith("bitsandbytes>=0.50.0"), (
|
||||
f"bitsandbytes <= 0.49.2 NaNs at 4-bit decode on ROCm; got {spec!r}"
|
||||
)
|
||||
|
||||
def test_amd_extra_pulls_the_torch_free_runtime(self):
|
||||
extras = _load()["project"].get("optional-dependencies", {})
|
||||
assert any("huggingfacenotorch" in d for d in extras["amd"])
|
||||
assert "huggingfacenotorch" in extras
|
||||
Loading…
Add table
Add a link
Reference in a new issue