Dependency Injection: Scopes
Follows #1
When a constructor is registered in the DI Container, registrar can define a scope of the instance created by the constructor. I think of them as an Enum of objects (functions? objects with state?), that know when they want to create a new instance and when to return the old one.
I think of them as something like the code below:
from enum import Enum
import typing as t
from pca.utils.functools import reify
class Container:
_singleton_objects = {}
@reify
def _thread_local(self):
from threading import local
return local()
def find_by_interface(self, interface):
raise NotImplementedError
def register_by_interface(self, interface):
raise NotImplementedError
def instance_scope(container: Container, constructor: t.Callable) -> t.Any:
"""Every injection makes a new instance."""
return constructor()
def thread_scope(container: Container, constructor: t.Callable) -> t.Any:
_thread_local = container._thread_local
try:
objects = _thread_local.objects
except AttributeError:
objects = {}
_thread_local.objects = objects
if constructor in objects:
return objects[constructor]
else:
obj = constructor()
objects[constructor] = obj
return obj
def request_scope(container: Container, constructor: t.Callable) -> t.Any:
"""
Created once per request in the container. Uses Container to find the request
(whatever it is in your application), so don't get caught into infinite recursion
using the scope onto request.
"""
# TODO should request has the same API find/register_by_interface?
# TODO or should request generate some UUID which is used as a key for the container
# TODO but when & how to clean objects for requests that are already dead
request = container.find_by_interface(IRequest)
try:
return request.find_by_interface[constructor]
except KeyError:
obj = constructor()
request.register_by_interface[constructor] = obj
return obj
def session_scope(container: Container, constructor: t.Callable) -> t.Any:
# TODO same problem as in the request_scope
raise NotImplementedError
def singleton_scope(container: Container, constructor: t.Callable) -> t.Any:
"""Created only once in the container."""
try:
return container._singleton_objects[constructor]
except KeyError:
obj = constructor()
container._singleton_objects[constructor] = obj
return obj
class Scopes(Enum):
INSTANCE = instance_scope # every injection makes a new instance
REQUEST = request_scope # per a "request" (whatever a request is in your application)
SESSION = session_scope # per session (whatever is a user session in your application)
THREAD = thread_scope # per thread (via `threading.local`)
SINGLETON = singleton_scope # always the same instance in this container
def get_object(self, container: Container, constructor: t.Callable):
return self.value(container, constructor)
The snippet above is not all that is needed. Consider following use-case:
@scope(Scopes.SINGLETON)
class ConsoleMailer(IMailer):
def __init__(self, container, session):
self.container = container
def send_to_user(self, body: str, ...):
print("Sending email to current user")
print(body)
@scope(Scopes.SESSION)
class SmtpMailer(IMailer):
def __init__(self, container):
self.container = container
def send_to_user(self, body: str, ...):
session = self.container.find_by_interface(ISession)
email.send(to=session.user.email, body=body, ...)
# app.py
Container.register_by_interface(IMailer, SmptMailer)
# ctl.py
Container.register_by_interface(IMailer, ConsoleMailer)
So:
- DI scope might be defined at the implementation level, not by the registrar
- scopes may pass something to the constructor (the Container!!) so it would be nice if the
scopedecorator check for the possibility for passing the container
I have flagged this issue as Important: goals of incoming version (see roadmap) will need session scope and request scope, at least in the context of CLI.
Note: the request scope in the context of CLI is just a singleton scope, as the process running CLI command finishes at the end of the request. I See two solutions:
- scope choosing has some kind of fallback mode (I have no request scope then I choose the singleton)
- or it has to be by passing strategies to Container construction?