Services
Services are types registered with the dependency injection container that can be
injected in other services and handlers. They are defined with the decorator @service
.
from typing import Annotated
from selva.di import Inject, service
@service
class MyService:
pass
@service
class MyOtherService:
dependency: Annotated[MyService, Inject]
class SomeClass:
pass
class OtherClass:
def __init__(self, dependency: SomeClass):
self.dependency = dependency
@service
async def factory() -> SomeClass:
return SomeClass()
@service
async def other_factory(dependency: SomeClass) -> OtherClass:
return OtherClass(dependency)
Service classes
Services defined as classes have dependencies as class annotations.
from typing import Annotated
from selva.di import Inject, service
@service
class MyService:
pass
@service
class OtherService:
property: Annotated[MyService, Inject]
When the service type is requested in the dependency injection container, the class will be inspected for the annotated dependencies that will be created and then injected into the requested service.
Annotations without Inject
will be ignored.
Initializers and finalizers
Optionally, service classes can define two methods: initialize()
, that will be
called after service creation and dependency injection; and finalize()
, that will
be called on application shutdown.
from selva.di import service
@service
class MyService:
async def initialize(self):
"""perform initialization logic"""
async def finalize(self):
"""perform finalization logic"""
The initialize()
and finalize()
methods do not need to be async.
Services providing an interface
You can have services that provide an interface instead of their own type, so we request the interface as dependency instead of the concrete type.
from typing import Annotated
from selva.di import Inject, service
class Interface:
def some_method(self): pass
@service(provides=Interface)
class MyService:
def some_method(self): pass
@service
class OtherService:
dependency: Annotated[Interface, Inject]
When OtherService
is created, the dependency injection container look for a service
of type Interface
and will produce an instance of the MyService
class.
Named services
Services can be registered with a name, so you can have more than one service of the same type, given they have distinct names. Without a name, a service is registered as the default for that type.
from typing import Annotated
from selva.di import Inject, service
class Interface: pass
@service(name="A", provides=Interface)
class ServiceA: pass
@service(name="B", provides=Interface)
class ServiceB: pass
@service
class OtherService:
dependency_a: Annotated[Interface, Inject(name="A")]
dependency_b: Annotated[Interface, Inject(name="B")]
Optional dependencies
If a requested dependency is not registered, an error is raised, unless there is a default value declared, in the case the property will have that value when the service is created.
from typing import Annotated
from selva.di import Inject, service
@service
class SomeService:
pass
@service
class MyService:
dependency: Annotated[SomeService, Inject] = None
def some_method(self):
if self.dependency:
...
Services as factory functions
In order to register a type that we do not own, for example, a type from an external library, we can use a factory function:
from selva.di import service
from some_library import SomeClass
@service
async def some_class_factory() -> SomeClass:
return SomeClass()
The return type annotation is required in factory functions, as that will be the service provided by function. If the return type annotation is not provided, an error is raised.
The value of the parameter provides
in @service
is ignored when decorating a
factory function, and a warning will be raised.
Factory function parameters do not need the Inject
annotation, unless they need
to specify a named dependency:
from typing import Annotated
from selva.di import Inject, service
from some_library import SomeClass
@service
async def some_class_factory(
dependency: MyService,
other: Annotated[OtherService, Inject(name="service_name")]
) -> SomeClass:
return SomeClass()
Initialization and finalization
To perform initialization on factory functions, you just execute the logic before returning the service.
from selva.di import service
class SomeClass:
pass
@service
async def factory() -> SomeClass:
some_service = SomeClass()
# perform initialization login
return some_service
To perform finalization, you use the yield
instead of return
and execute the
finalization logic afterward.
from selva.di import service
class SomeClass:
pass
@service
async def factory() -> SomeClass:
some_service = SomeClass()
# perform initialization logic
yield some_service
# perform finalization logic
Named services
Named services work the same as in service classes.
from typing import Annotated
from selva.di import Inject, service
from some_library import SomeClass
@service(name="service_name")
def factory() -> SomeClass:
return SomeClass
@service
class MyService:
dependency: Annotated[SomeClass, Inject(name="service_name")]
Optional dependencies
Optional dependencies work the same as in service classes, in that you specify a default value for the argument.