Repository: en0/pyioc3

Issue:
Missing interfaces.
StaticContainer should be built to a Container interface.
StaticContainerBuilder should be built to a ContainerBuilder interface
BoundMember should be built to a BoundMember interface.
Scope interface should be in interface module.

Discussion:

Patch:
diff --git a/pyioc3/__init__.py b/pyioc3/__init__.py
index d5d23b3..e8f2a66 100644
--- a/pyioc3/__init__.py
+++ b/pyioc3/__init__.py
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
 """Python IOC Container"""
 from .static_container_builder import StaticContainerBuilder
-from .static_container import StaticContainer
+from .interface import Container
 from .scope_enum import ScopeEnum
 
 name = "pyioc3"
-__all__ = ["StaticContainerBuilder", "ScopeEnum", "StaticContainer"]
+__all__ = ["StaticContainerBuilder", "Container", "ScopeEnum"]
diff --git a/pyioc3/adapters.py b/pyioc3/adapters.py
index 018036e..931e99d 100644
--- a/pyioc3/adapters.py
+++ b/pyioc3/adapters.py
@@ -1,4 +1,4 @@
-from pyioc3.static_container import StaticContainer
+from .static_container import StaticContainer
 
 
 class ValueAsImplAdapter:
diff --git a/pyioc3/bound_member.py b/pyioc3/bound_member.py
index d2097b9..72c668e 100644
--- a/pyioc3/bound_member.py
+++ b/pyioc3/bound_member.py
@@ -1,14 +1,23 @@
-from inspect import signature
+from typing import Callable, List
+from .scope_enum import ScopeEnum
 
 
 class BoundMember:
-    def __init__(self, annotation, implementation, scope):
-        params = signature(implementation).parameters
-        self.annotation = annotation
-        self.annotation = annotation
-        self.implementation = implementation
-        self.scope = scope
-        self.parameters = [params[p].annotation for p in params]
-        self.depends_on = []
 
+    def __init__(self,
+        annotation: any,
+        implementation: Callable,
+        scope: ScopeEnum,
+        parameters: List[any]
+    ) -> None:
+        self.annotation: any = annotation
+        self.implementation: Callable = implementation
+        self.scope: ScopeEnum = scope
+        self.parameters: List[any] = parameters
+        self._depends_on: List["BoundMember"] = []
 
+    def bind_dependant(self, dependant: "BoundMember") -> None:
+        self._depends_on.append(dependant)
+
+    def __iter__(self):
+        return iter(self._depends_on)
diff --git a/pyioc3/bound_member_factory.py b/pyioc3/bound_member_factory.py
new file mode 100644
index 0000000..b8b897b
--- /dev/null
+++ b/pyioc3/bound_member_factory.py
@@ -0,0 +1,22 @@
+from inspect import signature
+from typing import Callable
+
+from .bound_member import BoundMember
+from .scope_enum import ScopeEnum
+
+
+class DefaultBoundMemberFactory:
+
+    def build(
+        self,
+        annotation: any,
+        implementation: Callable,
+        scope: ScopeEnum
+    ) -> BoundMember:
+        params = signature(implementation).parameters
+        return BoundMember(
+            annotation=annotation,
+            implementation=implementation,
+            scope=scope,
+            parameters=[p.annotation for p in params.values()],
+        )
diff --git a/pyioc3/errors.py b/pyioc3/errors.py
new file mode 100644
index 0000000..4f243e9
--- /dev/null
+++ b/pyioc3/errors.py
@@ -0,0 +1,2 @@
+class CircularDependencyError(RuntimeError):
+    pass
diff --git a/pyioc3/interface.py b/pyioc3/interface.py
new file mode 100644
index 0000000..2006ee0
--- /dev/null
+++ b/pyioc3/interface.py
@@ -0,0 +1,199 @@
+from abc import ABC, abstractmethod
+from typing import Callable, Dict, List
+
+from .scope_enum import ScopeEnum
+from .bound_member import BoundMember
+
+
+class Scope(ABC):
+    """An IOC Instance Scope"""
+
+    @abstractmethod
+    def __contains__(self, annotation: any) -> bool:
+        """Check if the given annotation exists in this scope.
+
+        Arguments:
+         annotation: The str or type var used to identify an instance.
+
+        Returns:
+         True, if the annotation exists. Else, False.
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    def add(self, annotation: any, instance: object) -> None:
+        """Add a instance to this scope under the given annotation
+
+        Arguments:
+         annotation: The str or type var used to identify an instance.
+         instance: Any value to store as the instance associated with the annotation.
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    def use(self, annotation: any) -> object:
+        """Get the instance associated with the given annotation.
+
+        Arguments:
+         annotation: The str or type var used to identify an instance.
+
+        Raises:
+         KeyError, if the the given annotation does not exist in this scope.
+        """
+        raise NotImplementedError()
+
+
+class Container(ABC):
+    """An IOC Container"""
+
+    @abstractmethod
+    def get(self, annotation: any) -> object:
+        """Retrieve an instance from the container
+
+        The instance will be produced according to it's scope. If the instance
+        is new, it's dependency chain will also be created according to their scope.
+
+        Arguments:
+         annotation: The hint used to locate the member
+
+        Returns:
+         An object instance, constant, or function
+        """
+        raise NotImplementedError()
+
+
+class ContainerBuilder(ABC):
+    """Bind classes, values, functions, and factories to a container."""
+
+    @abstractmethod
+    def bind(
+        self,
+        annotation: any,
+        implementation: Callable,
+        scope: ScopeEnum = ScopeEnum.TRANSIENT
+    ):
+        """Bind a class.
+
+        Bind any callable type to an annotation. Dependencies will be
+        injected into this object as needed when created.
+
+        Scoping can be set to control reuse.
+
+        Arguments:
+          annotation:     The hint used to inject an instance of implementation
+
+          implementation: A callable type who's result will be stored return and
+                          stored according to the scope
+
+          scope:          Identifies how the object should be cached. Options are
+                          Transient, Requested, Singleton
+                          Default: Transient.
+
+        Scopes:
+            Transient scopes and not cached.
+            Requested scopes are cached during the current execution of a container.get call.
+            Singleton scopes are only instanced once and cached for the lifetime of the container.
+
+        Example:
+
+            class Duck:
+                def quack(self):
+                    print("quack")
+
+            ioc_builder.bind(
+                annotation="duck",
+                implementation=Duck)
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    def bind_constant(self, annotation: any, value: object):
+        """Bind a constant value
+
+        This allows you to bind any object to an annotation in a singleton scope.
+
+        Arguments:
+          annotation: The hint used to inject the constant
+          value: Any value. Object, function, type, anything.
+
+        Example:
+
+            ioc_builder.bind_constant(
+                annotation="my_constant",
+                value="Hello, world!")
+
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    def bind_factory(self, annotation: any, factory: Callable[[Container], any]):
+        """Bind a higher order function
+
+        This approach allows you to control the creation of objects and gives you access
+        to the container. This lets you make runtime decision about how to create an instance.
+
+        Arguments:
+          annotation: The hint used to inject the factory
+          factory:    A higher order function that accepts the StackContainer as an
+                      arugment.
+
+        Example:
+            def my_factory_wrapper(ctx: StaticContainer)
+
+                def my_factory(foo):
+                    bar = ctx.get("bar")
+                    bar.baz(foo)
+                    return bar
+
+                return my_factory
+
+            ioc_builder.bind_factory(
+                annotation="my_factory",
+                factory=my_function)
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    def build(self) -> Container:
+        """Compute dependency graph and return the container
+
+        This call will roll over all the objects and compute the dependants of each
+        member. The container itself is also added to the graph and can thus be
+        injected using it's Type as the annotation.
+
+        Example:
+            ioc_builder = StaticContainerBuilder()
+            ioc = ioc_builder.build()
+            container = ioc.get(StaticContainer)
+            container == ioc ## True
+        """
+        raise NotImplementedError()
+
+
+class CycleTest(ABC):
+    """Test a graph of BoundMembers for circular dependencies."""
+    @abstractmethod
+    def find_cycle(self, bound_members: Dict[any, BoundMember]) -> List[BoundMember]:
+        """Check graph of BoundMembers for cycles.
+
+        Arguments:
+          bound_members: The graph to test.
+        """
+        raise NotImplementedError()
+
+class BoundMemberFactory(ABC):
+    @abstractmethod
+    def build(
+        self,
+        annotation: any,
+        implementation: Callable,
+        scope: ScopeEnum
+    ) -> BoundMember:
+        """Create a new bound member.
+
+        Arguments:
+          annotation: The hint associated with this member
+          implementation: The implementation used for this member.
+          scope: The scope used to construct implementations for this member.
+        """
+        raise NotImplementedError()
diff --git a/pyioc3/scope_container.py b/pyioc3/scope_container.py
index c215a73..b7cad02 100644
--- a/pyioc3/scope_container.py
+++ b/pyioc3/scope_container.py
@@ -1,23 +1,6 @@
-from abc import ABC, abstractmethod
-
-from pyioc3.scope_enum import ScopeEnum
-from pyioc3.bound_member import BoundMember
-
-
-class Scope(ABC):
-
-    @abstractmethod
-    def __contains__(self, item) -> bool:
-        raise NotImplementedError()
-
-    @abstractmethod
-    def add(self, annotation, instance) -> None:
-        raise NotImplementedError()
-
-    @abstractmethod
-    def use(self, annotation) -> object:
-        raise NotImplementedError()
-
+from .bound_member import BoundMember
+from .interface import Scope
+from .scope_enum import ScopeEnum
 
 class PersistentScope(Scope):
 
@@ -65,7 +48,7 @@ class ScopeContainer:
 
     def _create_instance(self, member: BoundMember) -> object:
         args = list()
-        for dep in member.depends_on:
+        for dep in member:
             args.append(self.get_instance_of(dep))
         return member.implementation(*args)
 
diff --git a/pyioc3/static_container.py b/pyioc3/static_container.py
index fe22596..de085df 100644
--- a/pyioc3/static_container.py
+++ b/pyioc3/static_container.py
@@ -1,23 +1,23 @@
 from collections import deque
 
-from pyioc3.scope_enum import ScopeEnum
-from pyioc3.bound_member import BoundMember
-from pyioc3.scope_container import PersistentScope, ScopeContainer
+from .scope_enum import ScopeEnum
+from .bound_member import BoundMember
+from .scope_container import PersistentScope, ScopeContainer
+from .interface import Container
 
-class StaticContainer:
-    """A static ioc container"""
+
+class StaticContainer(Container):
 
     def __init__(self, bound_members):
         self._singletons = PersistentScope()
         self._bound_members = bound_members
 
     def _build_scope(self, requested_member: BoundMember):
-        """Add members to a scope.
 
-        Build a scope using a post-order traversal of the
-        dependency tree.  This will guarantee the scope has
-        all dependencies for each object it is given to build.
-        """
+        # Build a scope using a post-order traversal of the
+        # dependency tree.  This will guarantee the scope has
+        # all dependencies for each object it is given to build.
+
         scope = ScopeContainer(self._singletons)
         stack = deque()
         stack.append((requested_member, 0))
@@ -27,24 +27,12 @@ class StaticContainer:
                 continue
             elif s == 0:
                 stack.append((m, 1))
-                [stack.append((v, 0)) for v in m.depends_on]
+                [stack.append((v, 0)) for v in m]
             else:
                 scope.add(m)
         return scope
 
     def get(self, annotation):
-        """Retrieve an instance from the container
-
-        The instance will be produced according to it's scope. If the instance
-        is new, it's dependency chain will also be created according to their scope.
-
-        Arguments:
-            annotation:
-                The hint used to locate the member
-
-        Returns:
-            An object instance, constant, or function
-        """
         member = self._bound_members[annotation]
         scope = self._build_scope(member)
         return scope.get_instance_of(member)
diff --git a/pyioc3/static_container_builder.py b/pyioc3/static_container_builder.py
index 0d358fc..293d2af 100644
--- a/pyioc3/static_container_builder.py
+++ b/pyioc3/static_container_builder.py
@@ -1,34 +1,43 @@
 from collections import deque
+from inspect import isclass
+from typing import Dict
 
-from pyioc3.adapters import FactoryAsImplAdapter, ValueAsImplAdapter
-from pyioc3.bound_member import BoundMember
-from pyioc3.scope_enum import ScopeEnum
-from pyioc3.static_container import StaticContainer
+from .adapters import FactoryAsImplAdapter, ValueAsImplAdapter
+from .bound_member import BoundMember
+from .bound_member_factory import DefaultBoundMemberFactory
+from .errors import CircularDependencyError
+from .interface import ContainerBuilder, CycleTest, Container, BoundMemberFactory
+from .queued_cycle_test import QueuedCycleTest as DefaultCycleTest
+from .scope_enum import ScopeEnum
+from .static_container import StaticContainer
 
 
-class StaticContainerBuilder:
-    """Bind classes, values, functions, and factories to a static container."""
+class StaticContainerBuilder(ContainerBuilder):
+    """Bind classes, values, functions, and factories to a container."""
 
-    _bound_members: dict = {}
+    def __init__(self, cycle_test: CycleTest = None):
+        self._bound_members: Dict[any, BoundMember] = {}
+        self._str_annotation_map: Dict[str, any] = {}
+        self._bound_member_factory: BoundMemberFactory = DefaultBoundMemberFactory()
+        self._cycle_test: CycleTest = cycle_test or DefaultCycleTest()
 
     def bind(self, annotation, implementation, scope: ScopeEnum = ScopeEnum.TRANSIENT):
-        """Bind a class
+        """Bind a class.
 
-        This allows you to bind any callable type to an annotation. Dependencies will be
+        Bind any callable type to an annotation. Dependencies will be
         injected into this object as needed when created.
 
         Scoping can be set to control reuse.
 
         Arguments:
-            annotation:
-                The hint used to inject an instance of implementation
+          annotation:     The hint used to inject an instance of implementation
 
-            implementation:
-                A callable type who's result will be stored return and stored according to the scope
+          implementation: A callable type who's result will be stored return and
+                          stored according to the scope
 
-            scope:
-                Identifies how the object should be cached. Options are Transient, Requested, Singleton
-                Default: Transient.
+          scope:          Identifies how the object should be cached. Options are
+                          Transient, Requested, Singleton
+                          Default: Transient.
 
         Scopes:
             Transient scopes and not cached.
@@ -45,7 +54,15 @@ class StaticContainerBuilder:
                 annotation="duck",
                 implementation=Duck)
         """
-        self._bound_members[annotation] = BoundMember(annotation, implementation, scope)
+
+        # This allows callers to use use quoted annotations in place of real types
+        if isclass(annotation):
+            self._str_annotation_map[annotation.__name__] = annotation 
+        # This allows binders to bind arbitrary strings to types
+        elif isinstance(annotation, str):
+            self._str_annotation_map[annotation] = annotation 
+
+        self._bound_members[annotation] = self._bound_member_factory.build(annotation, implementation, scope)
 
     def bind_constant(self, annotation, value):
         """Bind a constant value
@@ -53,11 +70,8 @@ class StaticContainerBuilder:
         This allows you to bind any object to an annotation in a singleton scope.
 
         Arguments:
-            annotation:
-                The hint used to inject the constant
-
-            value:
-                Any value. Object, function, type, anything.
+          annotation: The hint used to inject the constant
+          value: Any value. Object, function, type, anything.
 
         Example:
 
@@ -78,14 +92,12 @@ class StaticContainerBuilder:
         to the container. This lets you make runtime decision about how to create an instance.
 
         Arguments:
-            annotation:
-                The hint used to inject the factory
-
-            factory:
-                A higher order function that accepts the StackContainer as an arugment.
+          annotation: The hint used to inject the factory
+          factory:    A higher order function that accepts the StackContainer as an
+                      arugment.
 
         Example:
-            def my_factory_wrapper(ctx: StaticContainer)
+            def my_factory_wrapper(ctx: Container)
 
                 def my_factory(foo):
                     bar = ctx.get("bar")
@@ -103,49 +115,34 @@ class StaticContainerBuilder:
             FactoryAsImplAdapter(factory),
             scope=ScopeEnum.SINGLETON)
 
-    def build(self) -> StaticContainer:
+    def build(self) -> Container:
         """Compute dependency graph and return the container
 
-        This call will roll over all the objects and compute the dependants of each member. The container
-        itself is also added to the graph and can thus be injected using it's Type as the annotation.
+        This call will roll over all the objects and compute the dependants of each
+        member. The container itself is also added to the graph and can thus be
+        injected using it's Type as the annotation.
 
         Example:
             ioc_builder = StaticContainerBuilder()
             ioc = ioc_builder.build()
-            container = ioc.get(StaticContainer)
+            container = ioc.get(Container)
             container == ioc ## True
         """
-
         container = StaticContainer(self._bound_members)
+        self.bind_constant(Container, container)
+
+        # This is here for backwards compatability
         self.bind_constant(StaticContainer, container)
 
         for bound_member in self._bound_members.values():
             for annotation in bound_member.parameters:
-                bound_member.depends_on.append(self._bound_members[annotation])
+                if isinstance(annotation, str):
+                    annotation = self._str_annotation_map[annotation]
+                bound_member.bind_dependant(self._bound_members[annotation])
 
-        self._assert_acyclic()
+        cycle = self._cycle_test.find_cycle(self._bound_members)
 
-        return container
+        if cycle:
+            raise CircularDependencyError("Circular Dependency Detected: " + ", ".join([str(m.implementation) for m in cycle]))
 
-    def _assert_acyclic(self):
-
-        def _find_cycle(root: BoundMember):
-            visited = set()
-            stack = deque()
-            stack.append((root, 0))
-            while len(stack):
-                v, s = stack.pop()
-                if s == 0 and v in visited:
-                    return visited
-                elif s == 0:
-                    visited.add(v)
-                    stack.append((v, 1))
-                    [stack.append((v, 0)) for v in v.depends_on]
-                elif s == 1:
-                    visited.remove(v)
-            return None
-
-        for r in self._bound_members.values():
-            cycle = _find_cycle(r)
-            if cycle is not None:
-                raise Exception("Circular Dependency Detected: " + ", ".join([str(m.implementation) for m in cycle]))
+        return container


Test patch:
diff --git a/pyioc3/queued_cycle_test.py b/pyioc3/queued_cycle_test.py
new file mode 100644
index 0000000..99a11ab
--- /dev/null
+++ b/pyioc3/queued_cycle_test.py
@@ -0,0 +1,40 @@
+from collections import deque
+from typing import Set, Dict,List
+
+from .bound_member import BoundMember
+from .interface import CycleTest
+
+
+class QueuedCycleTest(CycleTest):
+    def find_cycle(self, bound_members: Dict[any, BoundMember]) -> List[BoundMember]:
+        """Check if a graph of bound members contains a cycle.
+
+        Arguments:
+        bound_members: A dict of BoundMembers.
+        """
+        for r in bound_members.values():
+            cycle = self._find_cycle(r)
+            if cycle is not None:
+                return cycle
+        return []
+
+    def _find_cycle(self, root: BoundMember) -> Set[BoundMember]:
+        """Find a set of bound members that contain circular dependencies.
+
+        Arguments:
+        root: The bound member to begin the cycle test.
+        """
+        visited = set()
+        stack = deque()
+        stack.append((root, 0))
+        while len(stack):
+            v, s = stack.pop()
+            if s == 0 and v in visited:
+                return visited
+            elif s == 0:
+                visited.add(v)
+                stack.append((v, 1))
+                [stack.append((d, 0)) for d in v]
+            elif s == 1:
+                visited.remove(v)
+        return None
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 6943cf0..e0ad55d 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -41,10 +41,6 @@ def duck_d(squeak: QuackBehavior):
     return DuckA(squeak)
 
 
-class Circle:
-    def __init__(self, c: "Circle"):
-        pass
-
 class HalfCircle1:
     def __init__(self, c: "HalfCircle2"):
         pass
diff --git a/tests/test_bound_member.py b/tests/test_bound_member.py
index c039a9a..5e868f6 100644
--- a/tests/test_bound_member.py
+++ b/tests/test_bound_member.py
@@ -1,12 +1,15 @@
 import unittest
 from pyioc3.bound_member import BoundMember
+from pyioc3.bound_member_factory import DefaultBoundMemberFactory
 from pyioc3.scope_enum import ScopeEnum
 
 from .fixtures import DuckA, DuckB, DuckC, duck_d, DuckInterface, QuackBehavior
 
 class BoundMemberTest(unittest.TestCase):
 
+
     def test_expands_parameter_annotations_for_classes(self):
+        factory = DefaultBoundMemberFactory()
         cases = [
             (DuckA, [QuackBehavior]),
             (DuckB, ["QuackBehavior"]),
@@ -15,6 +18,6 @@ class BoundMemberTest(unittest.TestCase):
         ]
         for impl, result in cases:
             with self.subTest(impl):
-                b = BoundMember(DuckInterface, impl, ScopeEnum.TRANSIENT)
+                b = factory.build(DuckInterface, impl, ScopeEnum.TRANSIENT)
                 self.assertListEqual(b.parameters, result)
 
diff --git a/tests/test_static_container.py b/tests/test_static_container.py
index 2c5f891..90491b0 100644
--- a/tests/test_static_container.py
+++ b/tests/test_static_container.py
@@ -3,32 +3,36 @@ from unittest.mock import MagicMock
 
 from pyioc3.static_container import StaticContainer
 from pyioc3.scope_enum import ScopeEnum
+from pyioc3.bound_member import BoundMember
 
 
 class StaticContainerTest(unittest.TestCase):
 
     def setUp(self):
 
-        foo1 = MagicMock()
-        foo1.annotation = "foo1"
-        foo1.parameters = []
-        foo1.depends_on = []
-        foo1.scope = ScopeEnum.TRANSIENT
-        foo1.implementation = object
-
-        foo2 = MagicMock()
-        foo2.annotation = "foo2"
-        foo2.parameters = ["foo1"]
-        foo2.depends_on = [foo1]
-        foo2.scope = ScopeEnum.TRANSIENT
-        foo2.implementation = MagicMock()
-
-        foo3 = MagicMock()
-        foo3.annotation = "foo3"
-        foo3.parameters = ["foo1"]
-        foo3.depends_on = [foo1]
-        foo3.scope = ScopeEnum.TRANSIENT
-        foo3.implementation = MagicMock()
+        foo1 = BoundMember(
+            annotation="foo1",
+            implementation=object,
+            scope=ScopeEnum.TRANSIENT,
+            parameters=[],
+        )
+
+        foo2 = BoundMember(
+            annotation = "foo2",
+            implementation = MagicMock(),
+            scope = ScopeEnum.TRANSIENT,
+            parameters = ["foo1"],
+        )
+
+        foo3 = BoundMember(
+            annotation = "foo3",
+            implementation = MagicMock(),
+            scope = ScopeEnum.TRANSIENT,
+            parameters = ["foo1"],
+        )
+
+        foo2.bind_dependant(foo1)
+        foo3.bind_dependant(foo1)
 
         self.members = {f.annotation: f for f in [ foo1, foo2, foo3 ]}
         self.container = StaticContainer(self.members)
@@ -80,7 +84,7 @@ class StaticContainerTest(unittest.TestCase):
     def test_injected_deps_are_the_same_when_requested_in_same_tree(self):
         self.members["foo1"].scope = ScopeEnum.REQUESTED
         self.members["foo3"].parameters = ["foo1", "foo2"]
-        self.members["foo3"].depends_on = [self.members["foo1"], self.members["foo2"]]
+        self.members["foo3"].bind_dependant(self.members["foo2"])
         self.container.get("foo3")
         foo2_args = self.members["foo2"].implementation.call_args
         foo3_args = self.members["foo3"].implementation.call_args
diff --git a/tests/test_static_container_builder.py b/tests/test_static_container_builder.py
index e61e5f7..c7131cb 100644
--- a/tests/test_static_container_builder.py
+++ b/tests/test_static_container_builder.py
@@ -1,9 +1,10 @@
-import unittest
+from pyioc3.errors import CircularDependencyError
+from pyioc3.interface import Container
+from pyioc3.static_container_builder import StaticContainerBuilder
 from unittest.mock import Mock, patch
+import unittest
 
-from pyioc3.static_container_builder import StaticContainerBuilder
 from .fixtures import (
-    Circle,
     DuckA,
     DuckInterface,
     HalfCircle1,
@@ -15,19 +16,13 @@ from .fixtures import (
 
 class StaticContainerTest(unittest.TestCase):
 
-
     def setUp(self):
         self.builder = StaticContainerBuilder()
 
-    def test_raises_if_cycle(self):
-        self.builder.bind(Circle, Circle)
-        with self.assertRaises(Exception):
-            self.builder.build()
-
     def test_raises_if_deep_cycle(self):
         self.builder.bind(HalfCircle1, HalfCircle1)
         self.builder.bind(HalfCircle2, HalfCircle2)
-        with self.assertRaises(Exception):
+        with self.assertRaises(CircularDependencyError):
             self.builder.build()
 
     def test_raises_if_any_cycle(self):
@@ -35,7 +30,7 @@ class StaticContainerTest(unittest.TestCase):
         self.builder.bind(QuackBehavior, Sqeak)
         self.builder.bind(HalfCircle1, HalfCircle1)
         self.builder.bind(HalfCircle2, HalfCircle2)
-        with self.assertRaises(Exception):
+        with self.assertRaises(CircularDependencyError):
             self.builder.build()
 
     @patch("pyioc3.static_container_builder.StaticContainer")
@@ -45,17 +40,15 @@ class StaticContainerTest(unittest.TestCase):
         container = self.builder.build()
         container_mock.assert_called()
 
-
     @patch("pyioc3.static_container_builder.StaticContainer")
     def test_builds_container_with_initialized_dep_graph(self, container_mock):
         self.builder.bind(DuckInterface, DuckA)
         self.builder.bind(QuackBehavior, Sqeak)
         container = self.builder.build()
         members, *_ = container_mock.call_args[0]
-        dep, = members[DuckInterface].depends_on
+        dep, = list(members[DuckInterface])
         self.assertEqual(QuackBehavior, dep.annotation)
 
-
     @patch("pyioc3.static_container_builder.StaticContainer")
     def test_builds_container_with_members(self, container_mock):
         self.builder.bind(DuckInterface, DuckA)
@@ -65,7 +58,6 @@ class StaticContainerTest(unittest.TestCase):
         self.assertIn(DuckInterface, members)
         self.assertIn(QuackBehavior, members)
 
-
     @patch("pyioc3.static_container_builder.StaticContainer")
     def test_builds_with_static_container(self, container_mock):
         self.builder.bind(DuckInterface, DuckA)
@@ -73,3 +65,11 @@ class StaticContainerTest(unittest.TestCase):
         container = self.builder.build()
         members, *_ = container_mock.call_args[0]
         self.assertIn(container_mock, members)
+
+    @patch("pyioc3.static_container_builder.StaticContainer")
+    def test_builds_with_static_container_as_container_interface(self, container_mock):
+        self.builder.bind(DuckInterface, DuckA)
+        self.builder.bind(QuackBehavior, Sqeak)
+        container = self.builder.build()
+        members, *_ = container_mock.call_args[0]
+        self.assertIn(Container, members)
diff --git a/tests/test_system.py b/tests/test_system.py
new file mode 100644
index 0000000..3217e85
--- /dev/null
+++ b/tests/test_system.py
@@ -0,0 +1,63 @@
+from abc import ABC, abstractmethod
+from pyioc3 import StaticContainerBuilder, ScopeEnum
+from unittest.mock import MagicMock
+import unittest
+
+
+class Interface(ABC):
+    @abstractmethod
+    def method(self) -> None:
+        raise NotImplementedError()
+
+class DependentInterface(ABC):
+    @abstractmethod
+    def x(self) -> None:
+        raise NotImplementedError()
+
+class CacheInterface(ABC):
+    @abstractmethod
+    def inc(self) -> None:
+        raise NotImplementedError()
+
+    @abstractmethod
+    def get_count(self) -> int:
+        raise NotImplementedError()
+
+
+class Cache(CacheInterface):
+    def __init__(self):
+        self._count = 0
+
+    def inc(self) -> None:
+        self._count += 1
+
+    def get_count(self) -> None:
+        return self._count
+
+class Parent(Interface):
+    def __init__(self, dep: DependentInterface):
+        self._dep = dep
+
+    def method(self):
+        self._dep.x()
+
+
+class Child(DependentInterface):
+    def __init__(self, cache: CacheInterface):
+        self.cache = cache
+
+    def x(self):
+        self.cache.inc()
+
+class SystemTest(unittest.TestCase):
+
+    def test_simple(self):
+        builder = StaticContainerBuilder()
+        builder.bind(Interface, Parent)
+        builder.bind(DependentInterface, Child)
+        builder.bind(CacheInterface, Cache, ScopeEnum.SINGLETON)
+        ioc = builder.build()
+        parent = ioc.get(Interface)
+        cache = ioc.get(CacheInterface)
+        parent.method()
+        self.assertEqual(cache.get_count(), 1)