Contract and dependencies
Table of contents
Introduction
Every feature declares in its pyproject.toml what it provides, what it requires and what others may override. You never write this by hand. The CLI generates it from your source.
splent feature:contract splent_feature_notes --write
feature:release refreshes it on every release. When config.py is missing, feature:contract --write offers to run feature:inject-config.
What a contract looks like
[tool.splent.contract]
description = "Notes with a list, an editor and an admin screen"
archetype = "full"
[tool.splent.contract.provides]
routes = ["/admin/notes", "/notes/", "/notes/<int:note_id>/delete", "/notes/create"]
blueprints = ["notes_bp"]
models = ["Notes"]
commands = ["export-notes", "purge-drafts"]
hooks = ["layout.authenticated_sidebar", "layout.sidebar.authenticated.items"]
services = ["NotesService"]
signals = ["note-created", "note-deleting"]
translations = ["es"]
docker = []
[tool.splent.contract.requires]
features = []
env_vars = ["NOTES_NAV", "NOTES_PAGE_SIZE", "NOTES_PATH", "NOTES_PLACEHOLDER"]
signals = []
[tool.splent.contract.extensible]
services = ["NotesService"]
templates = ["notes/create.html", "notes/index.html"]
models = ["Notes"]
hooks = ["notes.create.form_extra", "notes.index.before_list"]
routes = true
notes sends no mail. A feature that tells authors about a new note listens to note-created, and notes never learns it is there. env_vars names every variable config.py reads, so a product sees what it can set without reading the code. The extensible table lists what a refiner may touch. See Extension points.
Adding what the source cannot show
Regeneration keeps the fields you write by hand, which are description, category, tags, env, requires.features_manual and provides.routes_manual.
[tool.splent.contract.requires]
features_manual = ["media"]
features_manualdeclares a real dependency static analysis cannot see. It is merged intorequires.features, andcheck:contractssays when one is missing.routes_manualdeclares routes built from configuration, which have no literal path to find. They are merged intoprovides.routes.categoryandtagsdescribe the feature in the marketplace.env = "dev"or"prod"places the feature infeatures_devorfeatures_prodwhen it is installed.
A requires.features entry may carry a version floor written by hand, such as "notes>=1.20.0". A rewrite keeps it, and product:validate checks it.
Following the dependency rule
Dependencies between features are declared in the UVL model, and one rule follows from them.
If A depends on B (
A => B), A may import from B, and B never imports from A.
| Allowed | Forbidden |
|---|---|
reports imports from notes |
notes imports from reports |
pdf_export imports from reports |
reports imports from pdf_export |
To reach a feature that depends on yours, send a signal, never an import. product:validate checks the rule.
Naming things
| Element | Convention | Example |
|---|---|---|
| Package | splent_feature_<name> |
splent_feature_notes |
| Namespace | splent_io |
splent_io.splent_feature_notes |
| Blueprint | <name>_bp |
notes_bp |
| Template directory | templates/<name>/ |
templates/notes/ |
| Bundle file | <name>.bundle.js |
notes.bundle.js |
| Seeder class | <Name>Seeder |
NotesSeeder |
| Service class | <Name>Service |
NotesService |
Reference
What each field is inferred from
| Field | Inferred from |
|---|---|
archetype |
Its own models or migrations make it full, routes only light, implemented services service, anything else config |
provides.routes |
@<bp>.route(...) decorators in routes.py |
provides.blueprints |
Blueprint variables in __init__.py |
provides.models |
class <Name>(db.Model) in models.py |
provides.hooks |
register_template_hook("slot", func) calls in hooks.py |
provides.services |
class <Name>(BaseService) in services.py, and register_service(app, "Name", …) calls |
provides.signals |
define_signal("name") calls in signals.py |
provides.translations |
Language directories under translations/ |
provides.commands |
@click.command("name") decorators in commands.py, with an explicit name |
provides.docker |
The docker-compose*.yml files in docker/ |
requires.features |
Imports of other splent_feature_* packages, and service_proxy("XService") calls to services other features register |
requires.features_optional |
What the feature uses only when installed, such as a service_proxy or importlib.import_module call inside a tolerant try. Never enforced |
requires.env_vars |
os.getenv(...) and os.environ[...] calls |
requires.signals |
connect_signal("name", ...) calls in signals.py |
The requires fields are read from the feature’s code, never from its tests/.
Who reads the contract
| Reader | Reads | Why |
|---|---|---|
feature:install |
requires.features, env |
Checks dependencies, picks the feature list |
feature:add, feature:attach |
env |
Picks the feature list |
feature:remove, feature:detach |
requires.features |
Refuses while another feature depends on this one |
product:validate |
Every field | Finds conflicts between features |
product:build |
extensible and [tool.splent.refinement] |
Writes them to splent.contracts.json for the production image |
export:puml |
Every field | Draws PlantUML diagrams |
See also
- pyproject.toml, the rest of a feature’s
pyproject.toml feature:contract, infer the contract from the sourcefeature:create, scaffold a new feature