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:
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.