How to use service definition ============================== This guide shows how to use the ``ServiceDefinition`` class for advanced service configuration. When to use service definition ------------------------------- Use ``ServiceDefinition`` when you need: - Custom factory functions for service creation - Named service registrations (for future multi-registration support) - Pre-configured service definitions to share across modules - More explicit control over service configuration Basic usage ----------- Creating a service definition ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from hdmi import ServiceDefinition # Basic definition (singleton is default) definition = ServiceDefinition(MyService) # singleton (scoped=False, transient=False) # Or explicitly specify scoped or transient scoped_definition = ServiceDefinition(MyService, scoped=True) transient_definition = ServiceDefinition(MyService, transient=True) # Note: service_type must be positional, boolean flags must be keyword # This will fail: # definition = ServiceDefinition(service_type=MyService) # TypeError # definition = ServiceDefinition(MyService, True) # TypeError - use scoped=True Registering with container builder ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from hdmi import ContainerBuilder, ServiceDefinition builder = ContainerBuilder() # Register a ServiceDefinition definition = ServiceDefinition(DatabaseService) # singleton (default) builder.register(definition) # IMPORTANT: Cannot specify boolean flags when registering ServiceDefinition # This will raise ValueError: # builder.register(definition, scoped=True) # ValueError! # For simple registrations, use the shorthand: builder.register(UserService, scoped=True) # scoped service Using custom factories ---------------------- ServiceDefinition allows you to provide a custom factory function: .. code-block:: python from hdmi import ContainerBuilder, ServiceDefinition def create_database_connection(): """Custom factory that configures the database.""" return DatabaseConnection( host="localhost", port=5432, database="myapp" ) # Create definition with factory (singleton by default) db_definition = ServiceDefinition( DatabaseConnection, factory=create_database_connection ) import asyncio async def main(): builder = ContainerBuilder() builder.register(DatabaseConnection, factory=create_database_connection) async with builder.build() as container: db = await container.get(DatabaseConnection) # Uses the factory asyncio.run(main()) Factory requirements ~~~~~~~~~~~~~~~~~~~~ - Must be callable (function, method, or callable object) - Must be passed as keyword argument - Will be validated when ServiceDefinition is created .. code-block:: python # Valid factories def factory_function(): return MyService() definition = ServiceDefinition(MyService, factory=factory_function) # Invalid - not callable definition = ServiceDefinition(MyService, factory="not_callable") # Raises: ValueError: factory must be callable Using lifecycle hooks --------------------- Services can have initializers (called after instantiation) and finalizers (called when the container exits). Both can be sync or async. Initializers ~~~~~~~~~~~~ Initializers are called after a service is instantiated: .. code-block:: python import asyncio from hdmi import ContainerBuilder class DatabaseConnection: def __init__(self): self.connected = False def connect(self): self.connected = True async def main(): builder = ContainerBuilder() # Sync initializer builder.register( DatabaseConnection, initializer=lambda db: db.connect() ) async with builder.build() as container: db = await container.get(DatabaseConnection) assert db.connected # initializer was called asyncio.run(main()) Async initializers are also supported: .. code-block:: python async def async_connect(db): await db.connect_async() builder.register(DatabaseConnection, initializer=async_connect) Finalizers ~~~~~~~~~~ Finalizers are called when the container or scope exits: .. code-block:: python import asyncio from hdmi import ContainerBuilder class DatabaseConnection: def __init__(self): self.connected = True def disconnect(self): self.connected = False async def main(): builder = ContainerBuilder() builder.register( DatabaseConnection, finalizer=lambda db: db.disconnect() ) db_instance = None async with builder.build() as container: db_instance = await container.get(DatabaseConnection) assert db_instance.connected # After exiting the context, finalizer is called assert not db_instance.connected asyncio.run(main()) Combined initializer and finalizer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python builder.register( DatabaseConnection, initializer=lambda db: db.connect(), finalizer=lambda db: db.disconnect() ) Controlling autowiring ---------------------- The ``autowire`` parameter controls whether a service is automatically injected into optional dependencies. By default, ``autowire=True``. .. code-block:: python from hdmi import ContainerBuilder class OptionalFeature: pass class MyService: def __init__(self, feature: OptionalFeature | None = None): self.feature = feature # With autowire=True (default), OptionalFeature is injected if registered builder = ContainerBuilder() builder.register(OptionalFeature) # autowire=True by default builder.register(MyService) async with builder.build() as container: service = await container.get(MyService) assert service.feature is not None # OptionalFeature was injected # With autowire=False, the service is not injected into optional dependencies builder = ContainerBuilder() builder.register(OptionalFeature, autowire=False) builder.register(MyService) async with builder.build() as container: service = await container.get(MyService) assert service.feature is None # Default value used instead Named services -------------- ServiceDefinition supports named registrations for future multi-registration scenarios: .. code-block:: python primary_db = ServiceDefinition( DatabaseConnection, name="primary" # singleton by default ) readonly_db = ServiceDefinition( DatabaseConnection, name="readonly", factory=create_readonly_connection # singleton by default ) builder = ContainerBuilder() builder.register(primary_db) builder.register(readonly_db) .. note:: Named service resolution is planned for future releases. Currently, names are stored but not used for resolution. Exporting and importing definitions ------------------------------------ ServiceDefinition is now exported from the main hdmi package: .. code-block:: python # Direct import from hdmi from hdmi import ServiceDefinition, ContainerBuilder # Create reusable definitions (all singleton by default) STANDARD_SERVICES = [ ServiceDefinition(LoggingService), ServiceDefinition(ConfigService), ServiceDefinition(CacheService), ] # Use in multiple places def configure_container(): builder = ContainerBuilder() for definition in STANDARD_SERVICES: builder.register(definition) return builder Common patterns --------------- Module-level definitions ~~~~~~~~~~~~~~~~~~~~~~~~ Define services at module level for reuse: .. code-block:: python # services.py from hdmi import ServiceDefinition from myapp.database import DatabaseConnection from myapp.cache import RedisCache # Export pre-configured definitions (singleton by default) database_definition = ServiceDefinition( DatabaseConnection, factory=lambda: DatabaseConnection.from_env() ) cache_definition = ServiceDefinition( RedisCache, name="main_cache" # singleton by default ) # main.py from hdmi import ContainerBuilder from services import database_definition, cache_definition builder = ContainerBuilder() builder.register(database_definition) builder.register(cache_definition) Testing with factories ~~~~~~~~~~~~~~~~~~~~~~ Use factory functions to override services in tests: .. code-block:: python # test_services.py import pytest from hdmi import ContainerBuilder @pytest.mark.anyio async def test_with_mock_database(): # Create mock with custom factory (singleton by default) builder = ContainerBuilder() builder.register(DatabaseConnection, factory=lambda: MockDatabase()) builder.register(UserService) # depends on DatabaseConnection async with builder.build() as container: service = await container.get(UserService) # UserService will use the mock database Best practices -------------- 1. **Use shorthand for simple registrations**: If you only need type and scope, use ``builder.register(Type, scoped=True)`` or ``builder.register(Type, transient=True)`` directly. 2. **Always use async context manager**: Use ``async with builder.build() as container:`` to ensure proper lifecycle management and finalizer execution. 3. **Use initializers for setup**: Rather than calling setup methods manually, use initializers to ensure services are properly configured. 4. **Use finalizers for cleanup**: Register finalizers for services that need cleanup (database connections, file handles, etc.) to ensure resources are released. 5. **Use autowire=False for optional features**: When a service should only be injected explicitly, not automatically into optional dependencies. 6. **Validate early**: ServiceDefinition validates the factory, initializer, and finalizer parameters immediately, catching errors early. See also -------- - :doc:`/reference/api` for complete ServiceDefinition API - :doc:`/explanation/architecture` for understanding service lifecycles - :doc:`/tutorials/index` for step-by-step examples