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

  1. Overview
  2. FeatureManager
    1. What it does
    2. Parameters
    3. Methods
  3. FeatureRef
    1. import_name()
  4. Phase 1 — Parse (FeatureEntryParser)
    1. Accepted formats
  5. Phase 2 — Resolve (FeatureLoadOrderResolver)
    1. Algorithm
    2. Fallback
    3. Cycle detection
    4. Example
  6. Phase 3 — Validate (FeatureStructureValidator)
  7. Phase 4 — Import (FeatureImporter)
  8. Phase 5 — Integrate (FeatureIntegrator)
    1. 5a. Config injection (inject_config)
    2. 5b. Model extensions (apply_model_extensions)
    3. 5c. Feature init (init_feature)
    4. 5d. Service overrides (apply_service_overrides)
    5. 5e. Blueprint registration (register_blueprints)
  9. RefinementRegistry
  10. Service locator
  11. Template override
  12. Feature hooks reference
    1. __init__.py
    2. config.py
    3. Hook call order
  13. Error handling
  14. 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

  1. Reads [tool.splent].features from the active product’s pyproject.toml via PyprojectReader.
  2. Passes the raw feature strings to FeatureLoadOrderResolver to get a dependency-ordered list.
  3. Instantiates FeatureLoader and calls load(ref) for each FeatureRef in 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

  1. Parse the UVL file to extract:
    • A {short_name → package_name} mapping (from feature attribute blocks)
    • Implication constraints (A => B means “A requires B”)
  2. Build a directed graph: if A => B, add edge B → A (B must come before A)
  3. 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 FeatureError if found
  4. Return the reordered FeatureRef list

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)

  1. Resolve symlink (FeatureLinkResolver) — looks for the feature directory under the product’s features/<org_safe>/ directory. Tries <name>@<version> first, falls back to <name>, then any prefix match.
  2. Add to sys.path — inserts src_root into sys.path so that import splent_io.splent_feature_auth works.
  3. Import the package — imports the feature root package via importlib.import_module(ref.import_name()).
  4. Import submodules — attempts to import conventional submodules in order: routes, models, hooks. Missing submodules are silently ignored in lenient mode (strict=False), or raise FeatureError in 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


Back to top

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