pyproject.toml


Table of contents

  1. Introduction
  2. What a product’s file holds
    1. Choosing where a feature loads
    2. Pinning the SPL model
    3. Pinning what the image installs
    4. Installing a feature published under another name
    5. Handing variables to your product’s tests
    6. Deciding feature settings
  3. Commands that edit it
  4. What a feature’s file holds
    1. The feature contract
    2. The Docker contract
  5. 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

The tables of a product's pyproject.toml and what reads each one THE PRODUCT'S PYPROJECT.TOML READ BY [project] name, version, Python dependencies pip, product:release [tool.splent] features, features_dev, features_prod, spl the framework at boot, product:resolve, product:up, db:upgrade, product:build [tool.splent.spl_model] the DOI of the UVL model spl:*, product:validate, feature:order [tool.splent.runtime] framework, CLI and gunicorn pins product:build, the production Dockerfile [tool.splent.distributions] PyPI names that are not the feature's own feature:pip-install, in the image build [tool.splent.config] the feature settings the product decides product:env --merge, product:build
Each table has its readers. The feature lists are the ones almost every command reads.
[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:build never reads env itself. A development feature written by hand into features ships to production, so keep it in features_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.


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es