An IOC Container for Python 3.10+
Project description
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
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
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
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
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
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
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
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
container.register(Client, lifestyle=LifestyleType.transient)
once_per_graph (Default behaviour)
Only create one instance throughout the resolve call
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
container.register(Client, lifestyle=LifestyleType.scoped)
singleton
Only one instance of the object is created throughout the lifespan of the container
container.register(Client, lifestyle=LifestyleType.singleton)
Note: When registering an instance, then the behaviour is always singleton
container.register(int, instance=10)
Open Generics
Registers all generic subclasses of the service type and allows you to resolve with the generic alias
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
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
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.
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
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
We can get the desired behaviour as above
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
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
- Setting the dependency value explicitly
DependencySettings(value=50)
- Using the default parameter value if it exisis the dependency value explicitly
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 settingDependencySettings(use_default_paramater=False)
- 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
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
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
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
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for clean_ioc-0.0.3-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 6cc5af5acfd392fdd13e289a6e63f2d519c095ce5bc2f9c24d96af8543c7e009 |
|
MD5 | 7f36502546a19ad8257e5c65372f7e6a |
|
BLAKE2b-256 | 27cf6f29d1f334dbbcf30d54e50ee11123c0d4661fdfb372bafc70ffb9eba061 |