Package: aiodine
aiodineaiodine provides async-firstdependency injectionin the style ofPytest fixturesfor Python 3.6+.InstallationConceptsUsageFAQChangelogInstallationpipinstall"aiodine==1.*"Conceptsaiodine revolves around two concepts:Providersare in charge of setting up, returning and optionally cleaning upresources.Consumerscan access these resources by declaring the provider as one of their parameters.This approach is an implementation ofDependency Injectionand makes providers and consumers:Explicit: referencing providers by name on the consumer's signature makes dependencies clear and predictable.Modular: a provider can itself consume other providers, allowing to build ecosystems of reusable (and replaceable) dependencies.Flexible: provided values are reused within a given scope, and providers and consumers support a variety of syntaxes (asynchronous/synchronous, function/generator) to make provisioning fun again.aiodine isasync-firstin the sense that:It was made to work with coroutine functions and the async/await syntax.Consumers can only be called in an asynchronous setting.But provider and consumer functions can be regular Python functions and generators too, if only for convenience.UsageProvidersProvidersmake aresourceavailable to consumers within a certainscope. They are created by decorating aprovider functionwith@aiodine.provider.Here's a "hello world" provider:importaiodine@aiodine.providerasyncdefhello():return"Hello, aiodine!"Providers are available in twoscopes:function: the provider's value is re-computed everytime it is consumed.session: the provider's value is computed only once (the first time it is consumed) and is reused in subsequent calls.By default, providers are function-scoped.ConsumersOnce a provider has been declared, it can be used byconsumers. A consumer is built by decorating aconsumer functionwith@aiodine.consumer. A consumer can declare a provider as one of its parameters and aiodine will inject it at runtime.Here's an example consumer:@aiodine.consumerasyncdefshow_friendly_message(hello):print(hello)All aiodine consumers are asynchronous, so you'll need to run them in an asynchronous context:fromasyncioimportrunasyncdefmain():awaitshow_friendly_message()run(main())# "Hello, aiodine!"Of course, a consumer can declare non-provider parameters too. aiodine is smart enough to figure out which parameters should be injected via providers, and which should be expected from the callee.@aiodine.consumerasyncdefshow_friendly_message(hello,repeat=1):for_inrange(repeat):print(hello)asyncdefmain():awaitshow_friendly_message(repeat=10)Providers consuming other providersProviders are modular in the sense that they can themselves consume other providers.For this to work however, providers need to befrozenfirst. This ensures that the dependency graph is correctly resolved regardless of the declaration order.importaiodine@aiodine.providerdefemail():return"user@example.net"@aiodine.providerasyncdefsend_email(email):print(f"Sending email to{email}…")aiodine.freeze()# <- Ensures that `send_email` has resolved `email`.Note: it is safe to call.freeze()multiple times.A context manager syntax is also available:importaiodinewithaiodine.exit_freeze():@aiodine.providerdefemail():return"user@example.net"@aiodine.providerasyncdefsend_email(email):print(f"Sending email to{email}…")Generator providersGenerator providers can be used to perform cleanup (finalization) operations after a provider has gone out of scope.importosimportaiodine@aiodine.providerasyncdefcomplex_resource():print("setting up complex resource…")yield"complex"print("cleaning up complex resource…")Tip: cleanup code is executed even if an exception occurred in the consumer, so there's no need to surround theyieldstatement with atry/finallyblock.Important: session-scoped generator providers will only be cleaned up if using them in the context of a session. SeeSessionsfor details.Lazy async providersAsync providers areeagerby default: their return value is awaited before being injected into the consumer.You can mark a provider aslazyin order to defer awaiting the provided value to the consumer. This is useful when the provider needs to be conditionally evaluated.fromasyncioimportsleepimportaiodine@aiodine.provider(lazy=True)asyncdefexpensive_io_call():awaitsleep(10)return42@aiodine.consumerasyncdefcompute(expensive_io_call,cache=None):ifcache:returncachereturnawaitexpensive_io_callFactory providersInstead of returning a scalar value, factory providers return afunction. Factory providers are useful to implement reusable providers that accept a variety of inputs.This is adesign patternmore than anything else. In fact, there's no extra code in aiodine to support this feature.The following example defines a factory provider for a (simulated) database query:importaiodine@aiodine.provider(scope="session")asyncdefnotes():# Some hard-coded sticky notes.return[{"id":1,"text":"Groceries"},{"id":2,"text":"Make potatoe smash"},]@aiodine.providerasyncdefget_note(notes):asyncdef_get_note(pk:int)->list:try:# TODO: fetch from a database instead?returnnext(notefornoteinnotesifnote["id"]==pk)exceptStopIteration:raiseValueError(f"Note with ID{pk}does not exist.")return_get_noteExample usage in a consumer:@aiodine.consumerasyncdefshow_note(pk:int,get_note):print(awaitget_note(pk))Tip: you can combine factory providers withgenerator providersto cleanup any resources the factory needs to use. Here's an example that provides temporary files and removes them on cleanup:importosimportaiodine@aiodine.provider(scope="session")deftmpfile():files=set()asyncdef_create_tmpfile(path:str):withopen(path,"w")astmp:files.add(path)returntmpyield_create_tmpfileforpathinfiles:os.remove(path)Using providers without declaring them as parametersSometimes, a consumer needs to use a provider but doesn't care about the value it returns. In these situations, you can use the@useproviderdecorator and skip declaring it as a parameter.Tip: the@useproviderdecorator accepts a variable number of providers, which can be given by name or by reference.importosimportaiodine@aiodine.providerdefcache():os.makedirs("cache",exist_ok=True)@aiodine.providerdefdebug_log_file():withopen("debug.log","w"):passyieldos.remove("debug.log")@aiodine.consumer@aiodine.useprovider("cache",debug_log_file)asyncdefbuild_index():...Auto-used providersAuto-used providers areautomatically activated(within their configured scope) without having to declare them as a parameter in the consumer.This can typically spare you from decorating all your consumers with an@useprovider.For example, the auto-used provider below would result in printing the current date and time to the console every time a consumer is called.importdatetimeimportaiodine@aiodine.provider(autouse=True)asyncdeflogdatetime():print(datetime.now())SessionsAsessionis the context in whichsession providerslive.More specifically, session providers (resp. generator session providers) are instanciated (resp. setup) when entering a session, and destroyed (resp. cleaned up) when exiting the session.To enter a session, use:awaitaiodine.enter_session()To exit it:awaitaiodine.exit_session()An async context manager syntax is also available:asyncwithaiodine.session():...Context providersWARNING: this is an experimental feature.Context providers were introduced to solve the problem of injectingcontext-local resources. These resources are typically undefined at the time of provider declaration, but become well-defined when entering some kind ofcontext.This may sound abstract, so let's see an example before showing the usage of context providers.ExampleLet's say we're in a restaurant. There, a waiter executes orders submitted by customers. Each customer is given anOrderobject which they can.write()their desired menu items to.In aiodine terminilogy, the waiter is theproviderof the order, and the customer is aconsumer.During service, the waiter needs to listen to new customers, create a newOrderobject, provide it to the customer, execute the order as written by the customer, and destroy the executed order.So, in this example, thecontextspans from when an order is created to when it is destroyed, and is specific to a given customer.Here's what code simulating this situation on the waiter's side may look like:fromasyncioimportQueueimportaiodineclassOrder:defwrite(self,item:str):...classWaiter:def__init__(self):self._order=Noneself.queue=Queue()# Create an `order` provider for customers to use.# NOTE: the actually provided value is not defined yet!@aiodine.providerdeforder():returnself._orderasyncdef_execute(self,order:Order):...asyncdef_serve(self,customer):# NOTE: we've now entered the *context* of serving# a particular customer.# Create a new order that the customer can# via the `order` provider.self._order=Order()awaitcustomer()# Execute the order and destroy it.awaitself._execute(self._order)self._order=Noneasyncdefstart(self):whileTrue:customer=awaitself.queue.get()awaitself._serve(customer)It's important to note that customers can doanythingwith the order. In particular, they may take some time to think about what they are going to order. In the meantime, the server will be listening to other customer calls. In this sense, this situation is anasynchronousone.An example customer code may look like this:fromasyncioimportsleep@aiodine.consumerdefalice(order:Order):# Pondering while looking at the menu…awaitsleep(10)order.write("Pizza Margheritta")Let's reflect on this for a second. Have you noticed that the waiter holds onlyonereference to anOrder? This means that the code works fine as long as onlyonecustomer is served at a time.But what if another customer, saybob, comes along whilealiceis thinking about what she'll order? With the current implementation, the waiter will simplyforgetaboutalice's order, and end up executingbob's order twice. In short: we'll encounter arace condition.By using a context provider, we transparently turn the waiter'sorderinto acontext variable(a.k.a.ContextVar). It is local to the context of each customer, which solves the race condition.Here's how the code would then look like:importaiodineclassWaiter:def__init__(self):self.queue=Queue()self.provider=aiodine.create_context_provider("order")asyncdef_execute(self,order:Order):...asyncdef_serve(self,customer):order=Order()withself.provider.assign(order=order):awaitcustomer()awaitself._execute(order)asyncdefstart(self):whileTrue:customer=awaitself.queue.get()awaitself._serve(customer)Note:Customers can use theorderprovider just like before. In fact, it was created when calling.create_context_provider().Theorderis nowcontext-local, i.e. its value won't be forgotten or scrambled if other customers come and make orders concurrently.This situation may look trivial to some, but it is likely to be found in client/server architectures, including in web frameworks.UsageTo create a context provider, useaiodine.create_context_provider(). This method accepts a variable number of arguments and returns aContextProvider. Each argument is used as the name of a new@providerwhich provides the contents of aContextVarobject.importaiodineprovider=aiodine.create_context_provider("first_name","last_name")Each context variable containsNoneinitially. This means that consumers will receiveNone— unless they are called within the context of an.assign()block:withprovider.assign(first_name="alice"):# Consumers called in this block will receive `"alice"`# if they consume the `first_name` provider....FAQWhy "aiodine"?aiodine contains "aio" as inasyncio, and "di" as inDependency Injection. The last two letters end up making aiodine pronounce likeiodine, the chemical element.ChangelogSeeCHANGELOG.md.LicenseMIT