Feature loading
The feature loading subsystem is responsible for taking the raw list of feature identifiers in pyproject.toml, sorting them into the correct dependency order, and integrating each one into the running Flask app.
Table of contents
- Overview
- FeatureManager
- FeatureRef
- Phase 1 — Parse (
FeatureEntryParser) - Phase 2 — Resolve (
FeatureLoadOrderResolver) - Phase 3 — Validate (
FeatureStructureValidator) - Phase 4 — Import (
FeatureImporter) - Phase 5 — Integrate (
FeatureIntegrator) - RefinementRegistry
- Service locator
- Template override
- Feature hooks reference
- Error handling
- See also
Overview
The loading pipeline has 5 phases, each handled by a dedicated class:
FeatureManager ← entry point (called from create_splent_app)
│
├── 1. Parse FeatureEntryParser "org/name@vX.Y.Z" → FeatureRef
├── 2. Resolve FeatureLoadOrderResolver topological sort via UVL
├── 3. Validate FeatureStructureValidator check src/ layout
├── 4. Import FeatureImporter add to sys.path + import
└── 5. Integrate FeatureIntegrator wire into Flask app
FeatureManager
File: managers/feature_manager.py
The public API. Called once per app startup.
FeatureManager(app, strict=False).register_features()
What it does
- Reads
[tool.splent].featuresfrom the active product’spyproject.tomlviaPyprojectReader. - Passes the raw feature strings to
FeatureLoadOrderResolverto get a dependency-ordered list. - Instantiates
FeatureLoaderand callsload(ref)for eachFeatureRefin order.
Parameters
| Parameter | Default | Description |
|---|---|---|
app |
— | Flask application instance |
strict |
False |
If True, raise FeatureError on any missing optional submodule. If False, missing routes, models, hooks are silently ignored. |
Methods
| Method | Returns | Description |
|---|---|---|
register_features() |
None |
Load and integrate all features into the Flask app |
get_features() |
list[str] |
Return raw feature strings from pyproject.toml without loading |
FeatureRef
File: managers/feature_loader.py
Immutable value object representing a single feature reference.
@dataclass(frozen=True)
class FeatureRef:
org: str # "splent-io" (GitHub org name, with dashes)
org_safe: str # "splent_io" (filesystem-safe, underscores)
name: str # "splent_feature_auth"
version: str # "v1.2.7" or None (editable)
import_name()
Returns the fully qualified Python import path:
ref.import_name() # → "splent_io.splent_feature_auth"
Phase 1 — Parse (FeatureEntryParser)
File: managers/feature_loader.py
Converts raw pyproject.toml entry strings into FeatureRef objects.
Accepted formats
| Entry string | Parsed result |
|---|---|
splent_feature_auth@v1.2.7 |
org=splent-io, name=splent_feature_auth, version=v1.2.7 |
splent-io/splent_feature_auth@v1.2.7 |
org=splent-io, name=splent_feature_auth, version=v1.2.7 |
splent-io/splent_feature_auth |
org=splent-io, name=splent_feature_auth, version=None |
splent_feature_auth |
org defaults to splent-io, version=None |
The default namespace (splent-io) is used when no org is specified. Filesystem safety (- to _) is applied automatically to the org name.
Phase 2 — Resolve (FeatureLoadOrderResolver)
File: managers/feature_order.py
Reorders the raw feature list so that every dependency appears before the features that require it.
Algorithm
- Parse the UVL file to extract:
- A
{short_name → package_name}mapping (from feature attribute blocks) - Implication constraints (
A => Bmeans “A requires B”)
- A
- Build a directed graph: if
A => B, add edgeB → A(B must come before A) - Run stable Kahn’s topological sort:
- Use a min-heap seeded with all zero-in-degree nodes to preserve original order for independent features
- Detect cycles and raise
FeatureErrorif found
- Return the reordered
FeatureReflist
Fallback
If no UVL file exists or the file contains no applicable constraints, the original pyproject.toml declaration order is returned unchanged.
Cycle detection
Circular dependency detected among features: ['auth', 'profile'].
Review the 'constraints' section in your .uvl file.
Example
Given these UVL constraints:
constraints
profile => auth
confirmemail => mail
reset => mail
And this pyproject.toml order:
redis, public, mail, confirmemail, auth, profile, reset
The resolved load order is:
1 redis
2 public
3 mail
4 confirmemail ← after mail
5 auth
6 profile ← after auth
7 reset ← after mail
This is the same order shown by splent feature:order and used by db:seed.
Phase 3 — Validate (FeatureStructureValidator)
For each feature, checks that the resolved directory contains:
<feature_dir>/
└── src/
└── <org_safe>/
└── <name>/
Returns (src_root, org_ns_dir, pkg_dir). Raises FeatureError if the layout is wrong.
Phase 4 — Import (FeatureImporter)
- Resolve symlink (
FeatureLinkResolver) — looks for the feature directory under the product’sfeatures/<org_safe>/directory. Tries<name>@<version>first, falls back to<name>, then any prefix match. - Add to sys.path — inserts
src_rootintosys.pathso thatimport splent_io.splent_feature_authworks. - Import the package — imports the feature root package via
importlib.import_module(ref.import_name()). - Import submodules — attempts to import conventional submodules in order:
routes,models,hooks. Missing submodules are silently ignored in lenient mode (strict=False), or raiseFeatureErrorin strict mode.
Phase 5 — Integrate (FeatureIntegrator)
The integration phase wires each feature into the running Flask app. It runs 5 sub-steps in this fixed order:
5a. Config injection (inject_config)
Calls <feature>.config.inject_config(app) if a config.py exists. This lets features set app.config values before anything else runs.
5b. Model extensions (apply_model_extensions)
Applies any model extensions declared by the feature. This is how refinement features add columns or relationships to models owned by other features without modifying the original code.
5c. Feature init (init_feature)
Calls <feature>.init_feature(app) if the function is present in __init__.py. This is the feature’s main startup hook — register template hooks, initialize extensions, configure services.
5d. Service overrides (apply_service_overrides)
Applies service class overrides from the RefinementRegistry. If a refinement feature has registered a replacement for a service class, the override is applied here so that all subsequent get_service_class() lookups return the refinement’s version.
5e. Blueprint registration (register_blueprints)
Finds all Blueprint instances defined in the feature module and registers them with app.register_blueprint(bp).
RefinementRegistry
The RefinementRegistry is populated before the loading pipeline runs and is read during the integrate phase (steps 5b and 5d).
When a refinement feature is declared in pyproject.toml, the framework parses its [tool.splent.contract.refines] section to build a map of what it overrides. This map is available to FeatureIntegrator so that model extensions and service overrides are applied at the correct point in the pipeline.
The registry is read-only during loading — no feature can modify it at runtime.
Service locator
The framework provides a lightweight service locator for cross-feature service resolution:
| Function | Description |
|---|---|
register_service(name, cls) |
Register a service class under a string name |
get_service_class(name) |
Look up the current class for a service name |
Features register their services during init_feature(app):
from splent_framework.services.service_locator import register_service
def init_feature(app):
register_service("authentication", AuthenticationService)
Other features (or the product) retrieve the class by name:
from splent_framework.services.service_locator import get_service_class
AuthService = get_service_class("authentication")
When a refinement feature overrides a service via apply_service_overrides, the locator transparently returns the refined class. Callers do not need to know whether a refinement is active.
Template override
Features can override templates from other features (or the product) by placing a file at the same relative path in their own templates/ directory.
This works automatically via Flask’s Jinja template loader order. Because features are registered as blueprints in topological order, a refinement feature’s templates directory is searched after the base feature’s. To override a base template, the refinement feature places a template at the same path — Flask’s blueprint loader resolves it based on registration order.
No configuration is needed. The loader order follows the feature load order.
Feature hooks reference
These are the hooks the framework calls during feature loading. Define them in your feature’s __init__.py and config.py.
__init__.py
from splent_framework.blueprints.base_blueprint import BaseBlueprint
# Required: at least one Blueprint
splent_feature_auth_bp = BaseBlueprint(
"splent_feature_auth", __name__, template_folder="templates"
)
# Optional: one-time setup
def init_feature(app):
# initialize extensions, configure services, etc.
pass
# Optional: template globals
def inject_context_vars(app):
return {
"auth_enabled": True,
}
config.py
# Optional: called before init_feature, before blueprints are registered
def inject_config(app):
app.config["SESSION_TYPE"] = "redis"
app.config["SESSION_REDIS"] = redis.from_url(os.getenv("REDIS_URL"))
Hook call order
inject_config(app) ← config.py (5a)
apply_model_extensions() ← framework (5b)
init_feature(app) ← __init__.py (5c)
apply_service_overrides() ← RefinementRegistry (5d)
register_blueprint(bp) ← framework, per Blueprint (5e)
inject_context_vars(app) ← __init__.py, called later by JinjaManager
inject_context_vars is not called during feature loading — it is collected by JinjaManager and called on every request as part of the context processor pipeline.
Error handling
All errors during feature loading raise FeatureError (importable from splent_framework.managers.feature_loader):
from splent_framework.managers.feature_loader import FeatureError
In strict=False mode, FeatureError is raised only for hard failures (missing symlink, broken structure, import error). Missing optional submodules are silently skipped.
In strict=True mode, any missing submodule or hook raises FeatureError immediately.
See also
feature:order— CLI command that usesFeatureLoadOrderResolverfeature:xray— inspect extension points, hooks, and refinementsdb:seed— runs seeders in the same topological order- Application initialization — where
FeatureManageris called - Architecture layers —
BaseBlueprintused in every feature - Extensibility — refinement features, service overrides, template hooks
feature:contract— updateinject_context_varsand contract at any time during development