Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation: "Using Scopes to manage Resources" #252

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions docs/scopes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,105 @@ Scopes can be retrieved from the injector, as with any other instance. They are
True

For scopes with a transient lifetime, such as those tied to HTTP requests, the usual solution is to use a thread or greenlet-local cache inside the scope. The scope is "entered" in some low-level code by calling a method on the scope instance that creates this cache. Once the request is complete, the scope is "left" and the cache cleared.

Using Scopes to manage Resources
````````````````````````````````

Sometimes You need to inject classes, which manage resources, like database
connections. Imagine You have an :class:`App`, which depends on multiple other
services and some of these services need to access the Database. The naive
approach would be to open and close the connection everytime it is needed::

class App:
 @inject
def __init__(self, service1: Service1, service2: Service2):
Service1()
Service2()

class Service1:
def __init__(self, cm: ConnectionManager):
cm.openConnection()
# do something with the opened connection
cm.closeConnection()

class Service2:
def __init__(self, cm: ConnectionManager):
cm.openConnection()
# do something with the opened connection
cm.closeConnection()

Now You may figure, that this is inefficient. Instead of opening a new
connection everytime a connection is requested, it may be useful to reuse
already opened connections. But how and when should these connections be closed
in the example above?

This can be achieved with some small additions to the :class:`SingletonScope` and
to :class:`Injector` we can create singletons, which will be
cared for automatically, if they only implement a :meth:`cleanup` method and are
associated with our custom scope. Let's reduce our example from above a bit for
the sake of brevity to just one class, which needs cleanup. Remark the `@cleaned`
decorator, which we will implement shortly afterwards and which will associate the
class with our custom scope::

@cleaned
class NeedsCleanup:
def __init__(self) -> None:
print("NeedsCleanup: I'm alive and claiming lot's of resources!")

def doSomething(self):
print("NeedsCleanup: Now I have plenty of time to work with these resources.")

def cleanup(self):
print("NeedsCleanup: Freeing my precious resources!")

To achieve this, we first need to create a custom scope. This scope will just
collect all singletons, which were accessed using the :meth:`Scope.get`-method::

T = TypeVar('T')

class CleanupScope(SingletonScope):
def __init__(self, injector: 'Injector') -> None:
super().__init__(injector)
# We have singletons here, so never cache them twice, since otherwise
# the cleanup method might be invoked twice.
self.cachedProviders = set()

def get(self, key: Type[T], provider: Provider[T]) -> Provider[T]:
obj = super().get(key, provider)
self.cachedProviders.add(obj)
return obj

cleaned = ScopeDecorator(CleanupScope)

Next we will also create a custom :class:`Injector`, which will do the cleanup of all
our objects belonging to :class:`CleanupScope` after a call to :meth:`get`::

ScopeType = Union[ScopeDecorator, Type[Scope], None]

class CleanupInjector:
def __init__(self, injector: Injector) -> None:
self.injector = injector

@contextmanager
def get(self, interface: Type[T], scope: ScopeType = None) -> Generator[T, None, None]:
yield self.injector.get(interface, scope)
self.cleanup()

def cleanup(self):
print("CleanupInjector: Invoking 'cleanup' for all who need it.")
cleanupScope = self.injector.get(CleanupScope)
for provider in cleanupScope.cachedProviders:
obj = provider.get(self.injector)
if hasattr(obj, 'cleanup') and callable(obj.cleanup):
obj.cleanup()

Now we can simply use our custom injector and freeing resources will be done for
each object in :class:`CleanupScope` automatically::

injector = CleanupInjector(Injector())
with injector.get(NeedsCleanup) as obj:
obj.doSomething()

This is of course a simple example. In a real world example `NeedsCleanup` could
be nested deep and multiple times anywhere in a dependency structure. This
pattern would work irrespectively of where `NeedsCleanup` would be injected.