Strict Hook/Filter Registration Extension

The Strict Registration Extension enforces fail-fast validation when registering hooks and filters, preventing silent failures from typos or undefined hook/filter names.

Overview

Default Behavior: All hooks/filters MUST be defined before registration. Attempts to register to undefined hooks/filters raise FrameworkError immediately.

Key Features:

  • Fail-fast validation: Catches typos at startup, not runtime

  • Fuzzy matching suggestions: “Did you mean X?” error messages

  • Plugin identification: Shows which plugin caused the error

  • Optional escape hatch: @optional() decorator for truly optional registrations

Why Strict-by-Default?

Before (Cement default):

[DEBUG] filter name 'INFERENCE_STRAT_HOOK' is not defined! ignoring...
# Application continues
# Filter never executes
# Silent failure = hard to debug

After (Strict mode):

[ERROR] Filter 'INFERENCE_STRAT_HOOK' is not defined.
Did you mean: INFERENCE_START_HOOK?

FrameworkError: Filter 'INFERENCE_STRAT_HOOK' is not defined.
# Application stops immediately
# Error is obvious

Configuration

The extension is automatically loaded in vipr/main.py:

extensions = [
    'vipr.ext.strict_registration',  # Strict registration
    'vipr.ext.dynamic_hooks_filters',       # Dynamic YAML loading
]

Important: strict_registration MUST load BEFORE dynamic_hooks_filters to enforce validation.

Usage

Default: Strict Validation

All registrations are validated:

app.hook.register('TYPO_HOOK', my_func)
# ❌ FrameworkError: Hook 'TYPO_HOOK' is not defined.
vipr:
  inference:
    filters:
      INFERENCE_STRAT_HOOK:  # Typo!
        - class: MyPlugin
          method: my_method
# ❌ Error at startup with suggestions

Optional Registration: Two Methods

When a hook/filter might legitimately not exist (e.g., optional features):

Method 1: @optional() Decorator (Code)

from vipr.ext.strict_registration import optional

@optional()
@discover_filter('EXPERIMENTAL_FEATURE_FILTER')
def experimental_feature(self, data, **kwargs):
    """Only runs if experimental plugin loaded"""
    return data
# No special config needed
EXPERIMENTAL_FEATURE_FILTER:
  - class: MyPlugin
    method: experimental_feature
# ⚠️ Warning if filter doesn't exist, but continues

Method 2: optional: true Flag (YAML)

# No decorator needed
@discover_filter('EXPERIMENTAL_FEATURE_FILTER')
def experimental_feature(self, data, **kwargs):
    return data
EXPERIMENTAL_FEATURE_FILTER:
  - class: MyPlugin
    method: experimental_feature
    optional: true  # ← Handled by strict_registration
# ⚠️ Warning if filter doesn't exist, but continues

Choosing Between Methods

Aspect

@optional() Decorator

optional: true in YAML

Where

Python code

YAML config

Flexibility

Static

Dynamic per deployment

Best for

Architectural decisions

Configuration variations

Use @optional() when:

  • Feature is always optional (architectural decision)

  • Want code as single source of truth

Use optional: true when:

  • Optionality varies per deployment/environment

  • Need configuration flexibility

Error Messages

Typo Detection

FrameworkError: Filter 'INFERENCE_PREPROCESS_PRE_FILTER2' is not defined.
Attempted registration from plugin 'reflectorch_extension'.

Did you mean one of these?
  - INFERENCE_PREPROCESS_PRE_FILTER
  - INFERENCE_PREPROCESS_POST_FILTER
  - INFERENCE_POSTPROCESS_PRE_FILTER

Plugin Identification

Error messages identify the source plugin:

Attempted registration from plugin 'reflectometry'.

Integration with Dynamic Hooks/Filters

The optional: true YAML flag is processed by the Dynamic Hooks/Filters extension and checked by Strict Registration:

# In dynamic_hooks_filters extension:
if cfg.get("optional", False):
    callback.__optional__ = True  # Set attribute

# In strict_registration extension:
if hasattr(func, '__optional__') and func.__optional__:
    # Only warning, no error
    return False

See Dynamic Hooks/Filters Extension for YAML configuration details.

Use Cases

✅ Good Use Cases for @optional()

Plugin compatibility across versions:

@optional()
@discover_hook('VIPR_2_0_STREAMING_HOOK')
def use_streaming_feature(app):
    """Works with VIPR 1.x (ignored) and 2.x (executed)"""
    pass

Experimental features:

EXPERIMENTAL_VISUALIZATION_HOOK:
  - class: vipr.plugins.viz.Visualizer
    method: plot_results
    optional: true  # Not all installations have viz plugin

Conditional workflows:

@optional()
@discover_filter('ADVANCED_POSTPROCESS_FILTER')
def advanced_postprocessing(data):
    """Only if advanced plugin installed"""
    return data

❌ Bad Use Cases

Hiding typos:

# WRONG! Fix the typo instead
INFERENCE_STRAT_HOOK:  # Should be START
  - method: my_method
    optional: true  # ← Don't hide bugs!

Core features:

# WRONG! Core features should be required
@optional()
@discover_filter('INFERENCE_NORMALIZE_FILTER')
def normalize(data):
    pass

Troubleshooting

“Hook ‘X’ is not defined”

Cause: Typo in hook/filter name or hook not defined in framework.

Solution:

  1. Check error message for suggestions

  2. Verify hook is defined with app.hook.define('HOOK_NAME')

  3. If truly optional, add optional: true or @optional()

Optional callback not executing

Cause: Hook/filter doesn’t exist and callback is marked optional.

Solution: This is expected behavior. Check logs for warning message.

Attribute propagation issues

Cause: Custom wrappers not propagating __optional__ attribute.

Solution: Ensure wrappers use _propagate_attributes() from dynamic_hooks_filters.

Best Practices

  1. Default to strict: Only use @optional() when truly needed

  2. Fix typos: Don’t hide bugs with optional: true

  3. Document optionality: Comment why a callback is optional

  4. Choose one method: Don’t mix @optional() and optional: true for same callback

  5. Test both paths: Test with and without optional hooks/filters loaded

Technical Details

Implementation

The extension monkey-patches Cement’s registration methods:

def enhanced_hook_register(name, func, weight=0):
    if name not in app.hook.__hooks__:
        if hasattr(func, '__optional__') and func.__optional__:
            # Log warning, return False
            return False
        # Raise FrameworkError with suggestions
        raise FrameworkError(error_msg)
    return original_hook_register(name, func, weight)

Attribute Propagation

The __optional__ attribute is propagated through wrapper functions in dynamic_hooks_filters:

def _propagate_attributes(source_cb, target_cb):
    for attr in ("__name__", "__module__", "__optional__"):
        if hasattr(source_cb, attr):
            setattr(target_cb, attr, getattr(source_cb, attr))

Load Order

Critical: Extension must load BEFORE dynamic_hooks_filters:

extensions = [
    'vipr.ext.strict_registration',  # 1. Patches registration
    'vipr.ext.dynamic_hooks_filters',       # 2. Uses patched methods
]

Migration Guide

Breaking Change: Strict validation now fails immediately on undefined hooks/filters or typos.

Quick Fix:

  1. Run your app - errors show typos with “Did you mean…” suggestions

  2. Fix the typo in config/code, or

  3. Mark as optional with optional: true (YAML) or @optional() (code)

Example Error:

FrameworkError: Filter 'INFERENCE_PREDICITON_FILTER' is not defined.
Did you mean: INFERENCE_PREDICTION_FILTER?

Solution: Fix typo in YAML or mark genuinely optional features with optional: true.

See Also