Hook Specifications¶
This document provides comprehensive documentation for the Hop3 plugin hook system, built on pluggy.
Overview¶
Hop3 uses pluggy's hook system to allow plugins to register strategies and extend functionality. Hooks are defined in hop3/core/hookspecs.py and implemented by plugins using the @hookimpl decorator.
Hook System Architecture¶
Hook Definitions¶
Hooks are defined using the @hookspec decorator in hookspecs.py:
from hop3.core.hooks import hookspec
@hookspec
def get_builders() -> list:
"""Get build strategies provided by this plugin."""
Hook Implementations¶
Plugins implement hooks using the @hookimpl decorator:
from hop3.core.hooks import hookimpl
class MyPlugin:
@hookimpl
def get_builders(self) -> list:
return [MyBuildStrategy]
Hook Calling¶
Hooks are called via the plugin manager:
from hop3.core.plugins import get_plugin_manager
pm = get_plugin_manager()
results = pm.hook.get_builders() # Returns list of lists
# Flatten results
builders = [item for sublist in results for item in sublist]
Available Hooks¶
get_builders¶
Purpose: Register builders for converting source code to artifacts.
Location: hop3.core.hookspecs.get_builders
Signature:
@hookspec
def get_builders() -> list:
"""Get build strategies provided by this plugin.
Returns:
List of Builder classes
"""
Returns: List of classes implementing Builder protocol.
Implementation Example:
from hop3.core.hooks import hookimpl
from .python_builder import PythonBuilder
from .docker_builder import DockerBuilder
class MyPlugin:
@hookimpl
def get_builders(self) -> list:
"""Provide Python and Docker build strategies."""
return [PythonBuilder, DockerBuilder]
Usage in Core:
The build pipeline uses this hook to discover available builders:
from hop3.core.plugins import get_build_strategy
# Automatically finds accepting strategy from all registered builders
strategy = get_build_strategy(context, artifact)
Notes:
- Return strategy classes, not instances
- Each class must have a name attribute
- Each class must implement the Builder protocol
- Multiple plugins can provide build strategies
- Strategies are tried in registration order until one accepts
get_deployers¶
Purpose: Register deployment strategies (runtimes) for running artifacts.
Location: hop3.core.hookspecs.get_deployers
Signature:
@hookspec
def get_deployers() -> list:
"""Get deployment strategies provided by this plugin.
Returns:
List of Deployer classes
"""
Returns: List of classes implementing Deployer protocol.
Implementation Example:
from hop3.core.hooks import hookimpl
from .uwsgi_deployer import UWSGIDeployer
from .systemd_deployer import SystemdDeployer
class MyPlugin:
@hookimpl
def get_deployers(self) -> list:
"""Provide uWSGI and systemd deployment strategies."""
return [UWSGIDeployer, SystemdDeployer]
Usage in Core:
Deployment strategies are used in two ways:
-
During deployment (auto-selection):
-
For lifecycle operations (by name):
Notes:
- Return strategy classes, not instances
- Each class must have a name attribute
- Each class must implement the Deployer protocol
- The name attribute is used to match app.runtime field
- Multiple strategies can exist; selection is by accept() or by name
get_addons¶
Purpose: Register service strategies for managing backing services.
Location: hop3.core.hookspecs.get_addons
Signature:
@hookspec
def get_addons() -> list:
"""Get service strategies provided by this plugin.
Returns:
List of Addon classes
"""
Returns: List of classes implementing Addon protocol.
Implementation Example:
from hop3.core.hooks import hookimpl
from .postgres_service import PostgresService
from .redis_service import RedisService
class DatabasePlugin:
@hookimpl
def get_addons(self) -> list:
"""Provide PostgreSQL and Redis service strategies."""
return [PostgresService, RedisService]
Usage in Core:
Services are managed through the CLI and API:
from hop3.core.plugins import get_addon
# Find service strategy by name
strategy_class = get_addon("postgres")
# Instantiate for specific service instance
service = strategy_class(service_name="mydb")
# Use service
service.create()
connection = service.get_connection_details()
Notes:
- Return strategy classes, not instances
- Each class must have a name attribute (service type)
- Each class must implement the Addon protocol
- Service instances are created per database/cache/etc.
- Multiple plugins can provide different service types
get_os_implementations¶
Purpose: Register OS setup strategies for different Linux distributions.
Location: hop3.core.hookspecs.get_os_implementations
Signature:
@hookspec
def get_os_implementations() -> list:
"""Get OS setup strategies provided by this plugin.
Returns:
List of OS classes that can detect and configure
specific operating systems for hop3.
"""
Returns: List of classes implementing OS protocol.
Implementation Example:
from hop3.core.hooks import hookimpl
from .debian import DebianOS
from .ubuntu import UbuntuOS
class DebianPlugin:
@hookimpl
def get_os_implementations(self) -> list:
"""Provide Debian and Ubuntu OS strategies."""
return [DebianOS, UbuntuOS]
Usage in Core:
OS strategies are used during server setup:
from hop3.core.plugins import detect_os
# Auto-detect current OS
os_strategy = detect_os() # Calls detect() on each strategy
# Setup server
os_strategy.setup_server()
Notes:
- Return strategy classes, not instances
- Each class must have name and display_name attributes
- Each class must implement detect() to identify if it matches the current OS
- Only one strategy should detect True per system
- Used primarily by the installer and setup commands
get_proxies¶
Purpose: Register reverse proxy strategies (Nginx, Caddy, Traefik, etc.).
Location: hop3.core.hookspecs.get_proxies
Signature:
@hookspec
def get_proxies() -> list:
"""Get proxy strategies provided by this plugin.
Returns:
List of Proxy classes that can configure reverse proxies
(Nginx, Caddy, Traefik, etc.) for hop3 applications.
"""
Returns: List of classes implementing Proxy or inheriting from BaseProxy.
Implementation Example:
from hop3.core.hooks import hookimpl
from .nginx_proxy import NginxVirtualHost
from .caddy_proxy import CaddyVirtualHost
class ProxyPlugin:
@hookimpl
def get_proxies(self) -> list:
"""Provide Nginx and Caddy proxy strategies."""
return [NginxVirtualHost, CaddyVirtualHost]
Usage in Core:
Proxies are selected based on configuration:
from hop3.config import HopConfig
cfg = HopConfig.get_instance()
proxy_type = cfg.PROXY_TYPE # "nginx", "caddy", etc.
# Find matching proxy strategy
proxy_class = get_proxy_strategy(proxy_type)
# Instantiate and configure
proxy = proxy_class(app=app, env=env, workers=workers)
proxy.setup()
Notes:
- Return proxy classes, not instances
- Should inherit from BaseProxy for code reuse
- Each proxy type should have a unique name
- Only one proxy type is active per hop3 installation
- Configuration is in HopConfig.PROXY_TYPE
get_di_providers¶
Purpose: Register dependency injection providers for the application container.
Location: hop3.core.hookspecs.get_di_providers
Signature:
@hookspec
def get_di_providers() -> list:
"""Get DI providers from this plugin.
Plugins can implement this hook to contribute Dishka providers
to the application's dependency injection container.
Returns:
List of Dishka Provider instances that will be registered
in the application container.
Example:
```python
from dishka import Provider, provide, Scope
class MyPluginProvider(Provider):
scope = Scope.APP
@provide
def get_my_service(self) -> MyService:
return MyService()
@hop3_hook_impl
def get_di_providers() -> list:
return [MyPluginProvider()]
```
"""
Returns: List of Dishka Provider instances (not classes).
Implementation Example:
from dishka import Provider, provide, Scope
from hop3.core.hooks import hookimpl
from .postgres_service import PostgresService
class PostgresProvider(Provider):
"""DI provider for PostgreSQL service."""
scope = Scope.APP
@provide
def get_postgres_service(self) -> PostgresService:
"""Provide PostgresService singleton."""
return PostgresService()
class PostgresPlugin:
@hookimpl
def get_di_providers(self) -> list:
"""Register PostgreSQL service in DI container."""
return [PostgresProvider()]
Usage in Core:
DI providers are collected during application startup:
from hop3.core.plugins import get_plugin_manager
pm = get_plugin_manager()
provider_lists = pm.hook.get_di_providers()
providers = [p for sublist in provider_lists for p in sublist]
# Add to Dishka container
for provider in providers:
container.add_provider(provider)
Notes:
- Return provider instances, not classes (unlike other hooks)
- Use Dishka's Provider class
- Define scope appropriately (Scope.APP, Scope.REQUEST, etc.)
- Services are available for dependency injection in controllers, etc.
- See Dishka documentation for details
cli_commands¶
Purpose: Register custom CLI commands.
Location: hop3.core.hookspecs.cli_commands
Signature:
Returns: None (commands are registered via Click decorators).
Implementation Example:
import click
from hop3.core.hooks import hookimpl
@click.command()
@click.argument("service_name")
def create_postgres(service_name):
"""Create a PostgreSQL database."""
# ... implementation
click.echo(f"Created PostgreSQL service: {service_name}")
class PostgresPlugin:
@hookimpl
def cli_commands(self) -> None:
"""Register postgres CLI command."""
# Register with Click CLI
from hop3.cli import cli
cli.add_command(create_postgres, name="postgres:create")
Usage in Core:
CLI commands are loaded during CLI initialization:
from hop3.core.plugins import get_plugin_manager
pm = get_plugin_manager()
pm.hook.cli_commands() # Triggers all implementations
Notes:
- Use Click for command definitions
- Commands should follow naming convention: service:action
- Commands are added to the main hop CLI
- Return None (registration is side-effect based)
Hook Call Order¶
Pluggy calls hooks in last-registered-first-executed (LIFO) order by default. For Hop3:
- Internal plugins (in
hop3.plugins) are registered first (in package scan order) - External plugins (from entry points) are registered after
This means external plugins can override internal behavior if needed.
Controlling Hook Order¶
To ensure a hook runs first or last, use hookimpl options:
@hookimpl(tryfirst=True)
def get_builders(self) -> list:
"""This will be called before other implementations."""
return [MyBuilder]
@hookimpl(trylast=True)
def get_builders(self) -> list:
"""This will be called after other implementations."""
return [FallbackBuilder]
Return Value Conventions¶
Lists of Classes¶
Most hooks return lists of strategy classes (not instances):
# Correct
@hookimpl
def get_builders(self) -> list:
return [PythonBuilder, NodeBuilder]
# Wrong - don't instantiate
@hookimpl
def get_builders(self) -> list:
return [PythonBuilder(), NodeBuilder()] # ❌
Why: The core code needs to instantiate strategies with specific contexts, artifacts, etc. that aren't available during hook registration.
Exception: DI Providers¶
The get_di_providers() hook is the exception - it returns provider instances:
Hook Implementation Patterns¶
Simple Plugin¶
Implements one hook, provides one strategy:
from hop3.core.hooks import hookimpl
from .redis_service import RedisService
class RedisPlugin:
name = "redis"
@hookimpl
def get_addons(self) -> list:
return [RedisService]
# Auto-register
plugin = RedisPlugin()
Multi-Strategy Plugin¶
Implements one hook, provides multiple strategies:
from hop3.core.hooks import hookimpl
from .python_builder import PythonBuilder
from .node_builder import NodeBuilder
from .ruby_builder import RubyBuilder
class BuildpackPlugin:
name = "buildpack"
@hookimpl
def get_builders(self) -> list:
return [PythonBuilder, NodeBuilder, RubyBuilder]
plugin = BuildpackPlugin()
Multi-Hook Plugin¶
Implements multiple hooks:
from hop3.core.hooks import hookimpl
from .docker_builder import DockerBuilder
from .docker_deployer import DockerDeployer
class DockerPlugin:
name = "docker"
@hookimpl
def get_builders(self) -> list:
return [DockerBuilder]
@hookimpl
def get_deployers(self) -> list:
return [DockerDeployer]
plugin = DockerPlugin()
Plugin with DI¶
Provides both strategies and DI providers:
from dishka import Provider, provide, Scope
from hop3.core.hooks import hookimpl
from .postgres_service import PostgresService
class PostgresProvider(Provider):
scope = Scope.APP
@provide
def get_postgres(self) -> PostgresService:
return PostgresService()
class PostgresPlugin:
name = "postgres"
@hookimpl
def get_addons(self) -> list:
return [PostgresService]
@hookimpl
def get_di_providers(self) -> list:
return [PostgresProvider()]
plugin = PostgresPlugin()
Testing Hooks¶
Testing Hook Registration¶
Verify your plugin is discovered and hooks are called:
from hop3.core.plugins import get_plugin_manager
def test_plugin_registered():
"""Test that plugin hooks are called."""
pm = get_plugin_manager()
# Get all build strategies
results = pm.hook.get_builders()
strategies = [cls for sublist in results for cls in sublist]
# Check our strategy is in the list
strategy_names = [getattr(cls, "name", None) for cls in strategies]
assert "my-builder" in strategy_names
Testing Hook Implementations¶
Test the hook implementation directly:
from my_plugin.plugin import MyPlugin
def test_get_build_strategies():
"""Test hook implementation returns correct strategies."""
plugin = MyPlugin()
strategies = plugin.get_builders()
assert len(strategies) == 2
assert strategies[0].name == "python"
assert strategies[1].name == "node"
Testing Strategy Discovery¶
Test that the core correctly discovers and uses your strategy:
from hop3.core.plugins import get_build_strategy
from hop3.core.protocols import DeploymentContext
def test_strategy_discovery(tmp_path):
"""Test that build strategy is discovered and used."""
# Create test app
(tmp_path / "requirements.txt").write_text("flask==2.0.0")
context = DeploymentContext(
app_name="test",
source_path=tmp_path,
app_config={}
)
# Should find our Python builder
strategy = get_build_strategy(context)
assert strategy.name == "python"
Common Pitfalls¶
1. Returning Instances Instead of Classes¶
Wrong:
Correct:
2. Forgetting to Register Plugin Instance¶
Wrong:
# plugin.py
class MyPlugin:
@hookimpl
def get_builders(self):
return [MyBuilder]
# Missing: plugin = MyPlugin()
Correct:
class MyPlugin:
@hookimpl
def get_builders(self):
return [MyBuilder]
# Must instantiate for auto-registration
plugin = MyPlugin()
3. Not Importing Plugin¶
Wrong:
Correct:
4. Incorrect Return Type¶
Wrong:
Correct:
5. Not Flattening Hook Results¶
Wrong:
Correct:
results = pm.hook.get_builders()
strategies = [cls for sublist in results for cls in sublist]
# strategies = [Builder1, Builder2, Builder3] ✅ Flat list
Advanced Hook Usage¶
Conditional Strategy Registration¶
Only register strategies if certain conditions are met:
@hookimpl
def get_builders(self) -> list:
"""Only provide Docker builder if Docker is available."""
strategies = []
if self._is_docker_available():
strategies.append(DockerBuilder)
return strategies
def _is_docker_available(self) -> bool:
import shutil
return shutil.which("docker") is not None
Dynamic Strategy Generation¶
Generate strategies based on configuration:
@hookimpl
def get_deployers(self) -> list:
"""Provide strategies based on config."""
from hop3.config import HopConfig
cfg = HopConfig.get_instance()
strategies = [UWSGIDeployer] # Always available
if cfg.ENABLE_DOCKER:
strategies.append(DockerDeployer)
if cfg.ENABLE_SYSTEMD:
strategies.append(SystemdDeployer)
return strategies
Hook Wrappers¶
Wrap other plugins' hooks (advanced):
@hookimpl(hookwrapper=True)
def get_builders(self):
"""Wrap and modify other plugins' results."""
# Get results from other plugins
outcome = yield
results = outcome.get_result()
# Add our own strategies
results.append([MyCustomBuilder])
# Return modified results
outcome.force_result(results)
See Also¶
- Plugin Development Guide - How to create plugins
- Protocol Reference - Strategy protocol specifications
- External Plugin Guide - Publishing external plugins
- pluggy Documentation - Hook system details