Files
wildminder-ComfyUI-Chatterbox/tests/test_pyproject.py
T

284 lines
12 KiB
Python

"""S3.3/S3.4/S3.5 -- packaging.
Two failure modes this file exists to prevent:
* **Duplication.** ``requirements.txt`` and ``pyproject.toml`` disagree, and
the version nobody reads is the one ComfyUI Manager installs. The current
repository already ships the ``tqdm`` version of that bug.
* **Re-declaring the host environment.** Naming ``torch`` in a custom node's
requirements makes ComfyUI Manager try to reconcile it with the user's build.
ComfyUI bundles the ML stack; this node must declare only what it adds.
The deliberate absence of version pins is asserted too. A pin here downgrades a
package the user already has, which is exactly the fight we are avoiding.
"""
from __future__ import annotations
import os
import re
import pytest
from conftest import ROOT_DIR, COMFYUI_ROOT
REQUIREMENTS_PATH = os.path.join(ROOT_DIR, "requirements.txt")
PYPROJECT_PATH = os.path.join(ROOT_DIR, "pyproject.toml")
try: # Python >= 3.11
import tomllib
except ModuleNotFoundError: # pragma: no cover - Python 3.10
try:
import tomli as tomllib # type: ignore[no-redef]
except ModuleNotFoundError:
tomllib = None # type: ignore[assignment]
# What the ComfyUI environment already provides. Declaring any of these is the
# mistake; see the module docstring.
COMFYUI_BUNDLED: frozenset[str] = frozenset(
{
"torch",
"torchaudio",
"transformers",
"huggingface_hub",
"numpy",
"librosa",
"soundfile",
"safetensors",
"tokenizers",
"einops",
"scipy",
"tqdm",
"accelerate",
"diffusers",
}
)
# Everything ComfyUI must install for this node to work at all.
REQUIRED_DEPS: frozenset[str] = frozenset({"s3tokenizer"})
OPTIONAL_EXTRAS: dict[str, frozenset[str]] = {
"watermarking": frozenset({"resemble-perth"}),
"multilingual": frozenset({"pykakasi", "spacy-pkuseg"}),
"turbo": frozenset({"pyloudnorm"}),
}
# A requirement is a bare distribution name. Anything with a version operator,
# an environment marker or a URL is a pin or a lock, and both are banned.
_REQUIREMENT_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
def read_requirements() -> list[str]:
with open(REQUIREMENTS_PATH, "r", encoding="utf-8") as handle:
return [
line.strip()
for line in handle
if line.strip() and not line.strip().startswith("#")
]
def read_pyproject() -> dict:
if tomllib is None: # pragma: no cover - only on a 3.10 interpreter
pytest.skip("no TOML parser available (need Python >= 3.11 or `tomli`)")
with open(PYPROJECT_PATH, "rb") as handle:
return tomllib.load(handle)
def project(pyproject: dict) -> dict:
return pyproject["project"]
def distribution_name(requirement: str) -> str:
return requirement.split("[", 1)[0].strip()
@pytest.fixture(scope="module")
def requirements() -> list[str]:
return read_requirements()
@pytest.fixture(scope="module")
def pyproject() -> dict:
return read_pyproject()
def version_tuple(version: str) -> tuple[int, ...]:
"""Numeric prefix of a version, e.g. ``0.37.0.dev1`` -> ``(0, 37, 0)``."""
numbers = re.match(r"^(\d+(?:\.\d+)*)", version)
assert numbers, f"cannot interpret version {version!r}"
return tuple(int(part) for part in numbers.group(1).split("."))
def satisfies(actual: str, specifier: str) -> bool:
"""Evaluate a single-comparator PEP 440-style specifier.
Only what this repository actually declares is supported: ``>=`` and ``>``
with a dotted numeric version. Anything else is a fail, not a silent pass.
"""
match = re.fullmatch(r"\s*(>=|>|<=|<|==|!=)\s*([\w.+!-]+)\s*", specifier)
assert match, f"unsupported version specifier {specifier!r}"
operator, wanted = match.groups()
left, right = version_tuple(actual), version_tuple(wanted)
length = max(len(left), len(right))
left += (0,) * (length - len(left))
right += (0,) * (length - len(right))
return {
">=": left >= right,
">": left > right,
"<=": left <= right,
"<": left < right,
"==": left == right,
"!=": left != right,
}[operator]
class TestRequirementsPyprojectParity:
"""``requirements.txt`` and ``pyproject.toml`` must tell one story."""
def test_requirements_are_unpinned_bare_names(self, requirements: list[str]) -> None:
for requirement in requirements:
assert _REQUIREMENT_NAME.match(requirement), (
f"{requirement!r} is not a bare distribution name -- requirements "
f"must not carry version pins, markers or URLs"
)
def test_pyproject_dependencies_are_unpinned_bare_names(self, pyproject: dict) -> None:
for requirement in project(pyproject)["dependencies"]:
assert _REQUIREMENT_NAME.match(distribution_name(requirement)), (
f"{requirement!r} pins or qualifies a dependency"
)
def test_extras_are_unpinned_bare_names(self, pyproject: dict) -> None:
for extra, packages in project(pyproject).get("optional-dependencies", {}).items():
for requirement in packages:
assert _REQUIREMENT_NAME.match(distribution_name(requirement)), (
f"extra {extra!r} pins {requirement!r}"
)
def test_no_version_pins_anywhere(self, pyproject: dict, requirements: list[str]) -> None:
everything = list(requirements)
everything += list(project(pyproject)["dependencies"])
for packages in project(pyproject).get("optional-dependencies", {}).values():
everything += list(packages)
pins = [item for item in everything if not _REQUIREMENT_NAME.match(distribution_name(item))]
assert not pins, f"version pins found: {pins}"
def test_requirements_are_a_subset_of_pyproject(self, pyproject: dict, requirements: list[str]) -> None:
declared = {distribution_name(d) for d in project(pyproject)["dependencies"]}
missing = sorted(set(requirements) - declared)
assert not missing, (
f"{missing} are installed from requirements.txt but not declared in "
f"pyproject.toml; the two must agree or the registry build differs "
f"from the local install"
)
def test_every_hard_dependency_is_declared(self, pyproject: dict) -> None:
declared = {distribution_name(d) for d in project(pyproject)["dependencies"]}
assert REQUIRED_DEPS <= declared, f"missing hard dependencies: {REQUIRED_DEPS - declared}"
def test_no_comfyui_bundled_package_is_required(self, requirements: list[str]) -> None:
bundled = sorted(set(requirements) & COMFYUI_BUNDLED)
assert not bundled, (
f"{bundled} ship with ComfyUI; re-declaring them makes ComfyUI "
f"Manager fight the user's torch build"
)
def test_no_comfyui_bundled_package_is_declared(self, pyproject: dict) -> None:
declared = {distribution_name(d) for d in project(pyproject)["dependencies"]}
bundled = sorted(declared & COMFYUI_BUNDLED)
assert not bundled, f"{bundled} ship with ComfyUI and must not be declared"
@pytest.mark.parametrize("extra", sorted(OPTIONAL_EXTRAS))
def test_extra_exists_with_the_documented_packages(self, pyproject: dict, extra: str) -> None:
packages = project(pyproject).get("optional-dependencies", {}).get(extra)
assert packages is not None, f"the {extra!r} extra is missing"
assert {distribution_name(p) for p in packages} == OPTIONAL_EXTRAS[extra]
def test_optional_packages_are_not_hard_requirements(self, pyproject: dict, requirements: list[str]) -> None:
optional = set().union(*OPTIONAL_EXTRAS.values())
hard = set(requirements) | {distribution_name(d) for d in project(pyproject)["dependencies"]}
assert not optional & hard, (
f"{sorted(optional & hard)} are optional extras and must not be "
f"hard requirements -- the node works without them"
)
def test_diffusers_is_in_neither_list(self, pyproject: dict, requirements: list[str]) -> None:
"""diffusers 0.27 cannot import under huggingface_hub 1.x, and the
vendored tree no longer imports it. Re-adding it re-adds the breakage."""
assert "diffusers" not in requirements
declared = {distribution_name(d) for d in project(pyproject)["dependencies"]}
assert "diffusers" not in declared
def test_conformer_and_omegaconf_are_in_neither_list(self, pyproject: dict, requirements: list[str]) -> None:
"""Both were dead imports in the vendored tree and were dropped on
2026-10-07. Re-adding them re-adds dead code."""
declared = {distribution_name(d) for d in project(pyproject)["dependencies"]}
for package in ("conformer", "omegaconf"):
assert package not in requirements
assert package not in declared
def test_the_old_tqdm_parity_bug_is_gone(self, pyproject: dict) -> None:
"""It used to be declared in pyproject but missing from requirements."""
declared = {distribution_name(d) for d in project(pyproject)["dependencies"]}
assert "tqdm" not in declared
assert "tqdm" not in read_requirements()
class TestPyprojectMetadata:
def test_pyproject_parses(self, pyproject: dict) -> None:
assert project(pyproject)["name"]
def test_name_is_the_registered_name(self, pyproject: dict) -> None:
"""The registry entry is immutable: do not rename the package.
The node is registered on comfyregistry as ``ComfyUI-ChatterboxTTS``;
changing the ``name`` breaks the registry identity (and
ComfyUI-Manager rewrites the file back anyway). If this test fails,
revert the edit -- the name is not ours to change.
"""
assert project(pyproject)["name"] == "ComfyUI-ChatterboxTTS"
def test_registry_identity_is_present(self, pyproject: dict) -> None:
comfy = pyproject["tool"]["comfy"]
assert comfy["PublisherId"].strip()
assert comfy["DisplayName"].strip()
def test_requires_python_is_declared(self, pyproject: dict) -> None:
specifier = project(pyproject)["requires-python"]
assert satisfies("3.10", specifier)
assert satisfies("3.13", specifier)
def test_requires_comfyui_is_declared(self, pyproject: dict) -> None:
assert "requires-comfyui" in pyproject["tool"]["comfy"]
def test_requires_comfyui_is_satisfied_by_the_running_core(self, pyproject: dict) -> None:
specifier = pyproject["tool"]["comfy"]["requires-comfyui"]
core_version = _running_comfyui_version()
assert satisfies(core_version, specifier), (
f"the running ComfyUI core is {core_version}, which does not satisfy "
f"requires-comfyui = {specifier!r}"
)
def test_classifiers_are_present(self, pyproject: dict) -> None:
classifiers = project(pyproject).get("classifiers", [])
assert "Programming Language :: Python :: 3" in classifiers
assert any(c.startswith("Development Status ::") for c in classifiers)
def test_version_is_the_release_version(self, pyproject: dict) -> None:
"""S9.3 landed the release bump; the cross-file sync lives in
``tests/test_version_sync.py``, which also compares against the
changelog's top heading."""
assert project(pyproject)["version"] == "2.0.0"
def test_repository_url_is_preserved(self, pyproject: dict) -> None:
assert project(pyproject)["urls"]["Repository"].endswith("ComfyUI-Chatterbox")
def _running_comfyui_version() -> str:
"""Version of the ComfyUI checkout the suite is pointed at."""
path = os.path.join(COMFYUI_ROOT, "comfyui_version.py")
with open(path, "r", encoding="utf-8") as handle:
source = handle.read()
match = re.search(r"""__version__\s*=\s*["']([^"']+)["']""", source)
assert match, f"cannot read __version__ from {path}"
return match.group(1)