Feature loading
Table of contents
- Introduction
- How a feature is loaded
- What a feature gives the framework
- Loading features in order
- Where a feature is loaded from
- What happens to each feature
- Refining a feature
- Looking up a service
- Errors while loading
- Advanced
- Reference
- See also
Introduction
When your app starts, FeatureManager reads the features your product declares, orders them by the UVL model, checks their refinements and loads them one by one. You never call it, the app factory does. Read this page to know what a feature gives the framework, in which order, and why one did not load.
A feature needs little. A blueprint, and the hooks it uses.
# src/splent_io/splent_feature_notes/__init__.py
from splent_framework.blueprints.base_blueprint import create_blueprint
from splent_framework.services.service_locator import register_service
notes_bp = create_blueprint(__name__)
def init_feature(app):
register_service(app, "NotesService", NotesService)
# src/splent_io/splent_feature_notes/config.py
import os
def inject_config(app):
app.config.setdefault("NOTES_PAGE_SIZE", int(os.getenv("NOTES_PAGE_SIZE", "20")))
How a feature is loaded
Nothing loads until every feature is ordered and every refinement checked, so a broken declaration stops the boot before any feature runs. Then each feature goes through the same steps, in load order. A feature always loads after the features it requires, so their config, services and hooks are there when yours runs.
What a feature gives the framework
| You write | Where | The framework |
|---|---|---|
inject_config(app) |
config.py |
Calls it first, to set your config keys. See Configuration |
init_feature(app) |
__init__.py |
Calls it once, to register services, hooks, menus and the like |
| A blueprint | __init__.py |
Registers it. create_blueprint(__name__) names it after the feature |
inject_context_vars(app) |
__init__.py |
Calls it on every render and merges the dict it returns into the template’s context |
routes, models, hooks, signals |
Modules of the package | Imports them before inject_config |
translations/ |
The package | Adds it to Flask-Babel. See i18n |
cli_commands |
commands.py |
Offers them as splent feature:<name>. See product:commands |
Every hook is optional, though a feature without a blueprint loads with a warning. create_blueprint(__name__) turns splent_io.splent_feature_notes into notes, the name every url_for("notes.index") and the notes.assets route rely on. Name the blueprint by hand and both break.
Loading features in order
A feature loads after the features it requires in the UVL model, and a refiner after the feature it refines. Features that do not depend on each other keep their pyproject.toml order.
constraints
reports => notes
With features = ["reports", "notes", "mailer"], the app loads notes, then reports, then mailer. feature:order shows the order your product uses.
splent feature:order
Migrations and seeders follow the same order. Without a model on disk the pyproject.toml order is kept, with each refiner moved after its base. When your product names its SPL and no model is found, a warning says so. UVL feature model says where the model is looked for, and The SPL model in the image how a built image ships it.
A cycle stops the boot.
Circular dependency detected among features: ['notes', 'reports']. Review the 'constraints' section in your .uvl file.
So does a feature declared twice, with Duplicate features in the product.
Where a feature is loaded from
Each entry of features points at a link in your product’s features/<org>/ folder. notes@v1.2.7 is a pinned snapshot, notes alone is the editable working copy. See Editable and pinned features.
- When the exact link is missing, the first
<name>@*link is used, with a warning if it is another version. A pinned entry never falls back to the editable copy. - When no link works, the feature is imported as an installed package, as in a built image.
- When the package came from a local folder other than its link, a warning names both. Run
product:deriveto fix the links.
Loading another copy
SPLENT_FEATURE_SOURCES=splent_feature_notes=/workspace/splent_feature_notes
A feature named in SPLENT_FEATURE_SOURCES loads from that folder, the one that holds src/, instead of its link. The value is a comma separated list of name=folder pairs.
You never set it by hand. feature:test sets it so the app under test runs the copy the tests come from, and its --with adds other features. A path that is not a folder is treated like a missing link.
What happens to each feature
- Find and import. The link is resolved,
src/<org>/<name>/is checked and the package is imported, then itsroutes,models,hooksandsignals. inject_config(app). Then the keys the feature declares in[tool.splent.config_schema]are resolved and typed. A value weaker than your product’s choice is put back.- Model mixins. A refiner’s mixins are applied to its base’s models.
init_feature(app).- Blueprints. Route modules that refiners add to this feature are imported, then the feature’s own blueprints are registered.
- Context, translations and commands.
inject_context_vars,translations/andcommands.pyare picked up.
Once every feature has loaded, the refiners’ template overrides and the hook replacements that waited are applied. When the CLI is installed, each feature is then marked active in the manifest, once its migrations are applied.
Refining a feature
A refiner declares what it changes of its base in [tool.splent.refinement]. Before any feature loads, every declaration is checked against the base’s [tool.splent.contract.extensible], and the refiners against each other. Any problem stops the boot with a FeatureError, in every mode.
The refiners of this product collide, so it cannot start.
- ...
The rules are in Validation. product:validate runs the same ones before you derive.
Overriding a base’s template
Declare the path in [tool.splent.refinement.overrides] templates and ship the file at the same path in your templates/. Your copy wins, and only for the paths you declare. Templates are looked up in your product’s templates/ first, then in the declared overrides, then in each feature.
The boot refuses a file you ship without declaring when the base ships it too, and a declared path you do not ship.
In a built image
A built image ships features as packages, without their pyproject.toml. So product:build writes the declarations to splent.contracts.json in the product, and the boot reads it when a feature’s own file is not there.
{"format": 1,
"features": {
"splent_feature_notes_tags": {
"entry": "splent-io/splent_feature_notes_tags@v1.0.0",
"refinement": {"refines": "splent_feature_notes"},
"extensible": {"templates": [], "services": [], "models": [], "hooks": [], "routes": false}
}
}}
A feature found in neither place loads without its overrides, hook replacements and model extensions, and a warning says so. An entry with another version than your product declares asks you to rebuild.
Looking up a service
A feature registers its services by name in init_feature, and others look them up by that name, so a refiner can replace one without its callers knowing.
from splent_framework.services.service_locator import service_proxy
notes_service = service_proxy("NotesService")
service_proxy looks the class up again on every use, so an override always applies. get_service_class(app, name) returns the class itself. See Service locator.
Errors while loading
Every loading error is a FeatureError, from splent_framework.managers.feature_loader. The message names the feature and the cause.
| Situation | Default | strict=True |
|---|---|---|
| No link and no installed package | Error | Error |
An import error, or an exception in inject_config or init_feature |
Error | Error |
| A module of the feature that needs a package nobody installed | Error | Error |
| A refinement that fails validation, or refiners that collide | Error | Error |
| A refiner’s route module that cannot be imported | Error | Error |
A feature pyproject.toml that is not valid TOML |
Error | Error |
A [tool.splent.config_schema] that cannot be used |
Error | Error |
No config.py, inject_config or init_feature |
Skipped | Error |
| No blueprint | Warning | Error |
| Two blueprints with the same name | The second is not registered | Error |
| A model mixin that cannot be imported | Warning | Error |
| A package loaded from another copy than its link | Warning | Error |
A splent.contracts.json that cannot be read, or features in neither source |
Warning | Error |
A commands.py that needs a missing package |
Warning, no commands | Warning, no commands |
No routes, models, hooks or signals module |
Skipped | Skipped |
The factory loads with strict=False unless you ask for strict mode.
Advanced
Before loading, the boot imports each refiner’s and base’s
modelsto look for collisions. That runs a refiner’s package__init__before its base has loaded. A hook replacement it asks for waits for the end of the load.
Each boot first clears the registries features fill, such as menus, assets, settings, parameters, file access and search, so several apps built in one process see only their own features. It also reads your product’s
[tool.splent.layout.menu]and[tool.splent.csp], and a malformed table stops the boot.
Reference
FeatureManager
from splent_framework.managers.feature_manager import FeatureManager
FeatureManager(app, strict=False).register_features()
| Member | Returns | Description |
|---|---|---|
FeatureManager(app, *, strict=True) |
create_splent_app passes strict=False unless told otherwise |
|
register_features() |
None |
Orders, checks and loads every feature. A second call does nothing |
get_features() |
list[str] |
The entries for the current SPLENT_ENV, without loading |
FeatureManager.read_refines(product_dir, entries) |
dict[str, str] |
{refiner: base}, for ordering features without booting, as feature:order and db:seed do |
Feature entries
The entries are features plus features_dev or features_prod of [tool.splent], for the current SPLENT_ENV.
| Entry | Org | Version |
|---|---|---|
splent-io/splent_feature_notes@v1.2.7 |
splent-io |
v1.2.7, pinned |
splent_feature_notes@v1.2.7 |
splent-io, the default |
v1.2.7, pinned |
splent-io/splent_feature_notes |
splent-io |
None, editable |
splent_feature_notes |
splent-io, the default |
None, editable |
Each becomes a FeatureRef with org, org_safe (- and . turned into _, the folder name), name and version. ref.import_name() is splent_io.splent_feature_notes.
See also
feature:order, the load order of your product.feature:xray, the extension points, hooks and refinements of a feature.- Extensibility, refining, extending and composing features.
- Application initialization, the steps around the feature loading.