diject
A powerful dependency injection framework that automatically injects objects,
promoting loose coupling, improving testability,
and centralizing configuration for easier maintenance.
What is dependency injection?
Dependency Injection (DI) is a design pattern that decouples the creation of
an object's dependencies from the object itself. Instead of hard-coding dependencies,
they are provided ("injected") from the outside.
Why Use Dependency Injection in Python?
Even though Python is a dynamically-typed language with flexible object construction, DI brings
significant benefits:
- Better Structure: By explicitly declaring dependencies, the code becomes self-documenting and
easier to understand.
- Simplified Testing: Dependencies can be replaced with mocks effortlessly, reducing the
overhead of setting up tests.
- Enhanced Maintainability: Changes to the implementation of a dependency do not ripple through
the codebase as long as the interface remains consistent.
How to use diject?
diject simplifies the process of configuring and setting object dependencies.
The framework allows you to:
- Define your classes with explicit dependencies in the constructor.
- Configure a container where you specify how each dependency should be provided (as singletons,
factories, etc.).
- Use decorators (e.g., @di.inject) to automatically inject dependencies into functions or class
methods.
By centralizing the configuration in a container, diject enables consistent dependency management
across your application.
Examples
Basic Example
A typical Python application without dependency injection might instantiate dependencies directly:
import os
class Database:
def __init__(self) -> None:
self.uri = os.getenv("DATABASE_URI")
class Service:
def __init__(self) -> None:
self.db = Database()
def main() -> None:
service = Service()
...
if __name__ == "__main__":
main()
Dependency Injection Pattern
In a DI pattern, dependencies are passed into objects rather than being created inside them:
import os
class Database:
def __init__(self, uri: str) -> None:
self.uri = uri
class Service:
def __init__(self, db: Database) -> None:
self.db = db
DATABASE = Database(
uri=os.getenv("DATABASE_URI"),
)
def create_service() -> Service:
return Service(
db=DATABASE,
)
def main(service: Service) -> None:
...
if __name__ == "__main__":
main(
service=create_service(),
)
Using diject
With diject, you can simplify dependency management further by declaring a container for your
configurations:
import os
import diject as di
class Database:
def __init__(self, uri: str) -> None:
self.uri = uri
class Service:
def __init__(self, db: Database) -> None:
self.db = db
class MainContainer(di.Container):
database = di.Singleton[Database](
uri=os.getenv("DATABASE_URI"),
)
service = di.Transient[Service](
db=database,
)
@di.inject
def main(service: Service = MainContainer.service) -> None:
...
if __name__ == "__main__":
main()
Key Concepts and Features
- Sync & Async Support: Seamlessly manage both synchronous and asynchronous dependencies.
- Pure Python Implementation: No need for external dependencies or language modifications.
- Performance: Low overhead with efficient dependency resolution.
- Clear Debugging: Built-in logging and debugging aids help trace dependency injection flow.
- Type Safety: Full MyPy and type annotation support ensures robust static analysis.
- Easy Testing: Simplified testing with native support for mocks and patches.
- Integration: Easily integrate with other frameworks and libraries.
- Inheritance and Protocols: Use Python's protocols to enforce contracts and ensure consistency
across implementations.
Installation
pip install diject
Providers
diject gives you fine-grained control over the lifecycle of your objects.
Consider the following example functions:
def some_function(arg: str) -> str:
return "some_output"
def some_iterator(arg: str) -> Iterator[str]:
yield "some_output"
Creators
Creators are responsible for creating new instances whenever a dependency is requested.
Transient
A Transient creates a new instance on every request.
some_transient = di.Transient[some_function](arg="some_value")
Services
Services manage dependencies that require a setup phase (before use) and a cleanup phase (after
use). They are especially useful for dependencies defined as generators, but they also work
with functions and classes.
Singleton
A Singleton is lazily instantiated and then shared throughout the application's lifetime.
some_singleton = di.Singleton[some_iterator](arg="some_value")
To clear the singleton's state, call:
di.shutdown(some_singleton)
Scoped
A Scoped provider behaves like a singleton within a specific scope. Within that scope, the
same instance is reused.
scoped_provider = di.Scoped[some_iterator](arg="some_value")
You can inject a scoped dependency using the @di.inject
decorator:
@di.inject
def func(some_instance: str = scoped_provider):
pass
Or by using a context manager:
with di.inject():
some_instance = di.provide(scoped_provider)
Transient
A Transient dependency is similar to a scoped dependency but creates a new instance every time
it is requested—behaving like a transient.
transient_provider = di.Transient[some_iterator](arg="some_value")
Object
An Object holds a constant value that is injected on request.
Instances defined in containers or as function arguments are automatically wrapped
by an ObjectProvider.
Selector
Selectors allow you to include conditional logic (like an if statement) to configure
your application for different variants.
For example, to choose a repository implementation based on an environment variable:
repository = di.Selector[os.getenv("DATABASE")](
in_memory=di.Transient[InMemoryRepository](),
mysql=di.Transient[MySqlRepository](),
)
You can also use a grouped approach if multiple selectors share the same selection variable:
with di.Selector[os.getenv("DATABASE")] as Selector:
user_repository = Selector[UserRepository]()
book_repository = Selector[BookRepository]()
with Selector == "in_memory" as Option:
Option[user_repository] = di.Transient[InMemoryUserRepository]()
Option[book_repository] = di.Transient[InMemoryBookRepository]()
with Selector == "mysql" as Option:
Option[user_repository] = di.Transient[MySqlUserRepository]()
Option[book_repository] = di.Transient[MySqlBookRepository]()
Container
Containers group related dependencies together. They are defined by subclassing di.Container:
class MainContainer(di.Container):
service = di.Transient[Service]()
Traversal
The Travers functionality allows you to iterate over all providers. Its parameters include:
- types: Filter by specific provider types.
- recursive: Traverse providers recursively.
- only_public: Include only public providers.
- only_selected: Include only providers that have been selected.
for name, provider in MainContainer.travers():
pass
Status
You can retrieve the status of a Resource to determine whether it has started, stopped,
or if an error occurred during startup:
di.status(some_resource)
License
Distributed under the terms of the MIT license,
diject is free and open source framework.
Contribution
Contributions are always welcome! To ensure everything runs smoothly,
please run tests using tox
before submitting your changes.
Your efforts help maintain the project's quality and drive continuous improvement.
Issues
If you encounter any problems, please leave issue, along with a detailed
description.
Happy coding with diject! Enjoy cleaner, more maintainable Python applications through effective
dependency injection.