Factories
Use factories to handle complex creation logic, instantiate third-party classes, or manage resources that can't be handled by simple class constructors.
Third-party Classes¶
You can't add @injectable to code you don't own. Factories solve this by wrapping third-party classes in a function
you control. The factory creates and configures the object, and Wireup registers it by the return type.
Example: Redis Client
from typing import Annotated
import redis
from wireup import Inject, injectable
@injectable
def redis_factory(
url: Annotated[str, Inject(config="redis_url")],
) -> redis.Redis:
return redis.from_url(url)
# Usage:
@injectable
class AuthService:
def __init__(self, cache: redis.Redis):
self.cache = cache
Pure Domain Objects¶
If you prefer to keep your domain layer free of Wireup imports, don't use @injectable on those classes. Instead,
create factory functions in a separate wiring module that construct and return them.
# No Wireup imports here
class UserService:
def __init__(self, repository: UserRepository) -> None:
self.repository = repository
from wireup import injectable
from domain.services import UserService
@injectable
def user_service_factory(repository: UserRepository) -> UserService:
return UserService(repository)
Complex Initialization¶
Some objects need conditional logic, multi-step setup, or configuration that depends on the environment. Factories let you encapsulate this logic in one place rather than scattering it across your codebase.
from wireup import injectable
@injectable
def db_connection_factory(config: AppConfig) -> DatabaseConnection:
timeout = config.timeout if config.is_production else 30
conn = DatabaseConnection(dsn=config.dsn, timeout=timeout)
conn.set_encoding("utf-8")
return conn
Injecting Primitives¶
If you register two factories that both return str, Wireup can't tell them apart. Use typing.NewType to create
distinct types so each primitive can be requested independently.
from typing import NewType
from wireup import injectable
AuthenticatedUsername = NewType("AuthenticatedUsername", str)
@injectable(lifetime="scoped")
def authenticated_username_factory(auth: AuthService) -> AuthenticatedUsername:
user = auth.get_current_user()
return AuthenticatedUsername(user.username)
Usage:
from wireup import injectable
@injectable(lifetime="scoped")
class UserProfileService:
def __init__(self, username: AuthenticatedUsername):
self.username = username
Strategy Pattern¶
When the correct implementation depends on runtime state (user preferences, feature flags, environment), a factory can inspect other dependencies and return the appropriate one.
from typing import Protocol
from wireup import injectable
class Notifier(Protocol):
def notify(self, message: str): ...
@injectable(lifetime="scoped")
def get_user_notifier(
user: AuthenticatedUser,
slack: SlackNotifier,
email: EmailNotifier,
) -> Notifier:
if user.prefers_slack:
return slack
return email
Models and DTOs¶
Sometimes you want to inject data that comes from another service, like the currently authenticated user. A factory can call that service and return the result, making it available as a dependency.
from wireup import injectable
@injectable(lifetime="scoped")
def get_current_user(auth_service: AuthService) -> AuthenticatedUser:
return auth_service.get_current_user()
Optional Dependencies¶
Factories can return None when a dependency is available only under certain conditions, such as a cache that is
disabled in development.
For the general optional-dependency rules, including parameters satisfied via Python default values, see Injectables: Optional Dependencies.
from wireup import injectable
@injectable
def cache_factory(settings: Settings) -> Redis | None:
if not settings.use_cache:
return None
return Redis.from_url(settings.redis_url)
Consumers can then request T | None as usual:
from wireup import injectable
@injectable
class UserService:
# Use T | None (Python 3.10+) or Optional[T]
def __init__(self, cache: Redis | None) -> None:
self.cache = cache
def get(self, user_id: str):
if self.cache:
return self.cache.get(user_id)
# ...
When a factory returns Redis | None, request it as Redis | None (or Optional[Redis]).
Null Object Pattern for Optional Dependencies
Instead of adding conditional checks throughout your code, use the pattern to handle optional dependencies cleanly. It involves creating a noop implementation that can be used when the real implementation is not available.
from typing import Annotated, Any, Protocol
from wireup import Inject, injectable
class Cache(Protocol):
def get(self, key: str) -> Any | None: ...
def set(self, key: str, value: str) -> None: ...
class RedisCache: ... # Real Redis implementation
class NullCache:
def get(self, key: str) -> Any | None:
return None # Always cache miss
def set(self, key: str, value: str) -> None:
return None # Do nothing
@injectable
def cache_factory(
redis_url: Annotated[str | None, Inject(config="redis_url")],
) -> Cache:
return RedisCache(redis_url) if redis_url else NullCache()
Usage
@injectable
class UserService:
def __init__(self, cache: Cache | None):
self.cache = cache
def get_user(self, user_id: str) -> User:
# Guard required
if self.cache and (cached := self.cache.get(f"user:{user_id}")):
return User.from_json(cached)
user = self.db.get_user(user_id)
# Guard required
if self.cache:
self.cache.set(f"user:{user_id}", user.to_json())
return user
@injectable
class UserService:
def __init__(self, cache: Cache):
self.cache = cache # Always a Cache instance
def get_user(self, user_id: str) -> User:
if cached := self.cache.get(f"user:{user_id}"):
return User.from_json(cached)
user = self.db.get_user(user_id)
self.cache.set(f"user:{user_id}", user.to_json())
return user