Getting Started#
Providers, Processors, and Stores#
in-n-out
is a dependency injection framework for Python. It allows
you to write functions using type annotations, and then inject
dependencies into those functions at call time. Two important concepts
in in-n-out
are providers and processors.
- Providers are functions that may be called with no arguments and return an instance of a type.
- Processors are functions that take an instance of a type and do something with it.
A Store
is a collection of providers and processors.
You will usually begin by creating a Store
instance to manage your providers and
processors. More than one provider/processor can be registered per type.
from in_n_out import Store
store = Store.create('my-store')
This store can be retrieved later using Store.get_store
, and
destroyed using Store.destroy
.
store = Store.get_store('my-store')
# Store.destroy('my-store') # would destroy the store... but we still want it :)
The global store
For convenience, any store methods accessed at the top level namespace will use a global store, unless a store name or instance is passed
Registering Providers#
Dependency inject works by providing an instance of a type to a function or method that requires it. Let's begin by declaring some type that will be important to our application:
class Thing:
"""Some thing I care about."""
def __init__(self, name: str):
self.name = name
Providers are registered using the Store.register_provider
. They are functions that may be called with no arguments
and return an instance of a type. in-n-out
will inspect the type annotations
of the function to determine what type it provides.
import in_n_out as ino
def heres_the_thing() -> Thing:
return Thing("Thing")
# register a provider of Thing
store.register_provider(heres_the_thing)
decorators
Registration functions may also be used as decorators:
@store.register_provider
def heres_the_thing() -> Thing:
return Thing("Thing")
Note
If you prefer not to use type annotations, or prefer to be explicit about
the type the provider provides, you may pass a type_hint
argument:
store.register_provider(heres_the_thing, Thing)
Injecting dependencies into functions#
Once you have registered a provider, you can use it to inject dependencies
into functions. Let's say we have a function that can use a Thing
:
def get_things_name(thing: Thing) -> str:
return thing.name
Naturally, this function will fail if we try to call it without providing
a Thing
:
get_things_name()
# TypeError: get_things_name() missing 1 required positional argument: 'thing'
We can use the Store.inject
method to inject a Thing
into the function:
get_things_name = store.inject(get_things_name)
print(get_things_name()) # prints "Thing"
multiple providers
In the case that more one provider has been registered for a given type. The store will
iterate through all providers registered for the required type (ordered by weight), stopping at the first one that returns an object that
is not None
.
Providers should return None
if it is not able to provide the requested object as
this allows in-n-out
to continue iterating through any other registered providers.
decorators
As with registration functions, we can use Store.inject
as a decorator:
@store.inject
def get_things_name(thing: Thing) -> str:
return thing.name
print(get_things_name()) # prints "Thing"
If the store is unable to find a provider for a required type, it will raise an exception:
@store.inject
def give_me_a_string(s: str) -> str:
return s
give_me_a_string()
# TypeError: Error calling in-n-out injected function '__main__.give_me_a_string' with kwargs {}.
# See TypeError above for more details.
function defaults
No dependency injection will be attempted for any parameters where a default value is given, e.g.,:
def get_things_name(thing: Thing = MyThing) -> str:
return thing.name
Weights and provider priority#
When registering multiple providers for the same type, you can use the
weight
parameter to specify the order in which providers should be tried.
(Providers with a higher weight will be tried first.)
def give_me_another_thing() -> Thing:
return Thing("Another Thing")
store.register_provider(give_me_another_thing, weight=10)
print(get_things_name()) # prints "Another Thing"
Temporary registration#
Registration functions may be used as context managers to temporarily register providers.
def most_important_thing() -> Thing:
return Thing("Most Important Thing")
with store.register_provider(most_important_thing, weight=20):
print(get_things_name()) # prints "Most Important Thing"
print(get_things_name()) # prints "Another Thing"
Undoing registration#
Alternatively, you can hang on to the object returned by the register_provider
function, and call its cleanup
method:
token = store.register_provider(most_important_thing, weight=20)
print(get_things_name()) # prints "Most Important Thing"
token.cleanup() # unregister
print(get_things_name()) # prints "Another Thing"
Processors#
Processors are functions that take an instance of a type and do something
with it, usually for the purpose of side effects. Processors are registered
using the Store.register_processor
method.
@store.inject_processors
def get_things_name(thing: Thing) -> str:
return thing.name
def greet_name(name: str):
print(f"Hello, {name}!")
store.register_processor(greet_name)
get_things_name(Thing('Bob')) # prints "Hello, Bob!" (and still returns "Bob")
Careful
Naturally, you want to be a bit careful with processors. It would be rather
unusual to register a processor for something as common as str
as we
did above. Or, at the very least, we wouldn't inject processors into a
function that returned str
.
Multiple processors#
As with providers, you may register multiple processors per return type, and the
weight
parameter will be used to specify the order in which they should be run,
with higher weights running first.
The default behavior is to call all processors that have been registered for a
given type. However, you can use first_processor_only=True
when
injecting processors with Store.inject_processors
or when manually calling Store.process
to specify that only the first processor should be used.
Note that all processors receive the same object (the return value of the function being injected into). They are not chained, and any value returned by a processor will be ignored.
If an unhandled exception is raised while executing a processor, it will be caught
and a warning will be emitted. If you would prefer to handle raised
exceptions yourself, you can pass raise_exception=True
to
Store.inject_processors
.
Real world example#
Let's look at a more realistic example.
Suppose we have an application like a text editor or IDE that allows plugins to
provide functionality like syntax highlighting, code completion, etc. We might
allow plugins to define functions that accept a Document
and use it's API to
provide additional functionality.
Rather than asking plugins to call some get_current_document()
function, we
can allow them to write functions that state their dependencies using type hints,
and then we (the application) determine how we will provide those dependencies.
def highlight_document(document: Document):
# do something with the document
pass
from in_n_out import Store
# create a store
store = Store.create('my-store')
# register a Document provider
@store.register_provider
def get_current_document() -> Document:
# get the current document from somewhere
...
# somehow gather functionality from plugins
from some_plugin import highlight_document
plugin_functions = [highlight_document]
# inject dependencies into plugin functions
injected = [store.inject(f) for f in plugin_functions]
Now we can call the injected functions, and they will have access to the current document.
app-model
#
In a GUI, one often needs to be able to call functions in response to user actions, such as selection of a menu item or a button click. However, those commands usually want some additional arguments or context. Dependency injection provides a nice way to handle this problem with loose coupling.
See app-model
for an
example of a library that uses in-n-out
to inject dependencies
into commands in the context of a GUI application.
More information#
See the API documentation for greater detail on the
Store
class and its methods.