Skip to content

Small, lightweight dependency injection (IoC) container with component and dependency lifecycle management features for python.

License

Notifications You must be signed in to change notification settings

saintx/pycocontainer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pycocontainer

A small, lightweight dependency injection (IoC) container with component and dependency lifecycle management features written in and designed for writing idiomatic python.

Inspired by the seminal PicoContainer project at The Codehaus, but not in any way associated with PicoContainer. Also not a 'port' of PicoContainer to python.

How it works

Define your component classes and mark them up to use lifecycle management, like so:

from pycocontainer import *

class A(Lifecycle):
    def __init__(self, b):
        super(A, self).__init__()
        self.b = b

    @startmethod
    def foo(self, *args):
        print "Called foo()"

    @stopmethod
    def bar(self, *args):
        print "Called bar()"

    @failmethod
    def baz(self, *args):
        print "Called baz()"

class B(Lifecycle):
    def __init__(self):
        super(B, self).__init__()

    @startmethod
    def funk(self, *args):
        print "Called funk()"

    @stopmethod
    def soul(self, *args):
        print "Called soul()"

    @failmethod
    def boogie(self, *args):
        print "Called boogie()"

Now, create a container instance and register the component classes in the container:

pyco = Pycocontainer('Woot')
pyco.register(A, 'a')
pyco.register(B, 'b')

Once the class definitions of the components are registered in the container, we can use the container to assemble component instances for us:

a = pyco.instance_of(A, 'foo')
print a.b         # yields <test.B object at 0x9b59d0c>
print a.b.stage   # yields Stage.stopped

We can make as many as we like:

In [9]: [pyco.instance_of(A, 'foo%s' % x) for x in list(range(10))]
Out[9]:
[<test.A at 0x9b5d1cc>,
 <test.A at 0x9b5d1ec>,
 <test.A at 0x9b5d22c>,
 <test.A at 0x9b5d26c>,
 <test.A at 0x9b487ac>,
 <test.A at 0x9aa074c>,
 <test.A at 0x9a4fdec>,
 <test.A at 0x9a4fe8c>,
 <test.A at 0x9a4feac>,
 <test.A at 0x9a2512c>]

But this is only the beginning! Once we have our components instantiated and wired together, we can tell the container to start managing their lifecycle:

In [10]: a.stage
Out[10]: <Stage.stopped: 3>

In [11]: a.b.stage
Out[11]: <Stage.stopped: 3>

In [12]: pyco.start(a)
Called funk()
Called foo()

In [13]: a.stage
Out[13]: <Stage.started: 1>

And, in the process of transitioning a component into its started state, pycocontainer also did so for all of its dependencies, in the topological order of the backing acyclic digraph:

In [14]: a.b.stage
Out[14]: <Stage.started: 1>

Moreover, because the container knows which components depend on each other, we can have orderly shutdowns, allowing us to safely spin down a component and know that anything depending on it will be forced to properly handle the situation:

In [15]: pyco.stop(a.b)
Called bar()
Called soul()

In [16]: a.b.stage
Out[16]: <Stage.stopped: 3>

In [17]: a.stage
Out[17]: <Stage.stopped: 3>

But, it's smart enough not to do more than we ask:

In [18]: pyco.start(a.b)
Called funk()

In [19]: a.b.stage
Out[19]: <Stage.started: 1>

In [20]: a.stage
Out[20]: <Stage.stopped: 3>

And, if things go badly wrong, we can explicitly track and manage failure conditions:

In [21]: pyco.fail(a.b)
Called baz()
Called boogie()

In [22]: a.stage
Out[22]: <Stage.failed: 5>

In [23]: a.b.stage
Out[23]: <Stage.failed: 5>

Maybe we don't want to write adapter classes, and just want to rely on "duck" classes. Not a problem.

class E():
    from pycocontainer import Stage
    def __init__(self):
        self.stage = Stage.stopped

    def start(self):
        self.stage = Stage.starting
        print "Called E.start()"
        self.stage = Stage.started

    def stop(self):
        self.stage = Stage.stopping
        print 'Called E.stop()'
        self.stage = Stage.stopped

    def fail(self):
        self.stage = Stage.failing
        print 'Called E.fail()'
        self.stage = Stage.failed
...

In [35]: e = pyco.instance_of(E, 'metal')

In [36]: e.stage
Out[36]: <Stage.stopped: 3>

In [37]: pyco.start(e)
E.start()

In [38]: e.stage
Out[38]: <Stage.started: 1>

If you need to, you can give hints to the container about which specific component instances to use for which parameters in a given instance. Consider the following test case:

    def test_attribute_hints(self):
        # Register A and B
        # instantiate two named instances of B
        # instantiate one instance of A, pointing at first B instance.
        # instantiate one instance of A, pointing at second B instance.
        # The B instances should be distinct.
        pyco = self.pyco
        pyco.register(A, 'a')
        pyco.register(B, 'b')
        funk = pyco.instance_of(B, 'funk')
        soul = pyco.instance_of(B, 'soul')
        # Can't settle for any old B, I need the funk.
        foo = pyco.instance_of(A, 'foo', {'b':'funk'})
        # Not enough funk to go around, I'm afraid. Gimme some soul.
        bar = pyco.instance_of(A, 'bar', {'b':'soul'})
        self.assertIsNot(foo, bar)
        self.assertIsNot(foo.b, bar.b)
        self.assertIs(foo.b, funk)
        self.assertIs(bar.b, soul)

What else?!

There's more. The tests do a pretty good job illustrating how it all works.

It's fast. The backing DAG is very good at knowing what depends on what. It can detect complex cyclic dependencies when you register components, so you don't end up with infinite start/stop/fail loops. Topological ordering executes in O(V+E) time, scaling linearly with the numbers of vertices (instances), plus edges (dependency relationships) in the container.

The python class and method decorators are clean and intuitive, letting you spend less time on boilerplate lifecycle code.

Why Dependency Injection?

Is all of this really necessary?

Alex Martelli gave a great presentation on the dependency injection design pattern for Python. Over time, I hope to furnish additional concrete examples to illustrate the advantages of this design practice.

(Anecdotally, I think DI containers are useful tools that help drive test driven development. If used during the design phase, it can help inform very modular, loosely coupled, easily tested software components--ravioli code, vs spaghetti code. I personally find it helps me avoid creating modules like core, utils, helper and other junk drawer modules for dumping methods into, and it helps me stick to a more meticulous OOP design.)

About

Small, lightweight dependency injection (IoC) container with component and dependency lifecycle management features for python.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages