Skip to content

Commit

Permalink
Support threadlocal singletons and manual override for injected argum…
Browse files Browse the repository at this point in the history
…ents
  • Loading branch information
steinitzu committed May 31, 2017
1 parent f4a2a73 commit e74baa1
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 97 deletions.
92 changes: 1 addition & 91 deletions giveme/__init__.py
@@ -1,91 +1 @@
import inspect

from functools import wraps
import threading


class Manager(object):

def __init__(self):
self._registered = {}
self._singletons = {}
self._threadlocals = threading.local

def register(self, func, singleton=False, threadlocal=False):
"""
Register a dependency function
"""
func._giveme_singleton = singleton
func._giveme_threadlocal = threadlocal

self._registered[func.__name__] = func
return func

def remove(self, name):
"""
Remove a dependency by name
"""
del self._registered[name]

def get(self, name):
"""
Get a dependency factory by name, None if not registered
"""
return self._registered.get(name)

def get_value(self, name):
"""
Get return value of a dependency factory or
a live singleton instance.
"""
value = self._singletons.get(name)
if value:
return value
value = getattr(self._threadlocals, name, None)
if value:
return value

function = self._registered.get(name)

if function:
value = function()
if function._giveme_singleton:
self._singletons[name] = value
elif function._giveme_threadlocal:
setattr(self._threadlocals, name, value)
return value
raise KeyError('Name not found')

def clear(self):
self._registered = {}
self._singletons = {}


manager = Manager()


def register(function=None, singleton=False, threadlocal=False):
def decorator(function):
return manager.register(function, singleton=singleton, threadlocal=threadlocal)
if function:
return decorator(function)
else:
return decorator


def inject(func):
@wraps(func)
def wrapper(*args, **kwargs):
signature = inspect.signature(func)
params = signature.parameters
if not params:
return func(*args, **kwargs)
args = list(args)
for i, param in enumerate(signature.parameters):
try:
service = manager.get_value(param)
except KeyError:
continue
args.insert(i, service)
return func(*args, **kwargs)
return wrapper
from .core import inject, register, manager
111 changes: 111 additions & 0 deletions giveme/core.py
@@ -0,0 +1,111 @@
import inspect

from functools import wraps
import threading


class Manager(object):

def __init__(self):
self._registered = {}
self._singletons = {}
self._threadlocals = threading.local()

def register(self, func, singleton=False, threadlocal=False):
"""
Register a dependency function
"""
func._giveme_singleton = singleton
func._giveme_threadlocal = threadlocal

self._registered[func.__name__] = func
return func

def remove(self, name):
"""
Remove a dependency by name
"""
del self._registered[name]

def get(self, name):
"""
Get a dependency factory by name, None if not registered
"""
return self._registered.get(name)

def get_value(self, name):
"""
Get return value of a dependency factory or
a live singleton instance.
"""
factory = self._registered.get(name)
if not factory:
raise KeyError('Name not registered')
if factory._giveme_singleton:
if name in self._singletons:
return self._singletons[name]
self._singletons[name] = factory()
return self._singletons[name]
elif factory._giveme_threadlocal:
if hasattr(self._threadlocals, name):
return getattr(self._threadlocals, name)
setattr(self._threadlocals, name, factory())
return getattr(self._threadlocals, name)
return factory()

def clear(self):
self._registered = {}
self._singletons = {}


manager = Manager()


def register(function=None, singleton=False, threadlocal=False):
"""
Register a dependency factory in the dependency manager. The function name is the
name of the dependency.
This can be used as a decorator.
Args:
function (callable): The dependency factory function
Not needed when used as decorator.
singleton (``bool``, optional): If ``True`` the given function is only called once
during the application lifetime. Injectees will receive the already created
instance when available. Defaults to ``False``
threadlocal (``bool``, optional): Same as singleton except the returned instance
is available only to the thread that created it. Defaults to ``False``
"""
def decorator(function):
return manager.register(function, singleton=singleton, threadlocal=threadlocal)
if function:
return decorator(function)
else:
return decorator


def inject(func):
"""
Inject a dependency into given function's arguments.
Can be used as a decorator.
Args:
func (callable): The function that accepts a dependency.
"""
@wraps(func)
def wrapper(*args, **kwargs):
signature = inspect.signature(func)
params = signature.parameters
if not params:
return func(*args, **kwargs)
args = list(args)
if len(args)+len(kwargs) == len(params):
return func(*args, **kwargs)
for i, param in enumerate(signature.parameters):
try:
service = manager.get_value(param)
except KeyError:
continue
args.insert(i, service)
return func(*args, **kwargs)
return wrapper
3 changes: 2 additions & 1 deletion sample.py
@@ -1,4 +1,5 @@
from giveme import register, inject, manager
from giveme import register, inject
from giveme.core import manager

# Basic

Expand Down
68 changes: 63 additions & 5 deletions tests.py
@@ -1,7 +1,9 @@
import pytest
import time
from functools import wraps
from multiprocessing.pool import ThreadPool

from giveme import register, inject, manager
from giveme import register, inject


def test_inject():
Expand Down Expand Up @@ -42,6 +44,31 @@ def do_some(a, something, b, c=7):
assert do_some(2, 4, 12) == (2, 124, 4, 12)


def test_manual_override():
@register
def something():
return 5

@inject
def do_some(something, n):
return something, n

assert do_some(something=2, n=3) == (2, 3)


def test_manual_overrideb():
@register
def something():
return 5

@inject
def do_some(n, something, k=3):
return n, something, k

assert do_some(1, 2, 3) == (1, 2, 3)
assert do_some(n=1, something=2, k=3) == (1, 2, 3)


def test_nested():
@register
def something():
Expand Down Expand Up @@ -143,8 +170,39 @@ def do_some_again(something):
assert do_some() is do_some_again()
assert do_some().size == 42 and do_some_again().size == 42






def test_threadlocal():
@register(threadlocal=True)
def something():
return [1, 2, 3]

@inject
def do_some(something, add):
time.sleep(0.5) # Make sure they run in separate threads
something.append(add)
return something

tp = ThreadPool()

t1 = tp.apply_async(do_some, args=(4, ))
t2 = tp.apply_async(do_some, args=(5, ))

r1 = t1.get()
r2 = t2.get()

assert r1 == [1, 2, 3, 4]
assert r2 == [1, 2, 3, 5]


def test_no_decorator():
def something():
return [1, 2, 3]

def do_some(something):
something.append(4)
return something

register(something)
do_some = inject(do_some)

assert do_some() == [1, 2, 3, 4]

0 comments on commit e74baa1

Please sign in to comment.