Creating dependency containers
In this example, we demonstrate how to create and retrieve dependency containers using the DependencyContainer class. This is useful when you want to manage dependencies in different contexts or areas of your application.
from dependency_injection.container import DependencyContainer
# Get the default dependency container
dependency_container = DependencyContainer.get_instance()
# Get an additional container if necessary
another_container = DependencyContainer.get_instance(
name="another_container"
)
Registering dependencies with scopes
This example shows how to register dependencies with different scopes (transient, scoped, and singleton). This is important for controlling the lifecycle and reuse of your dependencies.
# Register a transient dependency
dependency_container.register_transient(
Connection,
PostgresConnection
)
# Register a scoped dependency
dependency_container.register_scoped(
Connection,
PostgresConnection,
scope_name="http_request"
)
# Register a singleton dependency
dependency_container.register_singleton(
Connection,
PostgresConnection
)
Registering with constructor arguments
Here, we illustrate how to register a dependency with constructor arguments. This allows you to provide specific values or configurations to your dependencies when they are instantiated.
# Register with constructor arguments
dependency_container.register_transient(
Connection,
PostgresConnection,
constructor_args={
"host": "localhost",
"port": 5432
}
)
Resolving with factory functions
In this section, we demonstrate how to register and resolve dependencies using the factory pattern. This provides flexibility in how your dependencies are created and configured. You can use factory functions, factory classes and factory lambdas.
Note
Any callable can be used as factory.
# Define factory function
def factory_function(host: str, port: int) -> Connection:
return PostgresConnection(
host=host,
port=port
)
# Register with factory function
dependency_container.register_factory(
Connection,
factory_function,
factory_args={
"host": "localhost",
"port": 5432
}
)
# Define factory class
class FactoryClass:
@staticmethod
def create(host: str, port: int) -> Connection:
return PostgresConnection(
host=host,
port=port
)
# Register with factory class
dependency_container.register_factory(
Connection,
FactoryClass.create,
factory_args={
"host": "localhost",
"port": 5432
}
)
# Register with lambda factory function
dependency_container.register_factory(
Connection,
lambda host, port: PostgresConnection(
host=host,
port=port
),
factory_args={
"host": "localhost",
"port": 5432
}
)
Registering and using instances
This example demonstrates how to register and use instances of your dependencies. This is useful when you want to provide a specific instance of a dependency for use throughout your application.
# Create instance
instance = PostgresConnection(
host="localhost",
port=5432
)
# Register instance
dependency_container.register_instance(
Connection,
instance
)
# Resolve instance
resolved_instance = dependency_container.resolve(Connection)
print(resolved_instance.host) # Output: localhost
Using constructor injection
This example illustrates how to use constructor injection to automatically inject dependencies into your classes. This is a common pattern for managing dependencies in object-oriented programming. This is probably how you’ll want to resolve 99% of the dependencies in your software application.
class OrderRepository:
def __init__(self, connection: Connection):
self.connection = connection
# Register dependencies
dependency_container.register_transient(
OrderRepository
)
dependency_container.register_singleton(
Connection,
PostgresConnection
)
# Resolve with injected dependencies
repository = dependency_container.resolve(
OrderRepository
)
# Use injected dependency
print(repository.connection.__class__.__name__) # Output: PostgresConnection
Using constructor injection with tagged dependencies
This example demonstrates how to use constructor injection to automatically inject tagged dependencies into your classes. By leveraging tags, you can group and categorize dependencies, enabling automatic injection based on specific criteria.
class PrimaryPort:
pass
class SecondaryPort:
pass
class HttpAdapter(PrimaryPort):
pass
class PostgresCarRepository(SecondaryPort):
pass
class Application:
def __init__(self, primary_ports: List[Tagged[PrimaryPort]], secondary_ports: List[Tagged[SecondaryPort]]):
self.primary_ports = primary_ports
self.secondary_ports = secondary_ports
# Register dependencies with tags
dependency_container.register_transient(HttpAdapter, tags={PrimaryPort})
dependency_container.register_transient(PostgresCarRepository, tags={SecondaryPort})
# Register the Application class to have its dependencies injected
dependency_container.register_transient(Application)
# Resolve the Application class, with tagged dependencies automatically injected
application = dependency_container.resolve(Application)
# Use the injected dependencies
print(f"Primary ports: {len(application.primary_ports)}") # Output: Primary ports: 1
print(f"Secondary ports: {len(application.secondary_ports)}") # Output: Secondary ports: 1
print(f"Primary port instance: {type(application.primary_ports[0]).__name__}") # Output: HttpAdapter
print(f"Secondary port instance: {type(application.secondary_ports[0]).__name__}") # Output: PostgresCarRepository
In this example, the Application class expects lists of instances tagged with PrimaryPort and SecondaryPort. By tagging and registering these dependencies, the container automatically injects the correct instances into the Application class when it is resolved.
Tags offer a powerful way to manage dependencies, ensuring that the right instances are injected based on your application’s needs.
Note
You can also use the AnyTagged and AllTagged classes to inject dependencies based on more complex tagging logic. AnyTagged allows injection of any dependency matching one or more specified tags, while AllTagged requires the dependency to match all specified tags before injection. This provides additional flexibility in managing and resolving dependencies in your application.
Using method injection
This example demonstrates how to use method injection to inject dependencies into methods at runtime. This is useful for dynamically providing dependencies to class- or static methods, without affecting the entire class.
Note
You can pass the arguments container_name and scope_name to @inject.
Note
The @inject has to be applied to the function after the @classmethod or @staticmethod.
class OrderController:
@staticmethod
@inject()
def place_order(order: Order, repository: OrderRepository):
order.set_status("placed")
repository.save(order)
# Register dependencies
dependency_container.register_transient(
OrderRepository
)
dependency_container.register_singleton(
Connection,
PostgresConnection
)
# Call decorated method (missing argument will be injected)
OrderController.place_order(
order=Order.create()
)