A Python monorepo
Distributions in one repository, built and uploaded with uv, with pyproject.toml and the requirements.txt beside
it both kept current by dispat.
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"]}
}
}
}
Poetry, PDM, Hatch, and plain build plus twine, all drop into the same two scripts; nothing above is uv-specific
except the commands themselves.
One option to know about before you need it: autoVersion.manifests defaults to root, which reconciles 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
acme-app 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
Without the ^, core would release alone and app would stay 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
Three files, two packages, one pass. versionWritten=false on the requirements file is not a failure: 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, which is what 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, and both PEP 735[dependency-groups]and non-main Poetry groups become dev dependencies.- Requirements files match by whole words, so
requirements.txt,dev-requirements.txtandrequirements-ci.txtall count and a file withdevortestin its name is read as dev dependencies. An editable local install,-e ./core, is reported as a link to that folder. - Names are normalised the way PyPI normalises them (PEP 503), so
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
--link writes a [tool.uv.sources] path entry, and 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, upload in the publish stage, and let the ordering guarantee that a dependency is on the index before its consumer needs it.
uv lockruns after the manifests are reconciled. That 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
- autoVersion for
manifests,matchandsyncLockin full. - An npm monorepo for the same shape in a different ecosystem.
- A Docker image chain if the requirements file exists to feed an image.