Skip to content

Setup and teardown

Why

It is a common sittuation that you would need a little bit of logic before building your objects and a little bit of logic after you use built objects. Let say, connection pool needs to be initialized before you could properly use your service objects. The same connection pool should be shutdown correctly after your service object done with its part. Of course, connection pool is a noticeable example of a general resource management problem.

Hidden complexity

In complex frameworks like object relation mappers usually all complexity of resource management is hidden by development API. Often you would use let say a database model and didn't even notice that connection to the database was made, or database transaction was started.

>>> from app.models import Report

>>> Report.objects.select()
[<Report: 1>, <Report: 2>, <Report: 3>]

This is a convenient approach to access data source you are interested in. And developers how designed framework API in such way done a greet job to simplify lives of their software users.

And what we would do in case there is no such library for a data source we would like to communicate with? What we gonna do in that case? Should we spend endless time crafting such library for ourselves?

For example, we need to integrate our application with a third-party service over ther JSON API. And developers of this third-party service didn't bother to write a proper client library for python. Should we pollute our business objects with knowledge of HTTP keep-alive management? It should be a better place for such low-level logic.

Principles

@value object could yield value

When you use Injector subclasses as context managers it allows you to write setup and teardown logic inside evaluated dependencies. @value object could be a generator function. Setup code would be executed before yield statement. An object returned by yield statement would be injected as an argument matching generator function name. When the code would leave with statement block the rest of the generator would be executed to the end of function. You could put a tear down logic in this place.

>>> from dependencies import Injector, value
>>> from requests import Session
>>> from dataclasses import dataclass

>>> @dataclass
... class Account:
...     http: Session
...
...     def suspend(self, user_id):
...         user = self.http.get(f"http://api.com/users/{user_id}/")
...         for group_id in user.json()["groups"]:
...             self.http.delete(f"http://api.com/groups/{group_id}/members/{user_id}/")

>>> class App(Injector):
...     account = Account
...
...     @value
...     def http():
...         with Session() as session:
...             yield session

>>> with App as app:
...     app.account.suspend(142)
...     app.account.suspend(318)

Note

A wise reader would notice that sticky scopes rules are applied. In both calls account would share the same Account instance which share the same Session instance which was initiated only once. That's mean both business object calls would use same keep-alive TCP connection.

It's a similar approach that pytest project takes with it's fixtures.

Teardown happens in opposite order to setup

As we mention previously, setup and teardown logic usually used to do side effects. The order in which you do side effect is not the same in which you undo side effects. A database transaction can't be committed or rollback after database connection was destroyed.

For that purpose dependencies would execute teardown steps in exactly opposite order to the setup steps. You could rely of this assumption during design of your value objects internals. It's a contract guarantied by our API.

>>> from app.database import Cursor, Connection

>>> @dataclass
... class Account:
...     cursor: Cursor
...
...     def suspend(self, nick):
...         self.cursor.select(name=nick).delete()


>>> class App(Injector):
...     account = Account
...
...     @value
...     def cursor(connection):
...         cur = Cursor(connection)
...         cur.begin_transaction()
...         yield cur
...         cur.commit_transaction()
...
...     @value
...     def connection():
...         db = Connection()
...         db.connect()
...         yield db
...         db.disconnect()

>>> with App as app:
...     app.account.suspend('Jeff')
CONNECT TO production;
BEGIN TRANSACTION;
SELECT * FROM users FOR UPDATE;
DELETE FROM users;
COMMIT TRANSACTION;
DISCONNECT FROM production;

— ⭐ —