Package: enterprython
enterprythonPython library providing type-based dependency injectionTable of contentsIntroductionFeaturesAbstract base classes and profilesFactoriesNon-singleton servicesService listsMixing managed and manual injectionFree functions as clientsValue storeValue injectionRequirements and InstallationIntroductionIf you plan to developSOLID/domain-driven(i.e., enterprisey) software, you probablywantto applyinversion of controlin the form ofdependency injectionwhen writing the constructors of your classes.
Also, you likely want to use a library doing the needed lookups for you based on static type annotations, instead of manually configuring the object graph.enterprythonprovides exactly that.fromenterprythonimportassemble,component@component()classService:def__init__(self)->None:self._greeting:str='Hello'defgreet(self,name:str)->str:returnf'{self._greeting},{name}!'classClient:def__init__(self,service:Service)->None:self._service=servicedefrun(self)->None:print(self._service.greet('World'))assemble(Client).run()Output:Hello, World!FeaturesAbstract base classes and profilesA client may depend on an abstract base class. Enterprython will inject the matching implementation.fromabcimportABCfromenterprythonimportassemble,componentclassServiceInterface(ABC):...@component()classServiceImpl(ServiceInterface):...classClient:def__init__(self,services:ServiceInterface)->None:...assemble(Client)One singleton instance ofServiceImplis created and injected intoClient.This feature enables the use of different profiles.
For example, you might want to use different classes implementing an interface
for your production environment compared to when running integration tests.
By providing aprofileslist, you can limit when the component is available.@component(profiles=['prod'])classServiceImpl(ServiceInterface):...@component(profiles=['test'])classServiceMock(ServiceInterface):...assemble(Client,profile='test')FactoriesAnnotating a function with@factory()registers a factory for its return type.fromenterprythonimportassemble,componentclassService:...@factory()defservice_factory()->Service:returnService()classClient:def__init__(self,service:Service)->None:...assemble(Client)service_factoryis used to create theServiceinstance for calling the constructor ofClient.Non-singleton servicesIf a service is annotated with@component(singleton=False)a new instance of it is created with every injection.@component(singleton=False)classService:...classClient:def__init__(self,service:Service)->None:...Service listsA client may depend on a list of implementations of a service interface.fromabcimportABCfromtypingimportListfromenterprythonimportassemble,componentclassServiceInterface(ABC):pass@component()classServiceA(ServiceInterface):...@component()classServiceB(ServiceInterface):...classClient:def__init__(self,services:List[ServiceInterface])->None:...assemble(Client)[ServiceA(), ServiceB()]is injected intoClient.Mixing managed and manual injectionOne part of a client's dependencies might be injected manually, the rest automatically.fromenterprythonimportassemble,component@component()classServiceA:...classServiceB:...classClient:def__init__(self,service_a:ServiceA,service_b:ServiceB)->None:...assemble(Client,service_b=ServiceB())service_acomes from the DI container,service_bfrom user code.IfServiceBalso has a@component()annotation, the manually provided object is preferred.Free functions as clientsSince class constructors are fundamentally just normal functions, we can inject dependencies into free functions too.fromenterprythonimportassemble,component@component()classService:...defclient(service:Service)->None:...assemble(client)A singleton instance ofServiceis created and used to callclient.Value StoreThe value store supports merging multiple sources using the following precedence order:Configuration files using the list provided order.tomlformat is the only supported for now.Environment variables. Variables must be prefixed with the application nameCommand line arguments. Arguments must follow the format: --key=value.Command-line arguments overwrite environment variables and environment variables overwrite configuration files.To load the value store use the helper functionload_configas below.load_config(app_name="myapp",paths=["config.toml"])app_nameis the application name and is required to identify environment variables.pathsis a list of relative file paths, files will be loaded and merged in the same order.Value InjectionService's value-type attributes are automatically injected from the value store using either the traversal path or a given value store key.Pythondataclassandattrsare supported.@attrs.defineor@dc.dataclassdecorators are provided after the@componentdecoratorNotice thatattrsanddcmodule alias is being used to highlight what library is used.Feel free to use the more compact@defineand@dataclassversions in your production code.importattrs@component()@attrs.defineclassService:attrib1:intattrib2:strattrib3:bool...classClient:service:Service...load_config("myapp",["config.toml"])assemble(Client)config.toml:service_attrib1=10service_attrib2="mystring"service_attrib3=falseattrib1, attrib2, and attrib3 will be injected using the configuration entries listed above.By default, enterprython will use the attribute path convention (notice theservice_prefix in each of the configuration entries )If multiple services need to read the same configuration entry, thesettingdecorator let you provide your custom key:@component()@attrs.defineclassService:attrib1:int=setting("MYATTRIB1")attrib2:str=setting("MYATTRIB2")attrib3:bool=setting("MYATTRIB3")...classClient:service:Service...load_config("myapp",["config.toml"])assemble(Client)config.toml:MYATTRIB1=10MYATTRIB2="mystring"MYATTRIB3=falseThe value injection provides type-checking and enforces injection of any attribute without defaults.To skip injecting an attribute, you can:Use the attribute default value.Use the attrs/dataclassfielddecorator providinginit=Falseanddefault=...to opt-out from injection.
Using this, the attribute will not get injected (even if a matching entry exists in the value store)@component()@attrs.defineclassService:# below attribute WILL be injected from the value store# an entry MUST exist in the value storeattrib1:int# below attribute CAN be injected from the value store,# if not provided in value store, then the default is usedattrib2:str="test"# below attribute will not be injected# any entry in the value store will be ignoredattrib3:bool=attrs.field(init=False,default=True)Requirements and InstallationYou need Python 3.7 or higher.python3-mpipinstallenterprythonOr, if you like to use the latest version from this repository:gitclonehttps://github.com/Dobiasd/enterprythoncdenterprython
python3-mpipinstall.LicenseDistributed under the MIT License.
(See accompanying fileLICENSEor athttps://opensource.org/licenses/MIT)