A Python monorepo
Keep your distributions in one repository and retain the Python tools that build and publish them. dispat can keep
pyproject.toml and requirements.txt current; the first walkthrough below uses uv.
For pip, PyPA build/Twine or a Pants monorepo, start with Python without uv, including Pants. It covers build metadata, shared version sources and the boundary between dispat's release graph and Pants' build graph.
The layout
packages/core/pyproject.toml acme-core 1.2.0
packages/app/pyproject.toml acme-app 0.4.1, depends on acme-core
packages/app/requirements.txt the pins the container image installs
dispat.json
The configuration
{
"scripts": {
"build": "uv build",
"publish": "uv publish",
"lock": "uv lock"
},
"spaces": {
"packages": {
"path": "packages",
"flow": {"build": "build", "publish": "publish"},
"autoVersion": {"enabled": true, "syncLock": ["lock"]}
}
}
}
You can use Poetry, PDM, Hatch, or plain build plus twine. They all drop into the same two scripts. Nothing in the
configuration is uv-specific except the commands themselves.
Check autoVersion.manifests before you need it. It defaults to root to reconcile the manifests directly in the
package folder. Set it to all when a package keeps a second manifest deeper down, such as a deploy/requirements.txt
next to a Dockerfile.
A release
The acme-app package has no commits of its own here. It moves because the commit on core asked for its dependants
with ^:
$ git commit -m "feat(core)^: stream responses"
$ dispat status
12:39:15 INF ● changed baselineFromInitials=true bump=minor channel=stable dueToProviders=[] ownCommits=1 package=core reason=direct space=packages version="1.2.0 -> 1.3.0"
12:39:15 INF ● changed baselineFromInitials=true bump=patch channel=stable dependsOn=["core"] dueToProviders=["core"] ownCommits=0 package=app reason="propagated from core" space=packages version="0.4.1 -> 0.4.2"
12:39:15 INF release plan ready held=0 packages=2 releasing=2
Leave out the ^ to release core alone. The app package stays where it is until it next has a reason of its own.
Propagation is opt-in so that one library fix does not rebuild the world.
What the version stage does
$ dispat autoversion
12:39:15 INF manifest reconciled manifest=pyproject.toml package=core ranges=0 stage=version versionWritten=true
12:39:15 INF manifest reconciled manifest=pyproject.toml package=app ranges=1 stage=version versionWritten=true
12:39:15 INF manifest reconciled manifest=requirements.txt package=app ranges=1 stage=version versionWritten=false
12:39:15 INF auto-versioning finished failed=0 ran=2 skipped=0 stage=autoversion
dispat updates three files across two packages in one pass. A status of versionWritten=false on the requirements file
is not a failure because that format has no version of its own to write, only pins to reconcile. The result:
[project]
name = "acme-app"
version = "0.4.2"
dependencies = [
"acme-core==1.3.0",
"httpx>=0.27.0",
]
# runtime pins for the container image
acme-core==1.3.0
httpx==0.27.2
Only the version text moved. The comment, the ordering and httpx are exactly as they were. This makes the release
commit reviewable.
What dispat reads and writes
$ dispat scanner
packages/app/pyproject.toml python acme-app@0.4.1
dependencies acme-core ==1.2.0
dependencies httpx >=0.27.0
devDependencies pytest >=8.3.0
packages/app/requirements.txt python
dependencies acme-core ==1.2.0
dependencies httpx ==0.27.2
packages/core/pyproject.toml python acme-core@1.2.0
dependencies httpx >=0.27.0
3 manifest(s), 6 dependency declaration(s)
pyproject.tomlis read as PEP 621 first ([project]), falling back to[tool.poetry]. Optional dependencies become optional ones. Both PEP 735[dependency-groups]and non-main Poetry groups become dev dependencies.- Requirements files match by whole words. Files like
requirements.txt,dev-requirements.txtandrequirements-ci.txtall count, and a file withdevortestin its name is read as dev dependencies. dispat reports an editable local install,-e ./core, as a link to that folder. - Names are normalised the way PyPI normalises them (PEP 503). This means
Acme_Coreandacme-coreare one package. - Python ranges are written as
==X.Y.Z. Acaretortildepolicy is an npm idea. The Python writers use the spelling the ecosystem actually resolves.
Building against the package next door
Pass --link to write a [tool.uv.sources] path entry. The empty form removes it:
dispat autowriter --since all --link-local # develop against the working tree
dispat autowriter --since all --unlink-local # before anything is uploaded
dispat scanner --verify-unlinked # E215 if one survived
Worth knowing
- A version on PyPI cannot be replaced. Test in the build stage and upload in the publish stage. Let the ordering guarantee that a dependency is on the index before its consumer needs it.
uv lockruns after the manifests are reconciled. This is whatsyncLockmeans. The lock follows the manifest rather than choosing versions of its own.- A workspace-wide lock file lives at the repository root, outside every package folder. List it under
commit.includeso the release commit carries it. - Publishing needs credentials, once per space. A token in the environment is enough for
uv publish. If your registry needs a login command, theflow.loginslot runs it once per space per run.
See also
- Read autoVersion for
manifests,matchandsyncLockin full. - Read An npm monorepo for the same shape in a different ecosystem.
- Read A Docker image chain if the requirements file exists to feed an image.
Check the release file set
Define completion as the expected set of files, not as one successful upload command. Record the filename and digest of each sdist and wheel that PyPI accepted. If an upload may have succeeded before the release job recorded it, query the index before retrying: an accepted PyPI filename cannot be reused. A release can still need additional wheel files; compare the expected inventory rather than skipping the whole version because one file exists.
The publisher must reconcile files within the package before a retry; dispat records completion at the package boundary. It can order and retry that publisher, but cannot raise a quota or repair credentials. Perform remote reconciliation outside the publish command, then let a successful upload return immediately so dispat can write its release records. See release recovery.
See From one package to many to add deliverables while preserving existing package identities and release history.