Last active
August 29, 2015 14:06
-
-
Save chrisguitarguy/4aca2403c7b23b9001bf to your computer and use it in GitHub Desktop.
An IoC container in Python3 using function annotations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
import inspect | |
import functools | |
class InvalidObjectError(ValueError): | |
""" | |
Raised when the service locator cannot create a class | |
""" | |
pass | |
class UnknownServiceError(KeyError): | |
""" | |
Raised when a service is unknown to the service locator | |
""" | |
pass | |
class Inject: | |
""" | |
A simple marker to indicate that a parameter should be injected by the | |
ServiceLocator. | |
""" | |
def __init__(self, name): | |
self.name = name | |
class Container: | |
""" | |
Responsible for creating objects and injecting their dependencies | |
""" | |
def __init__(self, registry_cls=dict): | |
self.registry = registry_cls() | |
def make(self, klass): | |
sig = self._create_signature(klass) | |
if not len(sig.parameters): | |
return klass() | |
partial = False | |
kwargs = dict() | |
args = list() | |
for name, param in sig.parameters.items(): | |
inject = self._locate_service(param) | |
if inject is None and not self._has_default(param): | |
partial = True | |
continue | |
if param.kind is param.POSITIONAL_ONLY: | |
args.append(inject) | |
else: | |
kwargs[name] = inject | |
return functools.partial(klass, *args, **kwargs) if partial else klass(*args, **kwargs) | |
def add_service(self, servicename, callable_or_object): | |
self.registry[servicename] = callable_or_object | |
def _create_signature(self, klass): | |
try: | |
return inspect.signature(klass) | |
except (ValueError, TypeError): | |
raise InvalidObjectError('could not create '+repr(klass)) | |
def _locate_service(self, param): | |
if not self._is_injectable(param): | |
return None | |
servicename = param.annotation.name | |
try: | |
service = self.registry[servicename] | |
except KeyError: | |
raise UnknownServiceError('"{}" is not a known service'.format(servicename)) | |
if callable(service): | |
return service() | |
return service | |
def _is_injectable(self, param): | |
if param.annotation is param.empty: | |
return False | |
return isinstance(param.annotation, Inject) | |
def _has_default(self, param): | |
return param.default is not param.empty | |
class X: | |
def __init__(self, param: Inject('service')): | |
print(param) | |
class Y: | |
def __init__(self, param, param2: Inject('service')): | |
print(param) | |
print(param2) | |
def func(param : Inject('nope')): | |
pass | |
if __name__ == '__main__': | |
z = 0 | |
container = Container() | |
container.add_service('service', lambda: z + 1) | |
x = container.make(X) | |
y = container.make(Y) | |
y('param 2') | |
container.make(func) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment