Skip to content

Commit 09274c9

Browse files
bschnitzbschnitz-work
authored andcommitted
Documentation: "Using Scopes to manage Resources"
1 parent 78bb621 commit 09274c9

File tree

1 file changed

+102
-0
lines changed

1 file changed

+102
-0
lines changed

docs/scopes.rst

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,105 @@ Scopes can be retrieved from the injector, as with any other instance. They are
6262
True
6363

6464
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.
65+
66+
Using Scopes to manage Resources
67+
````````````````````````````````
68+
69+
Sometimes You need to inject classes, which manage resources, like database
70+
connections. Imagine You have an :class:`App`, which depends on multiple other
71+
services and some of these services need to access the Database. The naive
72+
approach would be to open and close the connection everytime it is needed::
73+
74+
class App:
75+
 @inject
76+
def __init__(self, service1: Service1, service2: Service2):
77+
Service1()
78+
Service2()
79+
80+
class Service1:
81+
def __init__(self, cm: ConnectionManager):
82+
cm.openConnection()
83+
# do something with the opened connection
84+
cm.closeConnection()
85+
86+
class Service2:
87+
def __init__(self, cm: ConnectionManager):
88+
cm.openConnection()
89+
# do something with the opened connection
90+
cm.closeConnection()
91+
92+
Now You may figure, that this is inefficient. Instead of opening a new
93+
connection everytime a connection is requested, it may be useful to reuse
94+
already opened connections. But how and when should these connections be closed
95+
in the example above?
96+
97+
This can be achieved with some small additions to the :class:`SingletonScope` and
98+
to :class:`Injector` we can create singletons, which will be
99+
cared for automatically, if they only implement a :meth:`cleanup` method and are
100+
associated with our custom scope. Let's reduce our example from above a bit for
101+
the sake of brevity to just one class, which needs cleanup. Remark the `@cleaned`
102+
decorator, which we will implement shortly afterwards and will associate the
103+
class with our custom scope::
104+
105+
@cleaned
106+
class NeedsCleanup:
107+
def __init__(self) -> None:
108+
print("NeedsCleanup: I'm alive and claiming lot's of resources!")
109+
110+
def doSomething(self):
111+
print("NeedsCleanup: Now I have plenty of time to work with these resources.")
112+
113+
def cleanup(self):
114+
print("NeedsCleanup: Freeing my precious resources!")
115+
116+
To achieve this, we first need to create a custom scope. This scope will just
117+
collect all singletons, which were accessed using the :meth:`Scope.get`-method::
118+
119+
T = TypeVar('T')
120+
121+
class CleanupScope(SingletonScope):
122+
def __init__(self, injector: 'Injector') -> None:
123+
super().__init__(injector)
124+
# We have singletons here, so never cache them twice, since otherwise
125+
# the cleanup method might be invoked twice.
126+
self.cachedProviders = set()
127+
128+
def get(self, key: Type[T], provider: Provider[T]) -> Provider[T]:
129+
obj = super().get(key, provider)
130+
self.cachedProviders.add(obj)
131+
return obj
132+
133+
cleaned = ScopeDecorator(CleanupScope)
134+
135+
Next we will also create a custom :class:`Injector`, which will do the cleanup of all
136+
our objects belonging to :class:`CleanupScope` after a call to :meth:`get`::
137+
138+
ScopeType = Union[ScopeDecorator, Type[Scope], None]
139+
140+
class CleanupInjector:
141+
def __init__(self, injector: Injector) -> None:
142+
self.injector = injector
143+
144+
@contextmanager
145+
def get(self, interface: Type[T], scope: ScopeType = None) -> Generator[T, None, None]:
146+
yield self.injector.get(interface, scope)
147+
self.cleanup()
148+
149+
def cleanup(self):
150+
print("CleanupInjector: Invoking 'cleanup' for all who need it.")
151+
cleanupScope = self.injector.get(CleanupScope)
152+
for provider in cleanupScope.cachedProviders:
153+
obj = provider.get(self.injector)
154+
if hasattr(obj, 'cleanup') and callable(obj.cleanup):
155+
obj.cleanup()
156+
157+
Now we can simply use our custom injector and freeing resources will be done for
158+
each object in :class:`CleanupScope` automatically::
159+
160+
injector = CleanupInjector(Injector())
161+
with injector.get(NeedsCleanup) as obj:
162+
obj.doSomething()
163+
164+
This is of course a simple example. In a real world example `NeedsCleanup` could
165+
be nested deep and multiple times anywhere in a dependency structure. This
166+
pattern would work irrespectively of where `NeedsCleanup` would be injected.

0 commit comments

Comments
 (0)