Result Collection Strategies¶
simplug provides 18 built-in strategies for collecting results from hook implementations, plus support for custom collectors.
Overview¶
Result strategies are specified in hook definition using the result parameter:
from simplug import Simplug, SimplugResult
@simplug.spec(result=SimplugResult.ALL_AVAILS)
def my_hook(data):
pass
Strategy Categories¶
Strategies are organized into three categories:
- ALL strategies - Execute all plugins and collect results
- FIRST/LAST strategies - Execute only first or last plugin
- SINGLE strategies - Execute a single plugin by name
ALL Strategies¶
Execute all plugin implementations and collect results.
ALL¶
Collect all results including None values.
@simplug.spec(result=SimplugResult.ALL)
def process(data):
pass
# Returns: ['result1', None, 'result3']
Use when you need to know which plugins returned None.
ALL_AVAILS¶
Collect all non-None results.
@simplug.spec(result=SimplugResult.ALL_AVAILS)
def process(data):
pass
# Returns: ['result1', 'result3']
Most common strategy for collecting results.
ALL_FIRST¶
Execute all plugins, return first result (even if None).
@simplug.spec(result=SimplugResult.ALL_FIRST)
def process(data):
pass
# Returns: 'result1' or None
# All plugins execute
Use when you want all side effects but only care about first result.
ALL_LAST¶
Execute all plugins, return last result (even if None).
@simplug.spec(result=SimplugResult.ALL_LAST)
def process(data):
pass
# Returns: 'result3' or None
# All plugins execute
ALL_FIRST_AVAIL¶
Execute all plugins, return first non-None result.
@simplug.spec(result=SimplugResult.ALL_FIRST_AVAIL)
def process(data):
pass
# Returns: 'result2' (first non-None)
# All plugins execute
ALL_LAST_AVAIL¶
Execute all plugins, return last non-None result.
@simplug.spec(result=SimplugResult.ALL_LAST_AVAIL)
def process(data):
pass
# Returns: 'result3' (last non-None)
# All plugins execute
FIRST/LAST Strategies¶
Execute only first or last plugin, skipping others.
FIRST¶
Execute only first plugin, return its result (even if None).
@simplug.spec(result=SimplugResult.FIRST)
def process(data):
pass
# Returns: 'result1' or None
# Only first plugin executes
LAST¶
Execute only last plugin, return its result (even if None).
@simplug.spec(result=SimplugResult.LAST)
def process(data):
pass
# Returns: 'result3' or None
# Only last plugin executes
FIRST_AVAIL¶
Execute plugins until first non-None result, then stop.
@simplug.spec(result=SimplugResult.FIRST_AVAIL)
def process(data):
pass
# Returns: 'result2' (first non-None)
# Plugins 1 and 2 execute, then stop
Good for fallback chains - first plugin to provide valid result wins.
LAST_AVAIL¶
Execute plugins in reverse until first non-None result, then stop.
@simplug.spec(result=SimplugResult.LAST_AVAIL)
def process(data):
pass
# Returns: 'result2' (first non-None from end)
# Plugins 3 and 2 execute, then stop
Good for overriding behavior - last valid result wins.
SINGLE Strategies¶
Execute a single plugin by name.
SINGLE¶
Execute plugin specified by __plugin parameter.
@simplug.spec(result=SimplugResult.SINGLE)
def get_value(key):
pass
# Execute specific plugin
result = simplug.hooks.get_value('key', __plugin='plugin1')
# If no __plugin, uses last plugin
result = simplug.hooks.get_value('key')
Warning
If no __plugin specified and multiple implementations exist, a warning is raised.
TRY Strategies¶
Return None instead of raising exception when no result available.
All strategies above have TRY_* variants that:
- Return None instead of raising ResultUnavailableError
- Otherwise behave identically
Available TRY Strategies¶
TRY_ALL- ReturnNoneif no resultsTRY_ALL_AVAILS- ReturnNoneif no non-NoneresultsTRY_ALL_FIRST- ReturnNoneif no resultsTRY_ALL_LAST- ReturnNoneif no resultsTRY_ALL_FIRST_AVAIL- ReturnNoneif no non-NoneresultsTRY_ALL_LAST_AVAIL- ReturnNoneif no non-NoneresultsTRY_FIRST- ReturnNoneif no resultTRY_LAST- ReturnNoneif no resultTRY_FIRST_AVAIL- ReturnNoneif no non-NoneresultTRY_LAST_AVAIL- ReturnNoneif no non-NoneresultTRY_SINGLE- ReturnNoneif plugin not found
Example:
# Would raise ResultUnavailableError if no results
@simplug.spec(result=SimplugResult.FIRST_AVAIL)
def get_data(data):
pass
# Returns None instead of raising exception
@simplug.spec(result=SimplugResult.TRY_FIRST_AVAIL)
def get_data(data):
pass
Custom Collectors¶
You can provide your own result collector function:
def custom_collector(calls):
"""Custom collector function.
Args:
calls: List of SimplugImplCall tuples
Each tuple: (plugin_name, impl_function, args, kwargs)
Returns:
Collected result in any format
"""
results = [call.impl(*call.args, **call.kwargs) for call in calls]
return '; '.join(str(r) for r in results if r)
@simplug.spec(result=custom_collector)
def process(data):
pass
# Returns: "result1; result2; result3"
Async Custom Collectors¶
For async hooks, provide an async collector:
async def async_custom_collector(calls):
results = [await call.impl(*call.args, **call.kwargs) for call in calls]
return sum(results)
@simplug.spec(result=async_custom_collector)
async def process_async(data):
pass
Strategy Selection Guide¶
| Use Case | Recommended Strategy |
|---|---|
| Collect all results | ALL_AVAILS |
| First valid result (chain) | FIRST_AVAIL |
| Last valid result (override) | LAST_AVAIL |
| Only first plugin | FIRST |
| Only last plugin | LAST |
| Execute all, return first | ALL_FIRST |
| Execute all, return last | ALL_LAST |
| Single plugin by name | SINGLE |
| Handle no results gracefully | Use TRY_* variants |
Examples¶
Collecting Transformations¶
@simplug.spec(result=SimplugResult.ALL_AVAILS)
def transform_text(text):
pass
class Plugins:
@simplug.impl
def transform_text(self, text):
return text.upper()
@simplug.impl
def transform_text(self, text):
return text.lower()
@simplug.impl
def transform_text(self, text):
return text.title()
results = simplug.hooks.transform_text("hello")
# Returns: ['HELLO', 'hello', 'Hello']
Fallback Chain¶
@simplug.spec(result=SimplugResult.FIRST_AVAIL)
def get_config(key):
"""Get config from first available source."""
# Plugins in priority order (env vars, then file, then defaults)
class EnvConfig:
priority = -1 # First
@simplug.impl
def get_config(self, key):
return os.environ.get(key)
class FileConfig:
priority = 0
@simplug.impl
def get_config(self, key):
# Returns None if not found
return read_config(key)
class DefaultConfig:
priority = 1 # Last (always has value)
@simplug.impl
def get_config(self, key):
return DEFAULTS.get(key)
value = simplug.hooks.get_config('timeout')
# Returns env value if set, file value if set, else default
Validation Pipeline¶
@simplug.spec(result=SimplugResult.ALL)
def validate(data):
"""Run all validations."""
class SchemaValidator:
@simplug.impl
def validate(self, data):
if not validate_schema(data):
raise ValueError("Invalid schema")
class BusinessValidator:
@simplug.impl
def validate(self, data):
if not meets_business_rules(data):
raise ValueError("Invalid business rules")
results = simplug.hooks.validate(data)
# Returns [None, ValueError] - all validators run
Next Steps¶
- Implementing Hooks - How to implement hooks
- Priority System - Control execution order
- Async Hooks - Async-specific considerations