Thanks for contributing. This document covers local setup, the checks CI
requires, and the one trap that bites new contributors: comfy_low's models
are generated, and hand-editing them is invisible to the linters but fails CI.
- Python 3.10+ — CI tests 3.10, 3.11, 3.12, and 3.13.
- uv — recommended for local work;
uv.lockis committed, souv rungives you the same resolved dependency set as everyone else. Plainpipworks too (see Using pip instead).
git clone https://github.com/Comfy-Org/comfy-python-sdk
cd comfy-python-sdk
uv sync --extra dev
--extra devis load-bearing. The dev tools (ruff, mypy, pytest, pytest-asyncio, pytest-cov) are declared inpyproject.tomlunder[project.optional-dependencies]— a PEP 621 extra, not a PEP 735[dependency-groups]entry. uv installs the default dependency groups automatically, but it never installs an extra unless you ask for it. Omit--extra devandruff/mypy/pytestsimply will not be there.
Every one of these runs in CI on each pull request and must pass. Run them locally before pushing:
uv run --extra dev ruff check . # lint
uv run --extra dev ruff format --check . # formatting
uv run --extra dev mypy src # type check
uv run --extra dev pytest -v # testsruff format --check . only reports; uv run --extra dev ruff format . fixes.
A few things that will fail you that are easy to miss:
- Formatting is a gate, not a suggestion. CI runs
ruff format --check. comfy_sdkrequires full annotations.disallow_untyped_defsis on forcomfy_sdk.*only (it is the hand-written public surface and shipspy.typed).comfy_lowwraps generated code, so it is exempt.- Deprecation warnings are errors.
filterwarningsturnsDeprecationWarningandPendingDeprecationWarninginto test failures — they are the advance warning that a dependency bump is about to break the SDK. - Markers and config are strict.
--strict-markers --strict-config, so a typo'd@pytest.mark.*or a bad ini key fails rather than being ignored.
CI runs two more jobs beyond the four above:
build-check— builds the sdist and wheel and runstwine check, so a broken distribution is caught in PR CI instead of at release time.public-repo-hygiene—python3 scripts/check_public_repo_hygiene.pyscans for internal-only references. This is a public repo; the check is a permanent gate, not a one-time cleanup.
src/comfy_low/models/_generated.py is the only generated file in the repo.
It is produced by datamodel-code-generator from the vendored OpenAPI document
at spec/openapi.yaml, and it is committed.
A hand-edit of that file is invisible to every local check and still fails CI.
Both linters deliberately skip it — [tool.ruff] extend-exclude and
[tool.mypy] exclude — so its formatting stays byte-identical to the
generator's output. What catches an edit is the separate codegen-drift CI
job, which regenerates the file into a temp directory and diffs it byte-for-byte
against the committed copy.
So if you need to change a model, change spec/openapi.yaml and regenerate:
uv run --extra codegen bash scripts/gen_models.sh # regenerate
uv run --extra codegen python scripts/check_drift.py # the exact check CI runsThen commit the regenerated src/comfy_low/models/_generated.py alongside your
spec change.
Notes:
scripts/gen_models.shis a bash script — run it withbash, notpython.datamodel-code-generatoris pinned (~=0.68.1) on purpose. The drift gate compares byte-for-byte, so the generator version is load-bearing; an unpinned bump would reformat the output and flag false drift. Do not loosen that pin without regenerating in the same commit.- ruff and mypy are pinned for the same class of reason: an unpinned bump silently changes what CI catches between PRs.
uv is a convenience, not a requirement — CI itself does not use it. The CI test job installs with pip and then invokes the tools directly:
pip install -e ".[dev]"
ruff check .
ruff format --check .
mypy src
pytest -vand for codegen:
pip install -e ".[codegen]"
bash scripts/gen_models.sh
python scripts/check_drift.py| Extra | What it is for |
|---|---|
dev |
ruff, mypy, pytest, pytest-asyncio, pytest-cov — everything the required checks need |
codegen |
datamodel-code-generator + PyYAML, for regenerating comfy_low models |
pil |
Pillow, so Preview.to_pil() can decode an in-progress preview frame |
uv run --extra dev pytest -v # the suite CI runs
uv run --extra dev pytest --cov # with coverage
uv run --extra dev pytest tests/test_jobs.py -v # a single fileasyncio_mode = "auto", so async def tests need no @pytest.mark.asyncio.
tests/integration/ holds a live end-to-end suite against a real gateway. It is
env-gated and skipped unless COMFY_BASE_URL and COMFY_API_KEY are set, so it
does not run in normal CI. If your change touches upload/dedup, submission,
polling, SSE, or output download, running it against a real deployment is worth
the effort — several past releases were verified that way.
- Branch from
mainand open the PR againstmain. - Conventional commits. The history uses
feat:,fix:,docs:,chore:,ci:,test:, andfeat!:/fix!:for breaking changes. PR titles follow the same form — they become the squashed commit message. - No AI attribution trailers in commit messages (no
Co-Authored-By:for an assistant, no "Generated with ..." lines). - CLA. A first-time contributor is asked by the CLA Assistant bot to comment the signing phrase on their PR. Only the PR author needs to sign.
- Review.
.github/CODEOWNERSrequires an approving review from@Comfy-Org/comfy-cloud-teamor@Comfy-Org/core-engine-teamon every PR. - Update
CHANGELOG.md. Add a bullet under## [Unreleased]describing the user-visible change. Purely internal changes (CI, refactors with no API effect) do not need an entry. - Update the README when you change the public surface — it is the primary documentation for this SDK.
Use the issue templates. For a bug, the SDK version, the Python version, and a minimal reproducible snippet are what make it actionable.
Maintainers only. Releases are published to PyPI by
.github/workflows/publish.yml when a GitHub
Release is published with a vX.Y.Z tag, using PyPI Trusted Publishing (OIDC) —
no API token lives in this repo.
Versioning is tag-driven: the tag is the single source of truth and is
injected into pyproject.toml at build time, so the version committed there is
a placeholder and no version-bump commit is needed. Before cutting a
release, move the ## [Unreleased] entries in CHANGELOG.md under the new
version heading.
Running the workflow manually (workflow_dispatch) is a dry run: it builds and
runs twine check but never reaches the publish job.