Hooks are shared integration points. Treat every target as potentially wrapped by another mod, preserve unknown behavior, and restore only what you still own.
Composable method wrapping
Capture the method currently installed at patch time, call it exactly once, preserve arguments and return values, and keep both an owned-installation ledger and a wrapper marker.
_hooks = {}
def install_hook(owner, method_name):
key = (owner, method_name)
if key in _hooks:
return
current = getattr(owner, method_name)
if getattr(current, '_example_hook_owner', False):
return
def wrapper(*args, **kwargs):
result = current(*args, **kwargs)
try:
after_original(*args, **kwargs)
except Exception:
logger.exception('Hook extension failed')
return result
wrapper._example_hook_owner = True
wrapper._previous = current
setattr(owner, method_name, wrapper)
_hooks[key] = wrapper
def remove_hook(owner, method_name):
wrapper = _hooks.pop((owner, method_name), None)
if wrapper is not None and getattr(owner, method_name) is wrapper:
setattr(owner, method_name, wrapper._previous)
The ledger guards repeated installation even when another wrapper now occupies the live slot and does not carry this example's marker. Checking only that outer callable can otherwise install the same extension again beneath/above the intervening chain. Record the owned entry only after attribute assignment succeeds, so a failed assignment does not leave a false installation receipt.
This guard lasts while the ledger entry exists. Removal can clear the entry while a later wrapper still retains the old callable; it does not establish that removing and reinstalling is safe. Follow the existing captured-callable lifetime guidance. The ledger change does not add descriptor-aware restoration; that remains a target-specific requirement when adapting this simplified removal sketch. Selected checks used inert owners and wrappers to compare consecutive installs, interposed-wrapper installs and failed assignment/retry, without client hooks or Python 2 descriptor execution.
Before or after the original?
Before
Use when you must snapshot old state, validate input, or intentionally suppress an action.
After
Use when the original creates the authoritative new state, view, cache entry, or result.
Around
Use sparingly for timing, temporary guards, or guaranteed cleanup with
try/finally.
Class versus instance patches
Prefer patching class attributes for client lifecycle methods so every instance follows the same verified wrapper and teardown can restore one owner. Patch an instance method only when the hook is deliberately scoped to that exact instance and the same instance owns teardown.
- Verify whether the target method is looked up on the class, installed dynamically on an instance, or replaced during view/controller reconstruction.
- Record the owner you patched and compare the installed callable before restoring.
- Re-resolve instance owners after route, page, battle, or replay reconstruction instead of retaining stale object references.
Event handler identity
Most event systems remove the exact callable object that was added. Store locally created functions and bound handlers instead of recreating them during unsubscribe.
class Listener(object):
def __init__(self):
self._on_update_handler = self._on_update
self._subscribed = False
def subscribe(self):
if self._subscribed:
return
self._subscribed = True
source.onUpdate += self._on_update_handler
def unsubscribe(self):
if not self._subscribed:
return
self._subscribed = False
source.onUpdate -= self._on_update_handler
def _on_update(self, value):
pass
class ViewController {
constructor() {
this.onResize = this.onResize.bind(this);
}
subscribe() {
engine.on('resize', this.onResize);
}
unsubscribe() {
engine.off('resize', this.onResize);
}
}
Recurring loops need one owner
- Use one scheduling guard so events cannot start duplicate loops.
- Set the callback ID to
Nonebefore the callback schedules its successor. - Bind loop variables into delayed closures so every callback keeps the intended value.
- Use a thread-local guard when a hook can indirectly recurse.
for vehicle_id in vehicle_ids:
BigWorld.callback(
0.0,
lambda vehicle_id=vehicle_id: refresh_vehicle(vehicle_id)
)
Hook review checklist
- The installation function can run twice without nesting the same wrapper.
- The original callable is the one active when the wrapper is installed.
- The wrapper forwards all arguments and preserves the return value.
- Unknown events and values continue through the original path.
- Exceptions in optional extension behavior are logged narrowly.
- Teardown restores only a wrapper still installed by this mod.
*args and **kwargs
are compatibility aids
Forwarding flexible arguments helps when a client update adds optional parameters. It does not protect against renamed methods, changed return values, moved side effects, or removed call sites. Treat every update as a reason to re-check the current callable and its call sites.
For a complete wrapper, restoration, name-mangling, and packaging example, read First Mod & Safe Patching.
Callback and hook pattern pages
Use Callback Lifecycle Patterns for delayed work and cancellation ownership. Use Hook Composition Rules when a private method must be wrapped safely.