pyproject.toml
Table of contents
- Introduction
- What a product’s file holds
- Commands that edit it
- What a feature’s file holds
- Who reads what
Introduction
Your product’s pyproject.toml says which features it runs, at which versions, which SPL model it follows and what its production image installs. A feature’s pyproject.toml carries its contract, what it provides and requires. There is no other file for any of this.
[tool.splent]
spl = "my_spl"
features = [
"splent-io/splent_feature_notes@v1.5.8",
"splent-io/splent_feature_reports",
]
This product follows the line my_spl and runs two features, one pinned and one editable.
What a product’s file holds
[project]
name = "my_product"
version = "0.0.1"
requires-python = ">=3.13"
[project.optional-dependencies]
dev = ["watchdog"]
core = ["splent_framework"]
[tool.splent]
cli_version = "1.44.0"
spl = "my_spl"
features = [
"splent-io/splent_feature_notes@v1.5.8",
"splent-io/splent_feature_notes_tags@v0.4.0",
"splent-io/splent_feature_cache@v1.5.6",
"splent-io/splent_feature_reports",
]
features_dev = []
features_prod = []
[tool.splent.spl_model]
mirror = "uvlhub.io"
doi = "10.5281/zenodo.10000002"
concept_doi = "10.5281/zenodo.10000001"
version = "v2"
file = "my_spl.uvl"
[tool.splent.runtime]
splent_framework = "1.26.0"
splent_cli = "1.44.0"
gunicorn = ">=23,<24"
Every feature you list must belong to the SPL that spl names. product:validate rejects any other with Unknown feature(s) not in UVL. cli_version records which CLI’s templates the product came from, for product:drift.
Choosing where a feature loads
| Key | Loaded | Typical content |
|---|---|---|
features |
In every environment | What the product needs everywhere |
features_dev |
With SPLENT_ENV=dev |
Debug panels, fake data, mail catchers |
features_prod |
With SPLENT_ENV=prod |
Monitoring, caching, analytics |
Development runs features + features_dev, and production runs features + features_prod. An entry with @<tag> is pinned and one without is editable. See Editable and pinned features.
A feature is the same whatever the spelling of its org (splent-io, splent_io and splent.io are one org), and an entry without an org is splent-io. Declaring the same feature twice with different versions, or once pinned and once editable, is an error.
Pinning the SPL model
[tool.splent.spl_model] turns the name in spl into a download, so any clone fetches the model from UVLHub with nothing else. Write it with spl:pin, not by hand.
| Key | Meaning |
|---|---|
mirror |
Where the model is published. Only uvlhub.io is implemented |
doi |
The DOI of this version, which pins the exact bytes |
concept_doi |
The DOI of the line. spl:outdated uses it to find a newer model |
version |
A label for people, and the cache directory’s name |
file |
The model’s filename on UVLHub. Without it, <name>.uvl |
A product that names no SPL can point at a model file with [tool.splent.uvl].file instead. See UVL feature model.
Pinning what the image installs
[tool.splent.runtime] pins what the image installs besides the features. product:build sets splent_framework and splent_cli on every build, and gunicorn is yours to change. A bare version means exactly that version, anything else is a pip specifier. A package you add is installed and left alone. Third-party packages are pinned apart, in docker/requirements.lock.
Installing a feature published under another name
[tool.splent.distributions]
splent_feature_notes = "splent-feature-notes-alice"
When a version was published under another [project].name, the image installs that distribution. feature:attach writes the entry, and feature:detach and feature:remove remove it.
Handing variables to your product’s tests
[tool.splent.test]
env = ["MIGRATION_V2_TEST_*"]
product:test hands these variables of your environment to your product’s own tests, in the web container too. Each entry is a name or a prefix ending in *. SPLENT_* and MARIADB_* are never handed over.
Deciding feature settings
[tool.splent.config] holds the feature settings your product decides. See Feature settings.
Commands that edit it
feature:add, feature:attach, feature:detach, feature:remove, feature:pin, feature:unlock, feature:rename, product:configure and product:auto-require write your product’s pyproject.toml. feature:upgrade writes its pins, and product:build writes [tool.splent.runtime].
None of them rewrites the file. Each edits only the entries it changes. Comments, blank lines, key order and quote style stay as they were, so the comments that document your settings survive any number of commands.
- A changed entry is rewritten between its quotes, so a comment after it stays.
- A new entry goes on its own line after the one before it, with the same indentation.
- A removed entry takes its line with it, comment included.
- Windows line endings stay.
When an edit cannot be made exactly, for instance with dotted keys (splent.features = [...] under [tool]), the command writes nothing, prints <path> was left as it was, because <reason> and exits 1. feature:rename is the exception. It warns, renames the feature anyway and leaves the entry for you.
What a feature’s file holds
A feature is a Python package with its own pyproject.toml. Besides the package metadata, it holds the feature contract.
[project]
name = "splent_feature_notes"
version = "1.5.8"
requires-python = ">=3.13"
[tool.splent.contract]
description = "Notes with a list, an editor and an admin screen"
archetype = "full"
[tool.splent.contract.provides]
routes = ["/notes/", "/notes/create"]
models = ["Notes"]
services = ["NotesService"]
[tool.splent.contract.requires]
features = []
env_vars = ["NOTES_NAV", "NOTES_PATH"]
[tool.splent.contract.extensible]
services = ["NotesService"]
templates = ["notes/index.html"]
The feature contract
feature:contract --write generates the contract from the source, and feature:release refreshes it, so you never maintain it by hand. Feature contract explains each field and what it is inferred from.
A feature that belongs in one environment says so with env = "dev" or env = "prod" in [tool.splent.contract]. feature:install, feature:add, feature:attach and product:configure then place it in the right list.
product:buildnever readsenvitself. A development feature written by hand intofeaturesships to production, so keep it infeatures_dev.
A refiner declares what it refines in [tool.splent.refinement]. See Refinement.
The Docker contract
A feature that ships Docker services gets [tool.splent.contract.docker], inferred from its compose file.
[tool.splent.contract.docker]
services = ["splent_feature_proxy"]
ports = ["${PROXY_HTTPS_HOST_PORT}:443", "${PROXY_HTTP_HOST_PORT}:80"]
volumes = ["certbot_webroot", "letsencrypt"]
networks = ["splent_network"]
build = true
healthcheck = false
[tool.splent.contract.docker.depends_on]
services = []
check:infra reads it to find port conflicts, volume collisions and missing health checks.
Who reads what
| Section | Read by |
|---|---|
[project] |
pip, the framework, the CLI, the release commands |
[tool.splent] feature lists |
The framework at boot, product:resolve, product:up, db:upgrade, db:migrate, db:seed, product:build, the startup scripts |
[tool.splent].spl, [tool.splent.spl_model] |
spl:*, product:validate, product:missing, product:auto-require, feature:order, doctor. The running app reads the model from disk only, to order features |
[tool.splent.runtime] |
The production Dockerfile, product:build, product:release |
[tool.splent.distributions] |
feature:pip-install |
[tool.splent.config] |
product:env --merge, product:build |
[tool.splent.test] |
product:test |
[tool.splent.contract] |
feature:install, feature:add, feature:attach, feature:remove, feature:detach, product:validate, product:build, export:puml |
[tool.splent.refinement] |
The framework, product:validate, product:build |
The CLI and the framework have their own pyproject.toml in the workspace. version shows the CLI’s version from it, and release:cli and release:framework bump it.