Repository: ettoreleandrotognoli/python-cdi

Issue:
Array Injection
support add multiples instances for one dependency
that would provide a cool way to set middlewares and etc


Comments:

Patch:
diff --git a/pycdi/core.py b/pycdi/core.py
index 7a39794..d7b6293 100644
--- a/pycdi/core.py
+++ b/pycdi/core.py
@@ -11,23 +11,33 @@ INJECT_KWARGS = '_inject_kwargs'
 
 
 class InjectionPoint(object):
-    def __init__(self, member=None, name=None, type=object, context=DEFAULT_CONTEXT):
+    @classmethod
+    def make(cls, member=None, name=None, type=object, context=DEFAULT_CONTEXT):
+        multiple = isinstance(type, (tuple, list,))
+        type = first(type) if multiple else type
+        return cls(member, name, type, context, multiple)
+
+    def __init__(self, member=None, name=None, type=object, context=DEFAULT_CONTEXT, multiple=False):
         self.context = context
         self.name = name
         self.member = member
         self.type = type
+        self.multiple = multiple
 
 
 class CDIContainer(object):
-    def register_instance(self, instance, product_type=None, context=DEFAULT_CONTEXT):
+    def register_instance(self, instance, product_type=None, context=DEFAULT_CONTEXT, priority=None):
         raise NotImplementedError()
 
-    def register_producer(self, producer, produce_type=object, context=DEFAULT_CONTEXT):
+    def register_producer(self, producer, produce_type=object, context=DEFAULT_CONTEXT, priority=None):
         raise NotImplementedError()
 
     def get_producer(self, produce_type=object, context=DEFAULT_CONTEXT):
         raise NotImplementedError()
 
+    def get_producers(self, produce_type=object, context=DEFAULT_CONTEXT):
+        raise NotImplementedError()
+
     def sub_container(self, *args, **kwargs):
         raise NotImplementedError()
 
@@ -56,43 +66,71 @@ def get_di_args(obj):
 def get_di_kwargs(obj):
     di_kwargs = getattr(obj, INJECT_KWARGS, {})
     forward_references = dict([
-        (k, (resolve_forward_reference(v[0], obj), v[1]))
-        for k, v in di_kwargs.items()
-        if isinstance(v[0], string_types)
-    ])
+                                  (k, (resolve_forward_reference(v[0], obj), v[1]))
+                                  for k, v in di_kwargs.items()
+                                  if isinstance(v[0], string_types)
+                                  ])
     di_kwargs.update(forward_references)
     return di_kwargs
 
 
+def first(it):
+    return it[0]
+
+
+def last(it):
+    return it[-1]
+
+
+def sorted_producers(producers):
+    none_priority = filter(lambda it: first(it) is None, producers)
+    with_priority = filter(lambda it: first(it) is not None, producers)
+    return list(none_priority) + sorted(with_priority, key=first)
+
+
 class PyCDIContainer(CDIContainer):
     def __init__(self, producers=None, parent=None):
         self.parent = parent
         self.producers = dict() if producers is None else producers
         self.register_instance(self)
 
-    def register_instance(self, instance, produce_type=None, context=DEFAULT_CONTEXT):
+    def register_instance(self, instance, produce_type=None, context=DEFAULT_CONTEXT, priority=None):
         producer = (lambda *args, **kwargs: instance)
         produce_type = type(instance) if produce_type is None else produce_type
-        self.register_producer(producer, produce_type, context)
+        self.register_producer(producer, produce_type, context, priority)
 
-    def register_producer(self, producer, produce_type=object, context=DEFAULT_CONTEXT):
+    def register_producer(self, producer, produce_type=object, context=DEFAULT_CONTEXT, priority=None):
         context_producers = self.producers.get(context, dict())
-        context_producers[produce_type] = producer
+        producer_item = (priority, producer,)
         types = inspect.getmro(produce_type)
         for t in types:
-            context_producers[t] = producer
+            producers = context_producers.get(t, [])
+            if priority is None:
+                context_producers[t] = [producer_item]
+            else:
+                context_producers[t] = sorted_producers([producer_item] + producers)
         self.producers[context] = context_producers
 
     def get_producer(self, produce_type=object, context=DEFAULT_CONTEXT):
         context_producers = self.producers.get(context, dict())
         producer = context_producers.get(produce_type, False)
         if producer:
-            return producer
+            return producer[0][1]
         if self.parent is not None:
             return self.parent.get_producer(produce_type, context=context)
         else:
             return produce_type
 
+    def get_producers(self, produce_type=object, context=DEFAULT_CONTEXT):
+        context_producers = self.producers.get(context, dict())
+        producer = context_producers.get(produce_type, False)
+        producers = []
+        if producer:
+            producers = producer
+        if self.parent is not None:
+            producers += self.parent.get_producers(produce_type, context=context)
+        return sorted_producers(producers)
+
     def sub_container(self, *args, **kwargs):
         container = PyCDIContainer(parent=self)
         for instance in args:
@@ -106,23 +144,32 @@ class PyCDIContainer(CDIContainer):
                 container.register_instance(instance, context=context)
         return container
 
-    def resolve(self, injection_point):
-        producer = self.get_producer(injection_point.type, injection_point.context)
+    def resolve(self, injection_point, kwargs=None):
+        if kwargs and injection_point.name in kwargs:
+            return kwargs[injection_point.name]
         sub_container = self.sub_container(self, injection_point)
-        return sub_container.call(producer)
+        if injection_point.multiple:
+            producers = self.get_producers(injection_point.type, injection_point.context)
+            return map(sub_container.call, map(last, producers))
+        else:
+            producer = self.get_producer(injection_point.type, injection_point.context)
+            return sub_container.call(producer)
 
     def produce(self, produce_type, context=DEFAULT_CONTEXT):
-        producer = self.get_producer(produce_type, context)
-        return self.call(producer)
+        if isinstance(produce_type, (tuple, list,)):
+            return map(self.call, map(last, self.get_producers(first(produce_type), context)))
+        else:
+            producer = self.get_producer(produce_type, context)
+            return self.call(producer)
 
     def _resolve_di_args(self, member, di_args, args):
-        injection_points = map(lambda kv: InjectionPoint(member, kv[0], *kv[1]), zip(range(len(di_args)), di_args))
+        injection_points = map(lambda kv: InjectionPoint.make(member, kv[0], *kv[1]), zip(range(len(di_args)), di_args))
         inject_args = list(map(lambda ij: self.resolve(ij), injection_points)) + list(args)
         return inject_args
 
     def _resolve_di_kwargs(self, member, di_kwargs, kwargs):
-        injection_points = map(lambda kv: InjectionPoint(member, kv[0], *kv[1]), di_kwargs.items())
-        inject_kwargs = dict(map(lambda ij: (ij.name, self.resolve(ij)), injection_points))
+        injection_points = map(lambda kv: InjectionPoint.make(member, kv[0], *kv[1]), di_kwargs.items())
+        inject_kwargs = dict(map(lambda ij: (ij.name, self.resolve(ij, kwargs)), injection_points))
         inject_kwargs.update(kwargs)
         return inject_kwargs
 
@@ -188,13 +235,14 @@ class Inject(CDIDecorator):
 
 
 class Producer(CDIDecorator):
-    def __init__(self, produce_type=None, _context=DEFAULT_CONTEXT, _container=DEFAULT_CONTAINER):
+    def __init__(self, produce_type=None, _context=DEFAULT_CONTEXT, _priority=None, _container=DEFAULT_CONTAINER):
         super(Producer, self).__init__(_container)
         self.produce_type = produce_type
         self.context = _context
+        self.priority = _priority
 
     def __call__(self, producer):
         annotations = getattr(producer, '__annotations__', {})
         produce_type = annotations.get('return', self.produce_type or object)
-        self.container.register_producer(producer, produce_type, self.context)
+        self.container.register_producer(producer, produce_type, self.context, self.priority)
         return producer


Test patch:
diff --git a/tests/common/test_interface.py b/tests/common/test_interface.py
index 48dbce9..3bb7a90 100644
--- a/tests/common/test_interface.py
+++ b/tests/common/test_interface.py
@@ -23,6 +23,8 @@ class InterfaceCDITest(unittest.TestCase):
             container.sub_container()
         with self.assertRaises(NotImplementedError):
             container.resolve(None)
+        with self.assertRaises(NotImplementedError):
+            container.get_producers()
 
     def test_decorator(self):
         with self.assertRaises(NotImplementedError):
diff --git a/tests/common/test_producer.py b/tests/common/test_producer.py
index c3573e7..291bdf7 100644
--- a/tests/common/test_producer.py
+++ b/tests/common/test_producer.py
@@ -1,8 +1,8 @@
 # -*- encoding: utf-8 -*-
 import unittest
-from pycdi.core import CDIContainer, DEFAULT_CONTAINER
+
 from pycdi import Producer, Inject
-from pycdi.shortcuts import new, call
+from pycdi.core import DEFAULT_CONTAINER
 
 SOME_STRING = 'some_string'
 
@@ -12,8 +12,41 @@ def get_some_string():
     return SOME_STRING
 
 
-
 class TestProducer(unittest.TestCase):
     def test_reference(self):
         self.assertIsNotNone(get_some_string)
         self.assertEqual(SOME_STRING, get_some_string())
+
+
+@Producer(int, _priority=None)
+def get_some_int():
+    return 0
+
+
+@Producer(int, _priority=0)
+def get_another_int():
+    return 1
+
+
+class TestMultipleProducer(unittest.TestCase):
+    def test_priority(self):
+        self.assertEqual(DEFAULT_CONTAINER.produce(int), 0)
+        self.assertEqual(list(DEFAULT_CONTAINER.produce([int])), [0, 1])
+
+    def test_with_subcontainer(self):
+        container = DEFAULT_CONTAINER.sub_container()
+        container.register_instance(2, priority=0)
+        self.assertEqual(list(container.produce([int])), [0, 2, 1])
+        container.register_instance(-1)
+        self.assertEqual(list(container.produce([int])), [-1, 0, 1])
+
+    called = None
+
+    def test_inject(self):
+        @Inject(numbers=([int], 'default'))
+        def function_with_injection(numbers):
+            self.assertEqual(list(numbers), [0, 1])
+            self.called = True
+
+        DEFAULT_CONTAINER.call(function_with_injection)
+        self.assertTrue(self.called)