Feature loading

Table of contents

  1. Introduction
  2. How a feature is loaded
  3. What a feature gives the framework
  4. Loading features in order
  5. Where a feature is loaded from
    1. Loading another copy
  6. What happens to each feature
  7. Refining a feature
    1. Overriding a base’s template
    2. In a built image
  8. Looking up a service
  9. Errors while loading
  10. Advanced
  11. Reference
    1. FeatureManager
    2. Feature entries
  12. 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

How features are discovered and loaded READS pyproject.toml declared features UVL model its constraints Contracts refines, extensible features/ a link per feature 1 Read the feature list, registries cleared 2 Order by the UVL model, refiner after base 3 Check refinements valid, no collisions 4 Load each feature, one at a time 5 Finish overrides applied, marked active EACH FEATURE, IN LOAD ORDER a Find the copy its features/ link b Check src/ src/<org>/<name>/ c Import package, submodules d inject_config from config.py e Model mixins when it is a refiner f init_feature services, hooks, nav g Blueprints routes registered h Context and i18n and CLI commands
The features are ordered and their refinements checked before any feature loads. Then each one goes through the same eight steps, in load order.

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:derive to 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

  1. Find and import. The link is resolved, src/<org>/<name>/ is checked and the package is imported, then its routes, models, hooks and signals.
  2. 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.
  3. Model mixins. A refiner’s mixins are applied to its base’s models.
  4. init_feature(app).
  5. Blueprints. Route modules that refiners add to this feature are imported, then the feature’s own blueprints are registered.
  6. Context, translations and commands. inject_context_vars, translations/ and commands.py are 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 models to 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


Back to top

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