A small dependency injection Python library
Project description
Tidi
A small dependency injection Python library.
Inspired by Kent Tong's
disl and
FastAPI's Depends
.
Motivation
I found myself wanting to learn more about how dependency injection can be done in a pythonic way, with type-hinting, so had the itch to develop (yet another) library for it and share it as open source 🧑💻✌️
Read more about the motivation in this Medium article.
Primary API
The top level import of tidi
provides everything needed it's primary intended
use.
@tidi.inject
- a decorator that will replace certain keyword arguments with dependencies, based on their type & if they haven't been passed intidi.Injected[DependencyClass]
- a type alias, wrapping typing.Annotated, that indicates that a keyword argument should be injectedtidi.register(dependency_instance)
- a function that registers an object to be available for injection as a dependencytidi.Provider(get_dependency_function)
- a wrapper class around a function that will be called to provide a dependencytidi.UNSET
- a sentinel object to indicate that a dependency should be loaded from the registrytidi.field_factory(DependencyClass)
- a helper function for injecting dependencies into dataclass fields
Example of use
Consider a micro-sized interactive CLI that lets a user choose a handbag then search through it,
# search-handbag.py
import tidi
from handbags import Handbag, HandbagItem, load_handbag
@tidi.inject
def dig_through_handbag(
item_type: str,
handbag: tidi.Injected[Handbag] = tidi.UNSET,
) -> HandbagItem | None:
return handbag.get_items_by_type(item_type).first(default=None)
def init_handbag():
selected_handbag = input("Select a handbag: ")
tidi.register(load_handbag(selected_handbag))
def run_search():
item_type = input("What are you looking for? ")
# ⬇️ registered `Handbag` instance gets injected ✨
item = dig_through_handbag(item_type)
if item is None:
print("Uh oh, can't find it 🤷♀️, try again")
run_search()
else:
print(f"We're in luck! Here's your {item.name} 😎")
if __name__ == "__main__":
init_handbag()
run_search()
Running it looks something like this,
$ python search-handbag
Select a handbag: BCBGMAXAZRIA
What are you looking for? Nail file
Uh oh, can't find it 🤷♀️, try again
What are you looking for? Lip balm
We're in luck! Here's your Blistex 😎
We can see Dependency Injection happening hear to achieve Inversion of Control and obey the Law of Demeter.
dig_through_handbag
isn't responsible for creating aHandbag
and doesn't require its caller to know about it, rather aHandbag
is injected ✨init_handbag
creates theHandbag
, but doesn't need to return it. An example of separating the app initialisation from the main logic.run_search
doesn't need to know about anything that it doesn't use (in this case theHandbag
), obeying the Law of Demeter.
When testing,
- a mock
Handbag
could be passed in as a keyword argument to testdig_through_handbag
, and - patching
dig_through_handbag
with a stub could be done to testrun_search
with no requirement for a mockHandbag
.
More examples
You can find some executable examples in the demo/ directory of the repo.
Also see the Usage documentation for more examples.
Interested in contributing?
Feel free to create an issue or author a PR 😊
For the latter, check out the CONTRIBUTING guide for a quick start on development.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.