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_manual declares a real dependency static analysis cannot see. It is merged into requires.features, and check:contracts says when one is missing.
  • routes_manual declares routes built from configuration, which have no literal path to find. They are merged into provides.routes.
  • category and tags describe the feature in the marketplace.
  • env = "dev" or "prod" places the feature in features_dev or features_prod when 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


Back to top

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