Extension Hooks and Customization Points¶
You can extend django-stratagem in several ways. This page covers hook methods, overridable methods, signals, and patterns for testing your extensions.
Hook Methods Overview¶
The Registry class provides four hook methods that let you customize the registration lifecycle without replacing register() or unregister() entirely.
Hook |
Called by |
When |
Default behavior |
|---|---|---|---|
|
|
Before storage |
Checks slug exists and interface subclass |
|
|
After validation |
Returns |
|
|
After storage, before signal |
No-op |
|
|
After removal, before signal |
No-op |
All four are classmethods. Override them in your Registry subclass and call super() to preserve default behavior.
validate_implementation¶
Called before an implementation is stored. Raise any exception to reject registration.
from django_stratagem import Registry
class StrictNotificationRegistry(Registry):
implementations_module = "notifications"
@classmethod
def validate_implementation(cls, implementation):
# Preserve default slug + interface checks
super().validate_implementation(implementation)
# Require a send() method
if not callable(getattr(implementation, "send", None)):
raise TypeError(
f"{implementation.__name__} must define a send() method"
)
If validate_implementation raises, register() stops immediately - the implementation is not stored, on_register is not called, and no signal is emitted.
build_implementation_meta¶
Called after validation. Returns the metadata dict that will be stored in registry.implementations[slug]. Override to add custom metadata fields.
from datetime import datetime
from django_stratagem import Registry
class AuditableRegistry(Registry):
implementations_module = "strategies"
@classmethod
def build_implementation_meta(cls, implementation):
meta = super().build_implementation_meta(implementation)
meta["version"] = getattr(implementation, "version", "0.0.0")
meta["author"] = getattr(implementation, "author", "unknown")
meta["registered_at"] = datetime.now().isoformat()
return meta
The extra keys are stored alongside the standard klass, description, icon, and priority keys. They are available through get_implementation_meta() and are passed to on_register and on_unregister.
on_register and on_unregister¶
Called after the implementation is stored (or removed) and cache is cleared, but before the Django signal is emitted. Use these for side effects like audit logging, metrics, or cache warming.
import logging
from django_stratagem import Registry
logger = logging.getLogger(__name__)
class LoggingRegistry(Registry):
implementations_module = "strategies"
@classmethod
def on_register(cls, slug, implementation, meta):
logger.info(
"Registered %s (priority=%d) in %s",
slug, meta.get("priority", 0), cls.__name__,
)
@classmethod
def on_unregister(cls, slug, meta):
logger.info("Unregistered %s from %s", slug, cls.__name__)
Execution Order¶
register()¶
1. validate_implementation(implementation) -- may raise
2. slug = implementation.slug
3. meta = build_implementation_meta(implementation)
4. implementations[slug] = meta
5. clear_cache()
6. on_register(slug, implementation, meta)
7. implementation_registered signal sent
unregister()¶
1. Check slug exists (raises ImplementationNotFound if missing)
2. meta = implementations.pop(slug)
3. clear_cache()
4. on_unregister(slug, meta)
5. implementation_unregistered signal sent
Overridable Methods¶
Beyond the four hooks, several Registry methods are designed for overriding:
get_display_name(implementation)Customize how implementations are labeled in choices, admin, and templates.
class MyRegistry(Registry):
implementations_module = "strategies"
@classmethod
def get_display_name(cls, implementation):
icon = getattr(implementation, "icon", "")
name = super().get_display_name(implementation)
return f"{icon} {name}" if icon else name
get_cache_key(suffix)Customize cache key format (e.g. for multi-tenant isolation).
class TenantRegistry(Registry):
implementations_module = "strategies"
@classmethod
def get_cache_key(cls, suffix):
from threading import current_thread
tenant = getattr(current_thread(), "tenant_id", "default")
return f"django_stratagem:{tenant}:{cls.__name__}:{suffix}"
is_valid(value)Customize what counts as a valid implementation reference.
When to Use Hooks vs Signals¶
Use hooks when:
You need to reject or modify registrations (validation, meta enrichment)
The behavior is specific to one registry subclass
You need guaranteed ordering relative to storage and cache clearing
Use signals when:
Multiple unrelated listeners need to react to registrations
The listener is defined outside the registry (e.g. a separate app)
You want loose coupling between the registry and the reaction
Signals¶
django-stratagem emits three Django signals:
implementation_registered¶
Sent when an implementation is registered with a registry.
from django.dispatch import receiver
from django_stratagem.signals import implementation_registered
@receiver(implementation_registered)
def on_registered(sender, registry, implementation, **kwargs):
print(f"{implementation.slug} registered in {registry.__name__}")
sender- The registry classregistry- The registry classimplementation- The implementation class
implementation_unregistered¶
Sent when an implementation is unregistered.
from django.dispatch import receiver
from django_stratagem.signals import implementation_unregistered
@receiver(implementation_unregistered)
def on_unregistered(sender, registry, slug, **kwargs):
print(f"{slug} unregistered from {registry.__name__}")
sender- The registry classregistry- The registry classslug- The slug that was unregistered
registry_reloaded¶
Sent when a registry is reloaded during discover_registries().
from django.dispatch import receiver
from django_stratagem.signals import registry_reloaded
@receiver(registry_reloaded)
def on_reloaded(sender, registry, **kwargs):
print(f"{registry.__name__} reloaded")
sender- The registry classregistry- The registry class
Signal Use Cases¶
Invalidate External Caches on Registration¶
from django.dispatch import receiver
from django_stratagem.signals import implementation_registered, implementation_unregistered
@receiver(implementation_registered)
@receiver(implementation_unregistered)
def invalidate_api_cache(sender, **kwargs):
"""Clear API response cache when implementations change."""
from django.core.cache import cache
cache.delete(f"api:registry:{sender.__name__}:choices")
Audit Trail on Unregister¶
from django.dispatch import receiver
from django_stratagem.signals import implementation_unregistered
@receiver(implementation_unregistered)
def log_unregistration(sender, registry, slug, **kwargs):
from myapp.models import AuditLog
AuditLog.objects.create(
action="implementation_unregistered",
registry=registry.__name__,
slug=slug,
)
Warm Caches After Reload¶
from django.dispatch import receiver
from django_stratagem.signals import registry_reloaded
@receiver(registry_reloaded)
def warm_caches(sender, registry, **kwargs):
"""Pre-populate choices and items caches after reload."""
registry.get_choices()
registry.get_items()
Testing Extensions¶
Temporary Registries¶
Create isolated registries in tests to avoid polluting the global state. The conftest _clean_stratagem_registry fixture (included by default) restores the global registry list after each test.
from django_stratagem import Registry, Interface
def test_my_custom_validation():
class TestRegistry(Registry):
implementations_module = "test_impls"
@classmethod
def validate_implementation(cls, implementation):
super().validate_implementation(implementation)
if not hasattr(implementation, "process"):
raise ValueError("Must define process()")
TestRegistry.implementations = {}
class Good(Interface):
slug = "good"
registry = None # Don't auto-register
def process(self):
pass
class Bad(Interface):
slug = "bad"
registry = None
TestRegistry.register(Good)
assert "good" in TestRegistry.implementations
with pytest.raises(ValueError, match="process"):
TestRegistry.register(Bad)
Testing Hook Ordering¶
Verify that hooks run in the expected order relative to signals:
def test_hook_runs_before_signal():
call_order = []
class OrderedRegistry(Registry):
implementations_module = "ordered_impls"
@classmethod
def on_register(cls, slug, implementation, meta):
call_order.append("hook")
def signal_handler(sender, **kwargs):
call_order.append("signal")
from django_stratagem.signals import implementation_registered
implementation_registered.connect(signal_handler)
OrderedRegistry.implementations = {}
class Impl(Interface):
slug = "test"
registry = None
try:
OrderedRegistry.register(Impl)
assert call_order == ["hook", "signal"]
finally:
implementation_registered.disconnect(signal_handler)
Testing Custom Conditions¶
from django_stratagem import Condition
class MinItemsCondition(Condition):
def __init__(self, min_count):
self.min_count = min_count
def is_met(self, context):
return context.get("item_count", 0) >= self.min_count
def explain(self):
return f"Requires at least {self.min_count} items"
def test_min_items_condition():
cond = MinItemsCondition(5)
assert not cond.is_met({"item_count": 3})
assert cond.is_met({"item_count": 5})
assert cond.is_met({"item_count": 10})
def test_condition_composition():
cond = MinItemsCondition(5) & MinItemsCondition(10)
assert not cond.is_met({"item_count": 7})
assert cond.is_met({"item_count": 10})