Brap is a dependency injection container for Python3.
- All the usual reasons you want to use DI
- Detect circular dependencies
- Detect missing dependencies
- ProviderInterface promotes simple ecosystem interoperability
- Inject via constructor
- Inject via method calls
- Define parameters into the container (Useful for a filesystem path or similar ideas)
- You get a reason to say "brap" in real life
Create a container
from brap import Container
container = Container()Brap contains services and parameters.
Services are instances that are unlikely to be created multiple times. A templating service used by a framework that only has one instance is an example (however, you may decide to create multiple instances for very specific reasons)
Something like a "User" object which represents a row in your user table in the database is not a service object.
A lambda is used so the container can look up other parameters in that container at time of creation.
from brap import Container
# Ideally, the classes would be in a different file and imported here
class ERBFileParser(object):
pass
class TemplateEngine(object):
def __init__(self, parser): # We want to inject the ERB parser here
self._parser = parser
container = Container() # Create one instance of a container
container.set(
'file_parser', # no magic here, string can be anything
ERBFileParser # Do not invoke constructor
)
container.set(
'template_engine',
TemplateEngine,
lambda c: c('file_parser') # Reference to service injected to TemplateEngine
)If you are ready to use the template engine, use the same instance of container from above to get (the container is not global).
template_engine = container.get('template_engine')
rendered_template = template_engine.render('some_ruby_template.erb', {
name: "Rick Deckard"
credit_card_number: "5221 2624 8015 6007"
cv2_code: "117"
expiry: "1219"
})If your service requires injection-via-method that can be done by adding a method calling list:
class Foo(object):
def injectMethod(self, parser):
container.set(
'template_engine',
TemplateEngine,
lambda c: c() # No constructor parameters in this example
[
('injectMethod', lambda c: c('file_parser')) # List method calls here
]
)
If your service requires injection-via-kwarg simply specify it in your lambda:
class Foo(object):
def __init__(self, some_kwarg):
container.set(
'template_engine',
TemplateEngine,
lambda c: c(some_kwarg='file_parser') # Reference by kwarg in lambda
)
Factories are no longer directly part of brap.
They can be accomplished by creating a function which returns a new instance, or adding a compiler which will create them for you.
Parameters are just like services, except they are not callable
container.set('database_password', 'swordfish')This lets you do useful things such as:
container.set(
'database_connection',
Database,
lambda c: c('database_password')
)As it is hard to determine if a function is pure, all functions will execute each time with each parameter set instead of memoizing.
def some_function(x)
return x+1
container.set('x_value', 100)
container.set(
'fn_ser',
some_function
lambda c: c('x_value')
)
container.get('fn_ser') # Returns 101
container.get('fn_ser') # Also returns 101For your own sanity, please be careful to not create impure functions with side efects.
One of the big advantages of picking a DI container is the reusability of a set of services.
If you have an encapsulated concept that could in theory become a package, you may wish to define a simple provider interface:
from brap import Container
vendor_container = Container()
thirdPartyContainer.set(
'file_parser', # no magic here, string can be anything
ERBFileParser, # Class we're using
lambda c: c('file_path_parameter') # Services/parameters injected into constructor
)
thirdPartyContainer.set(
'template_engine',
TemplateEngine,
lambda c: c('file_parser')
)Now in some other code base you can load all that configuration up:
container.set('file_path_parameter', '/path/to/templates')
container.merge(vendor_container)
rendered_template = container.get('template_engine').render('template.erb', {})This will only introduce new values in the container that were part of the vendor_container, existing keys will not be over-written, vendor_container is not modified.