dic¶
Registration¶
In order to create a dependency container, components must be registered within a dic.container.ContainerBuilder. The container builder controls how components will be resolved. Consider a builder as a ‘spec’ for how the container should be built.
Some key points:
- The order of registration does not matter
- A container builder may build multiple containers, each of which will be independent
Registering Classes¶
Core to dic is class registration. dic uses Python 3 annotations to provide ‘hints’ for the types of dependencies that components require. The annotations provide a ‘compile safe’ way of referencing types without creating any hard dependencies on the container, or other dic types.
class OtherDependency(object):
pass
class MyClass(object):
def __init__(self, dependency: OtherDependency):
pass
builder = dic.container.ContainerBuilder()
builder.register_class(MyClass)
builder.register_class(OtherDependency)
container = builder.build()
# use the container
A dic.container.DependencyResolutionError will be raised during a .resolve(...) call if an annotation is provided, but no component registered.
Registering Instances¶
An already-created dependency can be registered directly. This is useful if you’re integrating with other projects, or migrating to dic.
class MyExternalThing(object):
pass
instance = MyExternalThing()
builder = dic.container.ContainerBuilder()
builder.register_instance(MyExternalThing, instance)
container = builder.build()
# use the container
Note that:
- Scopes do not apply to components registered in this way
- Aliases can still be specified with the register_as argument
Custom Registration¶
Lastly, a callback can be provided to create components in any way you want. This provides a good way of integrating things that don’t play nicely with dic, or components that you don’t have control of.
- Scopes are respected
- Aliases can still be specified with the register_as argument
3. The callback is called with a component context that can be used to resolve other dependencies. Do not store this context, as it can only be used for the scope of the context callback.
class OtherThing(object):
pass
class MySpecialThing(object):
def __init__(self, other_thing):
pass
def create_my_thing(component_context):
return MyExternalThing(component_context.resolve(OtherThing))
builder = dic.container.ContainerBuilder()
builder.register_class(OtherThing)
builder.register_callback(MySpecialThing, create_my_thing)
# or as a lambda
# builder.register_callback(MySpecialThing, lambda context: MySpecialThing(context.resolve(OtherThing))
container = builder.build()
# use the container
Aliases (register_as)¶
It’s possible to register callbacks and classes under multiple types. This is useful if you want a specialised implementation available as its base class.
If register_as isn’t specified, then the type of the given component will be used instead. register_as can be:
- A list; or
- A tuple; or
- A single item
class BaseDependency(object):
pass
class SpecialDependency(BaseDependency):
pass
class MyClass(object):
def __init__(self, dependency: BaseDependency):
pass
builder = dic.container.ContainerBuilder()
builder.register_class(MyClass)
builder.register_class(SpecialDependency, register_as=BaseDependency)
# or available as both:
# builder.register_class(SpecialDependency, register_as=(BaseDependency, SpecialDependency))
container = builder.build()
# use the container
Technically any python object can be used as an alias, but to keep things simple and “self documenting” only types are recommended.
Modules¶
Modules are simple classes that help provide clarity when building the container. To use them, derive from dic.container.Module and register the instance of the module when building the container. For example:
class Filesystem(object):
pass
class WindowsFilesystem(Filesystem):
pass
class DefaultFilesystem(Filesystem):
pass
class FilesystemModule(dic.container.Module):
def load(self, builder):
if os.name == 'nt':
builder.register_class(WindowsFilesystem, register_as=[Filesystem])
else:
builder.register_class(DefaultFilesystem, register_as=[Filesystem])
# building the container now has none of this logic
builder = dic.container.ContainerBuilder()
builder.register_module(FilesystemModule())
container = builder.build()
fs = container.resolve(Filesystem)
Scopes¶
Scopes model how long resolved components should live for.
Instance Per Dependency (Default)¶
The default scope is to create a new instance each time the component is resolved.
class ManyOfThese(object):
pass
builder = dic.container.ContainerBuilder()
# this is the default, but shows how the scope can be set
builder.register_class(ManyOfThese, component_scope=dic.scope.InstancePerDependency)
container = builder.build()
# use the container
Single Instance¶
Models a ‘singleton’, no matter how many times the component is resolved, only one instance will be created.
class OneOfThese(object):
pass
builder = dic.container.ContainerBuilder()
builder.register_class(OneOfThese, component_scope=dic.scope.SingleInstance)
container = builder.build()
# use the container
only_one = container.resolve(OneOfThese)
other_only_one = container.resolve(OneOfThese)
# only_one is the same instance as other_only_one
Custom Scopes¶
Scopes are highly extensible, it’s possible to create new scopes by deriving from dic.scope.Scope.
For example, a scope that creates a dependency per calling thread may look like:
class ThreadingScope(dic.scope.Scope):
def __init__(self)
# thread -> instance
self._instances = {}
self._scope_lock = threading.RLock()
def instance(self, create_function):
with self._scope_lock:
thread_id = threading.current_thread().ident
if thread_id not in self.instances:
self._instances[thread_id] = create_function()
return self._instances[thread_id]
# use the scope
builder = dic.container.ContainerBuilder()
builder.register_class(MyClass, component_scope=ThreadingScope)
# ...
Note that the above is a sample. The instances will live beyond the threads.
Relationships¶
Relationships are ‘special’ dependencies that model something more complex than A depends on B. As you’d expect, scopes are respected.
Factory¶
A dic.rel.Factory relationship can be used when you want to be able to create dependencies from a class, without depending on the dic container directly.
For example:
class Part(object):
pass
class BuildsStuff(object):
def __init__(self, part_factory: dic.rel.Factory(Part))
self.part_factory = part_factory
def make_me_one(self):
return self.part_factory()
Custom Arguments¶
It’s possible to specify custom arguments when invoking a factory, the arguments will be used:
- When the dependency isn’t registered in the container; or
- To override a resolve operation for something that is registered in the container
An example of the first:
class Part(object):
def __init__(self, name):
self.name = name
class BuildsStuff(object):
def __init__(self, part_factory: dic.rel.Factory(Part))
self.part_factory = part_factory
def make_me_one_with_a_name(self, name):
return self.part_factory(name=name)
# can then resolve just BuildsStuff
Lazy¶
A dic.rel.Lazy relationship can be used where you want a component, but not yet. For example for breaking circular resolve dependencies. Lazy is implemented in a thread-safe way, so multiple threads will get the same instance.
class EventuallyNeeded(object):
def do_it(self):
pass
class EventuallyWantsYou(object):
def __init__(self, eventually_needed: dic.rel.Lazy(EventuallyNeeded)):
self.eventually_needed = eventually_needed
def ok_ready(self, name):
# EventuallyNeeded will be created here (rather than directly injected in to the constructor)
self.eventually_needed.value.do_it()
Resolving¶
Once a dic.container.Container has been built via the container builder, it’s ready for use. Generally you’ll resolve your application ‘bootstrapper’ after building the container.
Components can be directly resolved from the container:
class MyClass(object):
pass
builder = dic.container.ContainerBuilder()
builder.register_class(MyClass)
container = builder.build()
# factory to create MyClass
factory = container.resolve(dic.rel.Factory(MyClass))
# or instances
instance = container.resolve(MyClass)
Circular Dependencies¶
dic does not yet have ‘circular dependency’ detection yet, this means if a relationship like this is resolved it will likely crash.
Thread Safety¶
dic.container.Container.resolve() is thread-safe. Also see registration for implications with the callback resolve function .register_callback().
Samples¶
dic includes several samples, see the github repository.
CherryPy Songs¶
Extending the CherryPy songs tutorial:
# Example of dic based on the CherryPy songs tutorial
# See https://cherrypy.readthedocs.org/en/3.3.0/tutorial/REST.html#getting-started
import cherrypy
import dic
class Song(object):
def __init__(self, title, artist):
self.title = title
self.artist = artist
def __repr__(self):
return 'title: %s, artist: %s' % (self.title, self.artist)
class SongDatabase(object):
def __init__(self, song_factory: dic.rel.Factory(Song)):
self.song_factory = song_factory
self.songs = {}
def add_song(self, ident, title, artist):
self.songs[ident] = self.song_factory(title=title, artist=artist)
def __getitem__(self, ident):
return self.songs[ident]
def __str__(self):
return self.songs.__str__()
class Songs:
exposed = True
def __init__(self, song_database: SongDatabase):
self.songs = song_database
self.songs.add_song('1', 'Lumberjack Song', 'Canadian Guard Choir')
self.songs.add_song('2', 'Always Look On the Bright Side of Life', 'Eric Idle')
self.songs.add_song('3', 'Spam Spam Spam', 'Monty Python')
def GET(self, id=None):
if id is None:
return('Here are all the songs we have: %s' % self.songs)
elif id in self.songs:
song = self.songs[id]
return(
'Song with the ID %s is called %s, and the artist is %s' % (
id, song.title, song.artist))
else:
return('No song with the ID %s :-(' % id)
if __name__ == '__main__':
builder = dic.container.ContainerBuilder()
builder.register_class(Songs)
builder.register_class(SongDatabase)
builder.register_class(Song)
container = builder.build()
cherrypy.tree.mount(
container.resolve(Songs), '/api/songs',
{'/':
{'request.dispatch': cherrypy.dispatch.MethodDispatcher()}
}
)
cherrypy.engine.start()
cherrypy.engine.block()
Mocking¶
dic wouldn’t be very useful if it didn’t help you test your code. A full example of using dic to inject mocks during tests:
This sample shows: 1. Mocking the database from a service 2. Using the new Python 3.3 mocking library (``unittest.mock`)
# Shows how unittest.mock can be used with dic to mock components during testing
import dic
import unittest, unittest.mock
class Database(object):
def __init__(self):
self.data = {
'1': 'some data',
'2': 'other data',
}
def get_data(self):
return self.data
class Service(object):
def __init__(self, database: Database):
self.database = database
def load(self):
return self.database.get_data()
class ServiceTestCase(unittest.TestCase):
"""
Test case for `Service` that mocks out the database.
"""
def setUp(self):
builder = dic.container.ContainerBuilder()
builder.register_class(Service)
# register a mock instead of the real database
self.database_mock = unittest.mock.Mock(spec=Database)
builder.register_instance(Database, self.database_mock)
container = builder.build()
self.service = container.resolve(Service)
def test_get_data(self):
# Arrange
attrs = {'get_data.return_value': {'3': 'mocked data'}}
self.database_mock.configure_mock(**attrs)
# Act
self.service.load()
# Assert
self.database_mock.get_data.called_once_with()
if __name__ == '__main__':
unittest.main()
API¶
dic.container module¶
- class dic.container.Container(registry_map)¶
Bases: builtins.object
IoC container.
- resolve(component_type, **kwargs)¶
Resolves an instance of the component type. :param component_type: The type of the component (e.g. a class). :param kwargs: Overriding arguments to use (by name) instead of resolving them. :return: An instance of the component.
- class dic.container.ContainerBuilder¶
Bases: builtins.object
Builds a container from the registered configuration.
- build()¶
Builds a new container using the registered components. :return: A container
- register_callback(class_type, callback, component_scope=<class 'dic.scope.InstancePerDependency'>, register_as=None)¶
Registers the given class for creation via the given callback. :param class_type: The class type. :param callback: The function to call to create/get an instance, of the form fn(component_context) :param component_scope: The scope of the component, defaults to instance per dependency. :param register_as: The types to register the class as, defaults to the given class_type.
- register_class(class_type, component_scope=<class 'dic.scope.InstancePerDependency'>, register_as=None)¶
Registers the given class for creation via its constructor. :param class_type: The class type. :param component_scope: The scope of the component, defaults to instance per dependency. :param register_as: The types to register the class as, defaults to the given class_type.
- register_instance(class_type, instance, register_as=None)¶
Registers the given instance (already created). :param class_type: The class type. :param instance: The instance to register. :param register_as: The types to register the class as, defaults to the given class_type.
- register_module(module)¶
Registers the module instance. :param module: The module to register.
- exception dic.container.DependencyResolutionError¶
Bases: builtins.Exception
dic.rel module¶
- class dic.rel.Factory(component_type)¶
Bases: dic.rel.Relationship
Models a factory relationship capable of creating the given type. The scope of the registered component is respected. Meaning a SingleInstance registration will return the same instance for multiple factory calls.
Overriding arguments can be provided.
- resolve(container)¶
- class dic.rel.Lazy(component_type)¶
Bases: dic.rel.Relationship
Models a lazy relationship. Dependency lookup is delayed until the value is resolved for the first time. Lazy is thread-safe, so only one instance will be resolved if two threads ask at the same time.
- resolve(container)¶
dic.scope module¶
- class dic.scope.InstancePerDependency¶
Bases: dic.scope.Scope
Creates an instance per dependency
- instance(create_function)¶
- class dic.scope.Scope¶
Bases: builtins.object
Controls the lifetime scope of a component registration.
- instance(create_function)¶
Gets the instance of the component given its registration. :param create_function: The function to create a new component, if required. :return: The instance
- class dic.scope.SingleInstance¶
Bases: dic.scope.Scope
- instance(create_function)¶