# Clean IoC
A simple dependency injection library for python that requires nothing of your application code (except that you use typing).
## Basic Registering and resolving
There are 4 basic modes of registering a new set of classes
### Implementation
```python
class UserRepository(abc.ABC)
@abc.abstractmethod
def add(self, user):
pass
class InMemoryUserRepository(UserRepository)
def __init__(self):
self.users = []
def add(self, user):
# This is obviously terrible, but it's for demo purposes
self.users.append(user)
class SqlAlchemyUserRepository(UserRepository)
def __init__(self):
# Do some db stuff here
pass
def add(self, user):
# Do some db stuff here
pass
container = Container()
container.register(UserRepository, InMemoryUserRepository)
repository = container.resolve(UserRepository) # This will return an InMemoryUserRepository
```
### Concrete Class
```python
class ClientDependency
def get_int(self):
return 10
class Client
def __init__(self, dep: ClientDependency)
self.dep = dep
def get_number(self):
return self.dep.get_int()
container = Container()
container.register(ClientDependency)
container.register(Client)
client = container.resolve(Client)
client.get_number() # returns 10
```
### Factory
```python
class ClientDependency
def get_int(self):
return 10
class Client
def __init__(self, dep: ClientDependency)
self.dep = dep
def get_number(self):
return self.dep.get_int()
def client_factory(dep: ClientDependency):
return Client(dep=dep)
container = Container()
container.register(ClientDependency)
container.register(Client, factory=client_factory)
client = container.resolve(Client)
client.get_number() # returns 10
```
### Instance
```python
class ClientDependency
def __init__(self, num):
self.num = num
def get_int(self):
return self.num
class Client
def __init__(self, dep: ClientDependency)
self.dep = dep
def get_number(self):
return self.dep.get_int()
client_dependency = ClientDependency(num=10)
container = Container()
container.register(ClientDependency, instance=client_dependency)
container.register(Client)
client = container.resolve(Client)
client.get_number() # returns 10
```
## List resolving
```python
class ClientDependency
def __init__(self, numbers: list[int]):
self.numbers = numbers
def get_numbers(self):
return self.numbers
class Client
def __init__(self, dep: ClientDependency)
self.dep = dep
def get_numbers(self):
return self.dep.get_numbers()
container = Container()
container.register(ClientDependency)
container.register(Client)
container.register(int, instance=1)
container.register(int, instance=2)
container.register(int, instance=3)
client = container.resolve(Client)
client.get_numbers() # returns [3, 2, 1]
```
## Decorators
Follows a object orientated decoration pattern, rather than a decoration annotation.
The main reason for this was to allow decotation of registered instances
```python
class Client
def __init__(self, number: int)
self.number = number
def get_number(self):
return self.number
class DoubleClientDecorator(Client):
def __init__(self, client: Client):
self.client = client
def get_number(self):
return self.client.get_number() * 2
container = Container()
container.register(Client)
container.register_decorator(Client, DoubleClientDecorator)
container.register(int, instance=10)
client = container.resolve(Client)
client.get_number() # returns 20
```
## Subclasses registration
This feature allows registartion of all subclasses of a giveb type
```python
class Client(abc.ABC)
@abc.abstractmethod
def get_number(self):
pass
class TenClient(Client)
def get_number(self):
return 10
class TwentyClient(Client)
def get_number(self):
return 20
container = Container()
container.register_subclasses(Client)
ten_client = container.resolve(TenClient)
ten_client.get_number() # returns 10
twenty_client = container.resolve(TwentyClient)
twenty_client.get_number() # returns 20
# Resolve all subsclasses of Client
client = container.resolve(list[Client]) ## [TwentyClient(), TenClient()]
```
## Lifestyles
Lifestyles configure how long and resolved object says alive for
There are 4 lifestyle types
### transient
Always create a new instance
```python
container.register(Client, lifestyle=LifestyleType.transient)
```
### once_per_graph (Default behaviour)
Only create one instance throughout the resolve call
```python
container.register(Client, lifestyle=LifestyleType.once_per_graph)
```
### scoped
Only create a new instance through the life a scope. When not in a scope the behaviour is the same as **once_per_graph**
```python
container.register(Client, lifestyle=LifestyleType.scoped)
```
### singleton
Only one instance of the object is created throughout the lifespan of the container
```python
container.register(Client, lifestyle=LifestyleType.singleton)
```
*Note:*
When registering an instance, then the behaviour is always singleton
```python
container.register(int, instance=10)
```
## Open Generics
Registers all generic subclasses of the service type and allows you to resolve with the generic alias
```python
T = TypeVar("T")
class HelloCommand:
pass
class GoodbyeCommand:
pass
class CommandHandler(Generic[T]):
def handle(self, command: T):
pass
class HelloCommandHandler(CommandHandler[HelloCommand]):
def handle(self, command: HelloCommand):
print('HELLO')
class GoodbyeCommandHandler(CommandHandler[GoodbyeCommand]):
def handle(self, command: GoodbyeCommand):
print('GOODBYE')
container = Container()
container.register_open_generic(CommandHandler)
h1 = container.resolve(CommandHandler[HelloCommand])
h2 = container.resolve(CommandHandler[GoodbyeCommand])
h1.handle(HelloCommand()) # prints 'HELLO'
h2.handle(GoodbyeCommand()) # prints 'GOODBYE'
```
## Open Generic Decorators
Allows you to add decorators to your open generic registrations
```python
T = TypeVar("T")
class HelloCommand:
pass
class GoodbyeCommand:
pass
class CommandHandler(Generic[T]):
def handle(self, command: T):
pass
class HelloCommandHandler(CommandHandler[HelloCommand]):
def handle(self, command: HelloCommand):
print('HELLO')
class GoodbyeCommandHandler(CommandHandler[GoodbyeCommand]):
def handle(self, command: GoodbyeCommand):
print('GOODBYE')
class AVeryBigCommandHandlerDecorator(Generic[T]):
def __init__(self, handler: CommandHandler[T]):
self.handler = handler
def handle(self, command: T):
print('A VERY BIG')
self.handler.handle(command=command)
container = Container()
container.register_open_generic(CommandHandler)
container.register_open_generic_decorator(CommandHandler, AVeryBigCommandHandlerDecorator)
h1 = container.resolve(CommandHandler[HelloCommand])
h2 = container.resolve(CommandHandler[GoodbyeCommand])
h1.handle(HelloCommand()) # prints 'A VERY BIG\nHELLO'
h2.handle(GoodbyeCommand()) # prints 'A VERY BIG\nGOODBYE'
```
## Scopes
Scopes are a way to create a sub container that will live for a certain lifestyle.
Some good use cases for scope would be for the lifespan of handling a http request with a web server or a message/event if working on a message based system
```python
class Client
def __init__(self, number: int)
return number
def get_number(self):
return self.resolver.resolve(int)
container.register(int, instance=2)
container.register(Client)
client = container.resolve(Client)
client.get_number() # returns 2
with container.get_scope() as scope:
scope.register(int, instance=10)
scoped_client = scope.resolve(Client)
scoped_client.get_number() # returns 10
```
## Named registartions & Registartion filters
By default the last registration is what the container will return when resolve is called as below.
```python
container = Container()
container.register(int, instance=1)
container.register(int, instance=2)
container.register(int, instance=3)
number = container.resolve(int) # returns 3
```
To be more selective of what we return we can add a name to the registration and apply a registartion filter when we resolve.
A registartion filter is simply function that receives a **Registartion** and returns a **bool**
For example if we wanted to get the int named **"One"** we do the following
```python
container = Container()
container.register(int, instance=1, name="One")
container.register(int, instance=2, name="Two")
container.register(int, instance=3, name="Three")
number = container.resolve(int, filter=lambda r: r.name == "One") # returns 1
```
Clean IOC comes with a set of in built registartion filters that can be found [here](./clean_ioc/registration_filters.py)
We can get the desired behaviour as above
```python
from clean_ioc.registartion_filters import with_name
container = Container()
container.register(int, instance=1, name="One")
container.register(int, instance=2, name="Two")
container.register(int, instance=3, name="Three")
number = container.resolve(int, filter=with_name("One")) # returns 1
```
## Dependency Settings
Dependency settings are defined at registartion and allow you to define the selection or setting dependencies
```python
class Client
def __init__(self, number=10)
self.number = number
def get_number(self):
return self.number
container = Container()
container.register(int, instance=1, name="One")
container.register(int, instance=2, name="Two")
container.register(
Client,
name="SetsValue",
dependency_config={"number": DependencySettings(value=50)}
)
container.register(
Client,
name="UsesDefaultValue"
)
container.register(
Client,
name="IgnoresDefaultParameterValue",
dependency_config={"number": DependencySettings(use_default_paramater=False)}
)
container.register(
Client,
name="UsesRegistartionFilter",
dependency_config={"number": DependencySettings(use_default_paramater=False, filter=with_name("One"))}
)
client1 = container.resolve(Client, filter=with_name("SetsValue"))
client2 = container.resolve(Client, filter=with_name("UsesDefaultValue"))
client3 = container.resolve(Client, filter=with_name("IgnoresDefaultParameterValue"))
client4 = container.resolve(Client, filter=with_name("UsesRegistartionFilter"))
client1.get_number() # returns 50
client2.get_number() # returns 10
client3.get_number() # returns 2
client4.get_number() # returns 1
```
The order of a dependant value is as follows
1. Setting the dependency value explicitly
```python
DependencySettings(value=50)
```
2. Using the default parameter value if it exisis the dependency value explicitly
```python
class Client
def __init__(self, number=10)
self.number = number
```
If you don't want to use the default parameter value you can set it to false in the dependency setting
```python
DependencySettings(use_default_paramater=False)
```
3. Going to the container registry to find a registartion using the registration filter if, if there is a default value on the dependant paramater you must explicity set.
## Accessing the Container, Scope and Resolver within dependencies
Accessing container directly
```python
class Client
def __init__(self, container: Container)
self.container = container
def get_number(self):
return self.container.resolve(int)
container.register(int, instance=2)
container.register(Client)
client = container.resolve(Client)
client.get_number() # returns 2
```
Accessing Resolver also returns the container
```python
class Client
def __init__(self, resolver: Resolver)
self.resolver = resolver
def get_number(self):
return self.resolver.resolve(int)
container.register(int, instance=2)
container.register(Client)
client = container.resolve(Client)
client.get_number() # returns 2
```
When within a scope, Resolver returns the current scope rather than the container
```python
class Client
def __init__(self, resolver: Resolver)
self.resolver = resolver
def get_number(self):
return self.resolver.resolve(int)
container.register(int, instance=2)
container.register(Client)
client = container.resolve(Client)
client.get_number() # returns 2
with container.get_scope() as scope:
scope.register(int, instance=10)
scoped_client = scope.resolve(Client)
scoped_client.get_number() # returns 10
```
## Modules (BETA feature)
A module is a just a function that accepts a container, it can be used to set up common elements on the container
```python
class ClientDependency
def get_int(self):
return 10
class Client
def __init__(self, dep: ClientDependency)
self.dep = dep
def get_number(self):
return self.dep.get_int()
def client_module(c: Container):
container = Container()
container.register(ClientDependency)
container.register(Client)
container.apply_module(client_module)
client = container.resolve(Client)
client.get_number() # returns 10
```