Package: serum
Build StatusBuild status:Code quality:Descriptionserumis a fresh take on Dependency Injection in Python 3.serumis pure python and has no dependencies.Installation> pip install serumQuickstartfromserumimportinject,dependency,Context# Classes decorated with 'dependency' are injectable types.@dependencyclassLog:definfo(self,message:str):raiseNotImplementedError()classSimpleLog(Log):definfo(self,message:str):print(message)classStubLog(SimpleLog):definfo(self,message:str):pass@inject# Dependencies are injected using a class decorator...classNeedsLog:log:Log# ...and class level annotations...classNeedsSimpleLog:@inject# ...or using a function decoratordef__init__(self,log:SimpleLog):self.log=log@injectclassNeedsNamedDependency:named_dependency:str# class level annotations annotated with a type that is not# decorated with 'dependency' will be treated as a named# dependency# Contexts provide dependencieswithContext(SimpleLog,named_dependency='this name is injected!'):assertisinstance(NeedsLog().log,SimpleLog)assertNeedsNamedDependency().named_dependency=='this name is injected!'# Contexts will always provide the most specific# subtype of the requested type. This allows you to change which# dependencies are injected.withContext(StubLog):NeedsLog().log.info('Hello serum!')# doesn't output anythingNeedsSimpleLog().log.info('Hello serum!')# doesn't output anythingDocumentationinjectdependencyContextsingletonmockmatchIPython Integrationinjectinjectis used to decorate functions and classes in which you want to inject
dependencies.fromserumimportinject,dependency@dependencyclassMyDependency:pass@injectdeff(dependency:MyDependency):assertisinstance(dependency,MyDependency)f()Functions decorated withinjectcan be called as normal functions.serumwill
not attempt to inject arguments given at call time.@injectdeff(dependency:MyDependency):print(dependency)f('Overridden dependency')#  outputs: Overridden dependencyinjectwill instantiate classes decorated withdependency. In
this way, your entire dependency graph can be specified using justinjectanddependency.Instances of simple types and objects you want to instantiate yourself can be
injected using keyword arguments toContext.@injectdeff(dependency:str):assertdependency=='a named dependency'withContext(dependency='a named dependency'):f()injectcan also be used to decorate classes.@injectclassSomeClass:dependency:MyDependencyThis is roughly equivalent to:classSomeClass:@injectdef__init__(self,dependency:MyDependency):self.__dependency=dependency@propertydefdependency(self)->MyDependency:returnself.__dependencyDependencies that are specified as class level annotations can be overridden
using key-word arguments to__init__assertSomeClass(dependency='Overridden!').dependency=='Overridden!'dependencyClasses decorated withdependencycan be instantiated and injected
byserum.fromserumimportdependency,inject@dependencyclassLog:definfo(self,message):print(message)@injectclassNeedsLog:log:Logassertisinstance(NeedsLog().log,Log)serumrelies on being able to inject all dependencies fordependencydecorated classes
recursively. To achieve this,serumassumes that the__init__method
ofdependencydecorated classes can be called without any arguments.
This means that all arguments to__init__ofdependencydecorated classes must be injected usinginject.@dependencyclassSomeDependency:defmethod(self):pass@inject@dependencyclassValidDependency:# OK!some_dependency:SomeDependencydef__init__(self):...@dependencyclassAlsoValidDependency:# Also OK!@injectdef__init__(self,some_dependency:SomeDependency):...@dependencyclassInvalidDependency:def__init__(self,a):...@injectdeff(dependency:InvalidDependency):...f()# raises:# TypeError: __init__() missing 1 required positional argument: 'a'# The above exception was the direct cause of the following exception:# InjectionError                            Traceback (most recent call last)# ...# InjectionError: Could not instantiate dependency <class 'InvalidDependency'># when injecting argument "dependency" in <function f at 0x10a074ea0>.Note that circular dependencies preventing instantiation ofdependencydecorated
classes leads to an error.@dependencyclassAbstractA:pass@dependencyclassAbstractB:passclassA(AbstractA):@injectdef__init__(self,b:AbstractB):self.b=bclassB(AbstractB):@injectdef__init__(self,a:AbstractA):self.a=a@injectclassDependent:a:AbstractAwithContext(A,B):Dependent().a# raises: CircularDependency: Circular dependency encountered while injecting <class 'AbstractA'> in <B object at 0x1061e3898>ContextContexts provide implementations of dependencies. AContextwill always provide the most
specific subtype of the requested type (in Method Resolution Order).@dependencyclassSuper:passclassSub(Super):pass@injectclassNeedsSuper:instance:SuperwithContext(Sub):assertisinstance(NeedsSuper().instance,Sub)It is an error to inject a type in anContextthat provides two or more equally specific subtypes of that type:classAlsoSub(Super):passwithContext(Sub,AlsoSub):NeedsSuper()# raises: AmbiguousDependencies: Attempt to inject type <class 'Log'> with equally specific provided subtypes: <class 'MockLog'>, <class 'FileLog'>Contexts can also be used as decorators:context=Context(Sub)@contextdeff():assertisinstance(NeedsSuper().instance,Sub)You can provide named dependencies of any type using keyword arguments.@injectclassDatabase:connection_string:strconnection_string='mysql+pymysql://root:my_pass@127.0.0.1:3333/my_db'context=Context(connection_string=connection_string)withcontext:assertDatabase().connection_string==connection_stringContexts are local to each thread. This means that when using multi-threading
each thread runs in its own contextimportthreading@singletonclassSomeSingleton:passdefworker_without_environment():NeedsSuper().instance@injectdefworker(instance:SomeSingleton):print(instance)withContext():worker()# outputs: <SomeSingleton object at 0x101f75470>threading.Thread(target=worker).start()# outputs: <SomeSingleton object at 0x1035fb320>singletonTo always inject the same instance of a dependency in the sameContext, annotate your type withsingleton.fromserumimportsingleton@singletonclassExpensiveObject:pass@injectclassNeedsExpensiveObject:expensive_instance:ExpensiveObjectinstance1=NeedsExpensiveObject()instance2=NeedsExpensiveObject()assertinstance1.expensive_instanceisinstance2.expensive_instanceNote thatSingletondependencies injected in different environments
will not refer to the same instance.withContext():instance1=NeedsExpensiveObject()withContext():assertinstance1.expensive_instanceisnotNeedsExpensiveObject().expensive_instancemockserumhas support for injectingMagicMocks from the builtinunittest.mocklibrary in unittests using themockutility
function. Mocks are reset
when the environment context is closed.fromserumimportmock@dependencyclassSomeDependency:defmethod(self):return'some value'@injectclassDependent:dependency:SomeDependencycontext=Context()withcontext:mock_dependency=mock(SomeDependency)mock_dependency.method.return_value='some mocked value'instance=Dependent()assertinstance.dependencyismock_dependencyassertinstance.dependency.method()=='some mocked value'withcontext:instance=Dependent()assertinstance.dependencyisnotmock_dependencyassertisinstance(instance.dependency,SomeDependency)mockuses its argument to spec the injected instance ofMagicMock. This means
that attempting to call methods that are not defined by the mockedComponentleads to an errorwithcontext:mock_dependency=mock(SomeDependency)mock_dependency.no_method()# raises: AttributeError: Mock object has no attribute 'no method'Note thatmockwill only mock requests of the
exact type supplied as its argument, but not requests of
more or less specific typesfromunittest.mockimportMagicMock@dependencyclassSuper:passclassSub(Super):passclassSubSub(Sub):pass@injectclassNeedsSuper:injected:Super@injectclassNeedsSub:injected:Sub@injectclassNeedsSubSub:injected:SubSubwithContext():mock(Sub)needs_super=NeedsSuper()needs_sub=NeedsSub()needs_subsub=NeedsSubSub()assertisinstance(needs_super.injected,Super)assertisinstance(needs_sub.injected,MagicMock)assertisinstance(needs_subsub.injected,SubSub)matchmatchis small utility function for matchingContextinstances
with values of an environment variable.# my_script.pyfromserumimportmatch,dependency,Context,inject@dependencyclassBaseDependency:defmethod(self):raiseNotImplementedError()classProductionDependency(BaseDependency):defmethod(self):print('Production!')classTestDependency(BaseDependency):defmethod(self):print('Test!')@injectdeff(dependency:BaseDependency):dependency.method()context=match(environment_variable='MY_SCRIPT_ENV',default=Context(ProductionDependency),PROD=Context(ProductionDependency),TEST=Context(TestDependency))withcontext:f()> python my_script.py
Production!> MY_SCRIPT_ENV=PROD python my_script.py
Production!> MY_SCRIPT_ENV=TEST python my_script.py
Test!IPython IntegrationIt can be slightly annoying to import someContextand start it as a
context manager in the beginning of every IPython session.
Moreover, you quite often want to run an IPython REPL in a special context,
e.g to provide configuration that is normally supplied through command line
arguments in some other way.To this endserumcan act as an IPython extension. To activate it,
add the following lines to youripython_config.py:c.InteractiveShellApp.extensions=['serum']Finally, create a file namedipython_context.pyin the root of your project. In it,
assign theContextinstance you would like automatically started to a global
variable namedcontext:# ipython_context.pyfromserumimportContextcontext=Context()IPython will now enter this context automatically in the beginning of
every REPL session started in the root of your project.Why?If you've been researching Dependency Injection frameworks for python,
you've no doubt come across this opinion:You dont need Dependency Injection in python.
You can just use duck typing and monkey patching!The position behind this statement is often that you only need Dependency
Injection in statically typed languages.In truth, you don't reallyneedDependency Injection in any language,
statically typed or otherwise.
When building large applications that need to run in multiple environments however,
Dependency Injection can make your life a lot easier. In my experience,
excessive use of monkey patching for managing environments leads to a jumbled
mess of implicit initialisation steps andif value is Nonetype code.In addition to being a framework, I've attempted to designserumto encourage
designing classes that follow the Dependency Inversion Principle:one should “depend upon abstractions,notconcretions."This is achieved by letting inheritance being the principle way of providing
dependencies and allowing dependencies to be abstract.