More on IoC
===========
Object Factories
----------------
Spring Python offers two types of factories, *springpython.factory.ReflectiveObjectFactory*
and *springpython.factory.PythonObjectFactory*. These classes should rarely be used directly by
the developer. They are instead used by the different types of configuration
scanners.
Testable Code
-------------
One key value of using the IoC container is how you can isolate parts of
your code for better testing. Imagine you had the following configuration::
from springpython.config import *
from springpython.context import *
class MovieBasedApplicationContext(PythonConfig):
def __init__(self):
super(MovieBasedApplicationContext, self).__init__()
@Object(scope.PROTOTYPE)
def MovieLister(self):
lister = MovieLister()
lister.finder = self.MovieFinder()
lister.description = self.SingletonString()
self.logger.debug("Description = %s" % lister.description)
return lister
@Object(scope.SINGLETON)
def MovieFinder(self):
return ColonMovieFinder(filename="support/movies1.txt")
@Object # scope.SINGLETON is the default
def SingletonString(self):
return StringHolder("There should only be one copy of this string")
To inject a test double for *MovieFinder*, your test code would only have to
extend the class and override the *MovieFinder* method, and replace it with your
stub or mock object. Now you have a nicely isolated instance of *MovieLister*::
class MyTestableAppContext(MovieBasedApplicationContext):
def __init__(self):
super(MyTestableAppContext, self).__init__()
@Object
def MovieFinder(self):
return MovieFinderStub()
Mixing Configuration Modes
--------------------------
Spring Python also supports providing object definitions from multiple sources,
and allowing them to reference each other. This section shows the same app
context, but split between two different sources.
.. highlight:: xml
First, the XML file containing the key object that gets pulled::
"There should only be one copy of this string"
.. highlight:: python
Notice that *MovieLister* is referencing *MovieFinder*, however that object is NOT
defined in this location. The definition is found elsewhere::
class MixedApplicationContext(PythonConfig):
def __init__(self):
super(MixedApplicationContext, self).__init__()
@Object(scope.SINGLETON)
def MovieFinder(self):
return ColonMovieFinder(filename="support/movies1.txt")
.. note::
Object ref must match function name
In this situation, an XML-based object is referencing Python code by the
name MovieFinder. It is of paramount importance that the Python function
have the same name as the referenced string.
With some simple code, this is all brought together when the container is created::
from springpython.context import ApplicationContext
from springpython.config import PyContainerConfig
container = ApplicationContext([MixedApplicationContext(),
PyContainerConfig("mixed-app-context.xml")])
movieLister = container.get_object("MovieLister")
In this case, the XML-based object definition signals the container to look
elsewhere for a copy of the MovieFinder object, and it succeeds by finding it
in MixedApplicationContext.
It is possible to switch things around, but it requires a slight change::
class MixedApplicationContext2(PythonConfig):
def __init__(self):
super(MixedApplicationContext2, self).__init__()
@Object(scope.PROTOTYPE)
def MovieLister(self):
lister = MovieLister()
lister.finder = self.app_context.get_object("MovieFinder") # <-- only line that is different
lister.description = self.SingletonString()
self.logger.debug("Description = %s" % lister.description)
return lister
@Object # scope.SINGLETON is the default
def SingletonString(self):
return StringHolder("There should only be one copy of this string")
.. highlight:: xml
::
"support/movies1.txt"
An XML-based object definition can refer to a @Object by name, however,
the Python code has to change its direct function call to a container lookup,
otherwise it will fail.
.. note::
PythonConfig is ApplicationContextAware
In order to perform a *get_object*, the configuration needs a handle on the
surrounding container. The base class *PythonConfig* provides this, so that you
can easily look for any object (local or not) by using *self.app_context.get_object("name")*.
Querying and modifying the ApplicationContext in runtime
--------------------------------------------------------
*ApplicationContext* instances expose two attributes and an utility method which
let you learn about their current state and dynamically alter them in runtime.
* *object_defs* is a dictionary of objects definitions, that is, the templates
based upon which the container will create appropriate objects, e.g. your singletons,
* *objects* is a dictionary of already created objects stored for later use,
* *get_objects_by_type(type, include_type=True)* returns those ApplicationContext's
objects which are instances of a given type or of its subclasses.
If *include_type* is False then only instances of the type's *subclasses* will
be returned.
.. highlight:: python
Here's an example showing how you can easily query a context to find out what
definitions and objects it holds. The context itself is stored using
:doc:`PythonConfig ` in the *sample_context.py* module and
*demo.py* contains the code which examines the context::
#
# sample_context.py
#
from springpython.config import Object
from springpython.context import scope
from springpython.config import PythonConfig
class MyClass(object):
pass
class MySubclass(MyClass):
pass
class SampleContext(PythonConfig):
def __init__(self):
super(SampleContext, self).__init__()
@Object
def http_port(self):
return 18000
@Object
def https_port(self):
return self._get_https_port()
def _get_https_port(self):
return self.http_port() + 443
@Object
def my_class_object1(self):
return MyClass()
@Object
def my_class_object2(self):
return MyClass()
@Object
def my_subclass_object1(self):
return MySubclass()
@Object
def my_subclass_object2(self):
return MySubclass()
@Object
def my_subclass_object3(self):
return MySubclass()
::
#
# demo.py
#
# Spring Python
from springpython.context import ApplicationContext
# Our sample code.
from sample_context import SampleContext, MyClass, MySubclass
# Create the context.
ctx = ApplicationContext(SampleContext())
# Do we have an 'http_port' object?
print "http_port" in ctx.objects
# Does the context have a definition of an 'ftp_port' object?
print "ftp_port" in ctx.object_defs
# How many objects are there? Observe the result is 7, that's because one of
# the methods - _get_https_port - is not managed by the container.
print len(ctx.objects)
# List the names of all objects defined.
print ctx.object_defs.keys()
# Returns all instances of MyClass and of its subclasses.
print ctx.get_objects_by_type(MyClass)
# Returns all instances of MyClass' subclasses only.
print ctx.get_objects_by_type(MyClass, False)
# Returns all integer objects.
print ctx.get_objects_by_type(int)
The .object_defs dictionary stores instances of *springpython.config.ObjectDef*
class, these are the objects you need to inject into the container to later
successfully access them as if they were added prior to the application's start.
An *ObjectDef* allows one to specify the very same set of parameters an *@Object*
decorator does. The next examples shows how to insert two definitions into a
context, one will be a prototype - a new instance of *Foo* will be created on
each request, the second one will be a singleton - only one instance of *Bar*
will ever be created and stored in a cache of singletons. This time the example
employs the Python's standard library *logging* module to better show in the
*DEBUG* mode what is going on under the hood::
#
# sample_context2.py
#
# Spring Python
from springpython.config import PythonConfig
class SampleContext2(PythonConfig):
def __init__(self):
super(SampleContext2, self).__init__()
::
#
# demo2.py
#
# stdlib
import logging
# Spring Python
from springpython.config import Object, ObjectDef
from springpython.context import ApplicationContext
from springpython.factory import PythonObjectFactory
from springpython.context.scope import SINGLETON, PROTOTYPE
# Our sample code.
from sample_context2 import SampleContext2
# Configure logging.
log_format = "%(msecs)d - %(levelname)s - %(name)s - %(message)s"
logging.basicConfig(level=logging.DEBUG, format=log_format)
class Foo(object):
def run(self):
return "Foo!"
class Bar(object):
def run(self):
return "Bar!"
# Create the context - part 1. in the logs.
ctx = ApplicationContext(SampleContext2())
# Definitions of objects that will be dynamically injected into container.
@Object(PROTOTYPE)
def foo():
""" Returns a new instance of Foo on each call.
"""
return Foo()
@Object # SINGLETON is the default.
def bar():
""" Returns a singleton Bar every time accessed.
"""
return Bar()
# A reference to the function wrapping the actual 'foo' function.
foo_wrapper = foo.func_globals["_call_"]
# Create an object definition, note that we're telling to return
foo_object_def = ObjectDef(id="foo", factory=PythonObjectFactory(foo, foo_wrapper), scope=PROTOTYPE, lazy_init=foo_wrapper.lazy_init)
# A reference to the function wrapping the actual 'bar' function.
bar_wrapper = foo.func_globals["_call_"]
bar_object_def = ObjectDef(id="foo", factory=PythonObjectFactory(bar, bar_wrapper), scope=SINGLETON, lazy_init=bar_wrapper.lazy_init)
ctx.object_defs["foo"] = foo_object_def
ctx.object_defs["bar"] = bar_object_def
# Access "foo" - part 2. in the logs.
for x in range(3):
foo_instance = ctx.get_object("foo")
# Access "bar" - part 3. in the logs.
for x in range(3):
bar_instance = ctx.get_object("bar")
Here's how it shows in the logs. For clarity, the log has been divided into
three parts. Part 1. reads the object definitions from SampleContext2, as we
see, nothing has been read from it as it's still been empty at this point. After
adding definitions to the .object_defs dictionary, we're now at parts 2. and 3.
- in 2. the 'foo' object, a prototype one, is being created three times,
as expected. In part 3. the singleton 'bar' object is created and stored in
a singleton cache once only even though we're accessing it three times in our code.
::
# Part 1.
100 - DEBUG - springpython.config.PythonConfig - ==============================================================
100 - DEBUG - springpython.config.PythonConfig - Parsing
101 - DEBUG - springpython.config.PythonConfig - ==============================================================
101 - DEBUG - springpython.container.ObjectContainer - === Done reading object definitions. ===
# Part 2.
102 - DEBUG - springpython.context.ApplicationContext - Did NOT find object 'foo' in the singleton storage.
102 - DEBUG - springpython.context.ApplicationContext - Creating an instance of id=foo props=[] scope=scope.PROTOTYPE factory=PythonObjectFactory()
102 - DEBUG - springpython.factory.PythonObjectFactory - Creating an instance of foo
102 - DEBUG - springpython.config.objectPrototype - ()scope.PROTOTYPE - This IS the top-level object, calling foo().
102 - DEBUG - springpython.config.objectPrototype - ()scope.PROTOTYPE - Found <__main__.Foo object at 0x184b650>
102 - DEBUG - springpython.context.ApplicationContext - Did NOT find object 'foo' in the singleton storage.
102 - DEBUG - springpython.context.ApplicationContext - Creating an instance of id=foo props=[] scope=scope.PROTOTYPE factory=PythonObjectFactory()
102 - DEBUG - springpython.factory.PythonObjectFactory - Creating an instance of foo
103 - DEBUG - springpython.config.objectPrototype - ()scope.PROTOTYPE - This IS the top-level object, calling foo().
103 - DEBUG - springpython.config.objectPrototype - ()scope.PROTOTYPE - Found <__main__.Foo object at 0x184b690>
103 - DEBUG - springpython.context.ApplicationContext - Did NOT find object 'foo' in the singleton storage.
103 - DEBUG - springpython.context.ApplicationContext - Creating an instance of id=foo props=[] scope=scope.PROTOTYPE factory=PythonObjectFactory()
103 - DEBUG - springpython.factory.PythonObjectFactory - Creating an instance of foo
103 - DEBUG - springpython.config.objectPrototype - ()scope.PROTOTYPE - This IS the top-level object, calling foo().
103 - DEBUG - springpython.config.objectPrototype - ()scope.PROTOTYPE - Found <__main__.Foo object at 0x184b650>
# Part 3.
103 - DEBUG - springpython.context.ApplicationContext - Did NOT find object 'bar' in the singleton storage.
103 - DEBUG - springpython.context.ApplicationContext - Creating an instance of id=foo props=[] scope=scope.SINGLETON factory=PythonObjectFactory()
103 - DEBUG - springpython.factory.PythonObjectFactory - Creating an instance of bar
104 - DEBUG - springpython.config.objectSingleton - ()scope.SINGLETON - This IS the top-level object, calling bar().
104 - DEBUG - springpython.config.objectSingleton - ()scope.SINGLETON - Found <__main__.Bar object at 0x184b690>
104 - DEBUG - springpython.context.ApplicationContext - Stored object 'bar' in container's singleton storage
Please note that what has been shown above applies to runtime only, adding object
definitions to the container doesn't mean the changes will be in any way
serialized to the file system, they are transient and will be lost when the
application will be shutting down. Another thing to keep in mind is that you'll
be modifying a raw Python dictionary and if your application is multi-threaded,
you'll have to serialize the access from concurrent threads yourself.