Auto-Registration
AnyDI can auto-register dependencies without explicit registration. When you resolve a decorated class, the container automatically registers and resolves its entire dependency tree.
Decorators
@provided
The main decorator for auto-registration. Requires a scope:
from anydi import Container, provided
@provided(scope="singleton")
class UserRepository:
def find(self, id: int) -> dict:
return {"id": id, "name": "Alice"}
container = Container()
repo = container.resolve(UserRepository)
Shortcuts
@singleton, @transient, and @request are shortcuts:
| Decorator | Equivalent |
|---|---|
@singleton |
@provided(scope="singleton") |
@transient |
@provided(scope="transient") |
@request |
@provided(scope="request") |
from anydi import singleton, transient
@singleton
class Database:
pass
@transient
class RequestHandler:
pass
Decoupled with __provided__
To avoid importing decorators, use the __provided__ class variable:
class UserRepository:
__provided__ = {"scope": "singleton"}
def find(self, id: int) -> dict:
return {"id": id, "name": "Alice"}
Supported keys:
scope(required):"singleton","transient", or"request"alias(optional): type or list of types to register asfrom_context(optional):Truefor request-scoped context values
Aliases
Use alias to make a class resolvable by an interface or base type:
from abc import ABC, abstractmethod
from anydi import Container, singleton
class IRepository(ABC):
@abstractmethod
def find(self, id: int) -> dict:
pass
@singleton(alias=IRepository)
class UserRepository(IRepository):
def find(self, id: int) -> dict:
return {"id": id, "name": "Alice"}
container = Container()
container.scan(["myapp.repositories"])
repo = container.resolve(UserRepository) # By class
repo2 = container.resolve(IRepository) # By interface
assert repo is repo2
Multiple aliases:
@singleton(alias=[IReader, IWriter])
class CRUDRepository(IReader, IWriter):
pass
Note
The class is the primary registration. Aliases are additional keys resolving to the same instance. See Type Aliases.
Request Scope with Context
Use from_context=True when the instance is set via context.set():
from anydi import Container, request
@request(from_context=True)
class Request:
def __init__(self, path: str) -> None:
self.path = path
container = Container()
with container.request_context() as ctx:
ctx.set(Request, Request(path="/users"))
req = container.resolve(Request)
assert req.path == "/users"
Generic TypeVar Resolution
AnyDI automatically resolves TypeVars when inheriting from generic base classes:
from typing import Generic, TypeVar
from anydi import Container
T = TypeVar("T")
class User:
pass
class Repository(Generic[T]):
pass
class UserRepository(Repository[User]):
pass
class Handler(Generic[T]):
def __init__(self, repo: Repository[T]) -> None:
self.repo = repo
class UserHandler(Handler[User]):
pass # No need to override __init__!
container = Container()
container.register(UserRepository, alias=Repository[User])
container.register(UserHandler)
handler = container.resolve(UserHandler)
assert isinstance(handler.repo, UserRepository)
This works with:
- Multi-level inheritance:
A[T] → B[T] → C[User] - Multiple type parameters:
Handler[T, U]with partial specialization - Nested generics:
list[Repository[T]]→list[Repository[User]]
Scanning
Use scan() to discover decorated classes at startup:
from anydi import Container
container = Container()
# 1. Scan packages to find decorated classes
container.scan(["myapp.services", "myapp.repositories"])
# 2. Validate the dependency graph
container.build()
# 3. Use the container
service = container.resolve(MyService)
Ignoring packages
Exclude packages from scanning:
container.scan("myapp", ignore=["myapp.tests", "myapp.migrations"])
The ignore parameter accepts strings, module objects, or a mix of both.
Relative paths
Use relative paths for portable configuration:
# myapp/container.py
container.scan(".") # Current package
container.scan([".services", ".repos"]) # Submodules
container.scan(".", ignore=[".api"]) # Relative ignore
Circular import detection
If a scanned module imports the container at module level, it triggers another scan() call. AnyDI detects this and raises RuntimeError.
Solutions:
- Use lazy imports inside functions
- Add problematic modules to
ignore - Keep container in a module that doesn't get scanned
Mixing with Explicit Registration
Combine explicit and auto-registration:
from anydi import Container, singleton
class EmailService:
def send(self, to: str, message: str) -> None:
print(f"Sending to {to}")
@singleton
class NotificationService:
def __init__(self, email: EmailService) -> None:
self.email = email
container = Container()
container.register(EmailService, scope="singleton")
# NotificationService auto-registers when resolved
notifier = container.resolve(NotificationService)
Benefits
- Less boilerplate: No manual registration for every class
- Maintainability: Adding dependencies doesn't require updating registration
- Flexibility: Override specific dependencies while others auto-register
Limitations
- Explicit is better: For public APIs, explicit registration provides better documentation
- Circular dependencies: Auto-registration cannot resolve circular dependencies
- Scope validation: Scope decorators must match usage patterns
Related:
- Provider Basics - Explicit provider registration
- Resource Management - Lifecycle of auto-registered resources
- Scopes - Understanding scope decorators