Module server.core.dependency_injector
Classes
class DependencyInjector
-
Expand source code
class DependencyInjector(object): """ Does dependency injection. Dependencies are resolved by parameter name. So if a class has init method ``` def __init__(self, hello, world): pass ``` the injector will look for two dependencies, called `hello` and `world`. These could either be an object registered with `add_injectables`, or an instance of another class passed to `build_classes`. Injected arguments are only ever constructed once. So if two classes both depend on an object called `hello`, then they will both receive the same instance of the object called `hello` (whether that is an injectable, or another class in the class list). # Examples Create a class that depends on some external injectable. >>> class SomeClass(object): ... def __init__(self, external): ... self.external = external Create a class that depends on the first class. >>> class SomeOtherClass(object): ... def __init__(self, some_class): ... self.some_class = some_class Do the dependency injection. >>> injector = DependencyInjector() >>> injector.add_injectables(external=object()) >>> classes = injector.build_classes({ ... "some_class": SomeClass, ... "other": SomeOtherClass ... }) >>> assert isinstance(classes["some_class"], SomeClass) >>> assert isinstance(classes["other"], SomeOtherClass) >>> assert classes["other"].some_class is classes["some_class"] """ def __init__(self) -> None: # Objects which are available to the constructors of injected objects self.injectables: dict[str, object] = {} def add_injectables( self, injectables: dict[str, object] = {}, **kwargs: object ) -> None: """ Register additional objects that can be requested by injected classes. """ self.injectables.update(injectables) self.injectables.update(kwargs) def build_classes( self, classes: dict[str, type] = {}, **kwargs: type ) -> dict[str, object]: """ Resolve dependencies by name and instantiate each class. """ # kwargs is temporary so we won't be messing with the caller's data kwargs.update(classes) classes = kwargs dep = self._make_dependency_graph(classes) # Can get away with a shallow copy because dep values are not modified # in-place. param_map = dep.copy() instances = self._build_classes_from_dependencies( dep, classes, param_map ) self.add_injectables(**instances) return instances def _make_dependency_graph(self, classes: dict[str, type]) -> DependencyGraph: """ Build dependency graph """ graph: DependencyGraph = defaultdict(list) for name in self.injectables: graph[name] = [] for obj_name, klass in classes.items(): signature = inspect.signature(klass.__init__) # Strip off the `self` parameter params = list(signature.parameters.values())[1:] graph[obj_name] = [param.name for param in params] return graph def _build_classes_from_dependencies( self, dep: DependencyGraph, classes: dict[str, type], param_map: dict[str, list[str]] ) -> dict[str, object]: """ Tries to build all classes in the dependency graph. Raises RuntimeError if some dependencies are not available or there was a cyclic dependency. """ instances: dict[str, object] = {} resolved = ChainMap(instances, self.injectables) while True: if not dep: return instances # Find all services with no dependencies (leaves of our graph) leaves = [ name for name, dependencies in dep.items() if not dependencies ] if not leaves: # Find which dependencies could not be resolved missing = { d for dependencies in dep.values() for d in dependencies if d not in dep } if missing: raise RuntimeError( f"Some dependencies could not be resolved: {missing}" ) else: cycle = tuple(dep.keys()) raise RuntimeError( f"Could not resolve cyclic dependency: {cycle}" ) # Build all objects with no dependencies for obj_name in leaves: if obj_name not in resolved: klass = classes[obj_name] param_names = param_map[obj_name] # Build instances using the objects we've resolved so far instances[obj_name] = klass(**{ param: resolved[param] for param in param_names }) elif obj_name in classes: instances[obj_name] = resolved[obj_name] del dep[obj_name] # Remove leaves from the dependency graph for name, dependencies in dep.items(): dep[name] = [d for d in dependencies if d not in leaves]
Does dependency injection.
Dependencies are resolved by parameter name. So if a class has init method
def __init__(self, hello, world): pass
the injector will look for two dependencies, called
hello
andworld
. These could either be an object registered withadd_injectables
, or an instance of another class passed tobuild_classes
.Injected arguments are only ever constructed once. So if two classes both depend on an object called
hello
, then they will both receive the same instance of the object calledhello
(whether that is an injectable, or another class in the class list).Examples
Create a class that depends on some external injectable.
>>> class SomeClass(object): ... def __init__(self, external): ... self.external = external
Create a class that depends on the first class.
>>> class SomeOtherClass(object): ... def __init__(self, some_class): ... self.some_class = some_class
Do the dependency injection.
>>> injector = DependencyInjector() >>> injector.add_injectables(external=object()) >>> classes = injector.build_classes({ ... "some_class": SomeClass, ... "other": SomeOtherClass ... })
>>> assert isinstance(classes["some_class"], SomeClass) >>> assert isinstance(classes["other"], SomeOtherClass) >>> assert classes["other"].some_class is classes["some_class"]
Methods
def add_injectables(self, injectables: dict[str, object] = {}, **kwargs: object) ‑> None
-
Expand source code
def add_injectables( self, injectables: dict[str, object] = {}, **kwargs: object ) -> None: """ Register additional objects that can be requested by injected classes. """ self.injectables.update(injectables) self.injectables.update(kwargs)
Register additional objects that can be requested by injected classes.
def build_classes(self, classes: dict[str, type] = {}, **kwargs: type) ‑> dict[str, object]
-
Expand source code
def build_classes( self, classes: dict[str, type] = {}, **kwargs: type ) -> dict[str, object]: """ Resolve dependencies by name and instantiate each class. """ # kwargs is temporary so we won't be messing with the caller's data kwargs.update(classes) classes = kwargs dep = self._make_dependency_graph(classes) # Can get away with a shallow copy because dep values are not modified # in-place. param_map = dep.copy() instances = self._build_classes_from_dependencies( dep, classes, param_map ) self.add_injectables(**instances) return instances
Resolve dependencies by name and instantiate each class.