Package: junkie
JunkieJunkie is a Dependency Injection library for beginners and professionals.Installation:pip install junkieExample:fromjunkieimportJunkieclassApp:def__init__(self,addressee):self.addressee=addresseedefgreets(self):returnf"Hello{self.addressee}!"context={"addressee":"World"}withJunkie(context).inject(App)asapp:assertapp.greets()=="Hello World!"What is Dependency Injection, and why should we use it?Dependency Injection is a design pattern in which all dependent objects are created separately and handed over from
outside into the actual object. An object B depends on A if A calls a method of B.
Don't worry - it sounds more complicated than it really is.Example:In traditional source code, object A creates B in the constructor or a method. That means it is hard to reuse B in other
objects because the reference of B is only known by A. When using Dependency Injection, an independent software
component creates B separately and hands it over to all objects which need it. This amazing software component isJunkie!Finally, Dependency Injection helps you to implement highly decoupled and testable code.How does Junkie work?fromjunkieimportJunkieBefore using Junkie you need to prepare the so-calledcontext. This context is a Python dictionary, describing how
objects get created or which pre-defined values to use. Every dictionary key represents an argument name.
The corresponding value defines the constructor or function which assembles the requested object. A dictionary value can
also provide a primitive value or a non-callable object.Junkie also takes Python type hints into account. They are used if no mapping in the context for the argument name
exists.Additionally, Python lambdas can be used to adjust object construction.fromhttp.serverimportHTTPServer,SimpleHTTPRequestHandlercontext={"http_server":HTTPServer,# constructor"server_address":("0.0.0.0",8080),# pre-defined value"RequestHandlerClass":lambda:SimpleHTTPRequestHandler,# pre-defined callable via lambda (special case)}Now, Junkie can create new objects and their dependencies. All dependencies are resolved via their argument name in the
constructor. Only one object is created per argument name and is shared with all other objects.withJunkie(context).inject(HTTPServer)ashttp_server:# type: HTTPServerhttp_server.serve_forever()Python context managers provide methods to prepare and finalize an object. All context managers are also handled in this
way by Junkie.Best practicesUse type hints for object constructionJunkie uses constructor-based dependency injection. The constructor gets all references to dependent objects, and saves
them for later usage. The constructor should not do any work.The argument names and their type hints are the easiest and recommended way to define object construction of
dependencies. The context dictionary should be used to handle more complicated situations.fromjunkieimportJunkieclassDatabase:passclassApp:def__init__(self,database:Database):self.database=databasewithJunkie().inject(App)asapp:assertisinstance(app.database,Database)Write integration tests with modified application contextAfter defining the application context it is very easy to replace individual objects with test doubles for integration
tests.importunittestfromjunkieimportJunkieAPPLICATION_CONTEXT={"database_url":"postgresql://scott:tiger@localhost:5432/production",}classApp:def__init__(self,database_url):self.database_url=database_urldefmain():withJunkie(APPLICATION_CONTEXT).inject(App)asapp:assertapp.database_url.startswith("postgresql:")classAppTest(unittest.TestCase):deftest(self):test_context=APPLICATION_CONTEXT|{"database_url":"sqlite://"}withJunkie(test_context).inject(App)asapp:self.assertEqual(app.database_url,"sqlite://")Advanced usageAdjust object construction via lambdasThe following example code shows various ways to adjust object construction via Python lambdas.fromjunkieimportJunkieclassApp:def__init__(self,greeting:str):self.greeting=greetingcontext={# app1"app1":lambda:App("Hello Joe!"),# app2"greeting2":"Hello John!","app2":lambdagreeting2:App(greeting2),# app3"greeting3":lambda:"Hello Doe!","app3":lambdagreeting3:App(greeting3),}withJunkie(context).inject("app1","app2","app3")as(app1,app2,app3):assertapp1.greeting=="Hello Joe!"assertapp2.greeting=="Hello John!"assertapp3.greeting=="Hello Doe!"The_junkieargument nameIf you need Junkie in one of your classes or functions, you can use the argument name_junkie. This argument name is
reserved for the Junkie instance itself.fromcontextlibimportcontextmanagerfromjunkieimportJunkieclassSqlDatabase:passclassFileDatabase:passclassApp:def__init__(self,database):self.database=database@contextmanagerdefprovide_database(_junkie,url:str):ifurl.startswith("file:"):with_junkie.inject(FileDatabase)asdatabase:yielddatabaseelse:with_junkie.inject(SqlDatabase)asdatabase:yielddatabasecontext={"url":"file://local.db","database":provide_database,}withJunkie(context).inject(App)asapp:assertisinstance(app.database,FileDatabase)Instantiate list itemsSometimes you need a list of objects. This list can be instantiated with theinject_list()helper function. It works
similar to theJunkie.inject()method.fromjunkieimportJunkieclassCustomerDataSource:def__init__(self,connection_string:str):passclassProductDataSource:passclassSupplierDataSource:passclassApp:def__init__(self,data_sources):self.data_sources=data_sourcescontext={"customer_ds":lambda:CustomerDataSource("sqlite://"),"data_sources":["customer_ds",ProductDataSource,SupplierDataSource],}withJunkie().inject(App)asapp:assertisinstance(app.data_sources[0],CustomerDataSource)assertisinstance(app.data_sources[1],ProductDataSource)assertisinstance(app.data_sources[2],SupplierDataSource)Callables as pre-defined context valuesAll requested context values are evaluated if they are callables. If you want to provide a callable object, wrap
it via lambda expression.fromjunkieimportJunkieclassDatabase:def__call__(self,*args,**kwargs):return"called"classApp:def__init__(self,database):self.database=databasecontext={"database":lambda:Database(),}withJunkie(context).inject(App)asapp:assertapp.database=="called"Built-in functions as context values are not supportedUnfortunately, built-in functions (implemented in C) likesqlite3.connect()can not be inspected. That's why they are
not supported by Junkie as context values. Python lambdas help to work around this issue.importsqlite3fromjunkieimportJunkiecontext={"database":":memory:","connection":sqlite3.connect,"working_connection":lambdadatabase:sqlite3.connect(database)}# ValueError: no signature found for builtin <built-in function connect>withJunkie(context).inject("connection")asconnection:passwithJunkie(context).inject("working_connection")asworking_connection:passCollaborationGet InvolvedYou are warmly welcome to contribute to Junkie. Just initiate a pull request or report an issue.AuthorsJunkie was written byStefan Richter. Special thanks go
toErik Türkefor his valuable feedback and many helpful code snippets.DistributionCode:https://github.com/sealor/junkie/PyPI:https://pypi.org/project/junkie/LicenseMIT LicenseSeeLICENSEfor full text.