# 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`: ```python 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: ```python app.hook.register('TYPO_HOOK', my_func) # ❌ FrameworkError: Hook 'TYPO_HOOK' is not defined. ``` ```yaml 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) ```python 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 ``` ```yaml # 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) ```python # No decorator needed @discover_filter('EXPERIMENTAL_FEATURE_FILTER') def experimental_feature(self, data, **kwargs): return data ``` ```yaml 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: ```python # 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](dynamic_hooks_filters.md) for YAML configuration details. ## Use Cases ### ✅ Good Use Cases for `@optional()` **Plugin compatibility across versions:** ```python @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:** ```yaml EXPERIMENTAL_VISUALIZATION_HOOK: - class: vipr.plugins.viz.Visualizer method: plot_results optional: true # Not all installations have viz plugin ``` **Conditional workflows:** ```python @optional() @discover_filter('ADVANCED_POSTPROCESS_FILTER') def advanced_postprocessing(data): """Only if advanced plugin installed""" return data ``` ### ❌ Bad Use Cases **Hiding typos:** ```yaml # WRONG! Fix the typo instead INFERENCE_STRAT_HOOK: # Should be START - method: my_method optional: true # ← Don't hide bugs! ``` **Core features:** ```python # 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: ```python 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`: ```python 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`: ```python 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 - [Dynamic Hooks/Filters Extension](dynamic_hooks_filters.md) - YAML configuration - [Inference Plugin](../plugins/built-in/inference.md) - Hook/filter workflow - `vipr-core/vipr/ext/strict_registration.py` - Implementation