Skip to content

Instantly share code, notes, and snippets.

@chrisguitarguy
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrisguitarguy/4aca2403c7b23b9001bf to your computer and use it in GitHub Desktop.
Save chrisguitarguy/4aca2403c7b23b9001bf to your computer and use it in GitHub Desktop.
An IoC container in Python3 using function annotations
#! /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