Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Vladimir Iakolev: Abusing annotations with dependency injection

$
0
0

Python 3 has a nice feature – type annotations:

defadd(x:int,y:int)->int:returnx+y

That can be used by IDEs and stuff like mypy for type checking. However we can easily access it:

>>>add.__annotations__{'return':int,'x':int,'y':int}

And use it for things like dependency injection. For example we have a web app:

defget_db_connection()->abc.DBConnection:...defget_routes()->abc.Router:...defget_cache(db:abc.DBConnection)->abc.CacheManager:...definit_app():db=get_db_connection()routes=get_routes()cache=get_cache(db)app=Application(routes=routes,db=db,cache=cache)app.run()if__name__=='__main__':init_app()

Looks a bit Java-like with interfaces (abstract classes, abc), but it’s useful in huge apps. However components are tightly coupled, and we need to use monkey patching for testing it.

Let’s examine annotations:

>>>get_cache.__annotations__{'db':abc.DBConnection,'return':abc.CacheManager}

We can see that the function requires abc.DBConnection and provides abc.CacheManager. We need to track all functions like this, it’ll be easy with some decorator:

fromweakrefimportWeakValueDictionary_provides=WeakValueDictionary()defprovides(fn):"""Register function that provides something."""try:_provides[fn.__annotations__['return']]=fnexceptKeyError:raiseValueError('Function not annotated.')returnfn

We use WeakValueDictionary in case function somehow can be deleted.

Let’s apply this decorator:

@providesdefget_db_connection()->abc.DBConnection:...@providesdefget_routes()->abc.Router:...@providesdefget_cache(*,db:abc.DBConnection)->abc.CacheManager:...

And move dependencies of main function to arguments:

definit_app(*,routes:abc.Router,db:abc.DBConnection,cache:abc.CacheManager):app=Application(routes=routes,db=db,cache=cache)app.run()

So we can think about our functions as a graph:

graph TB A[init_app]---B[get_routes] A---C[get_db_connection] A---D[get_cache] D---C

And we can easily write injector that resolve and inject dependencies:

classInjector:"""Resolve and inject dependencies."""def__init__(self):self._resolved={}def_get_value(self,name):"""Get dependency by name (type)."""ifnamenotin_provides:raiseValueError("Dependency {} not registered.".format(name))ifnamenotinself._resolved:fn=_provides[name]kwargs=self._get_dependencies(fn)returnfn(**kwargs)returnself._resolved[name]def_get_dependencies(self,fn):"""Get dependencies for function."""return{key:self._get_value(value)forkey,valueinfn.__annotations__.items()ifkey!='return'}defrun(self,fn):"""Resolve dependencies and run function."""kwargs=self._get_dependencies(fn)returnfn(**kwargs)

So we can make our app work by adding:

if__name__=='__main__':Injector().run(init_app)

Although this approach is simple and straightforward, it’s overkill for most of apps.

Package on github.


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>