Back in 2007 I started programming in python. At first, the goal was
to learn how to write programs the pythonic way, being my background a
Java Enterprise programmer, I wanted to avoid programming java-like
in python.
We started building a web application, using Pylons, SQLAlchemy andp
Mako as the base frameworks. At first we struggled with poor
documentation (we were used to have lots of books about each
framework in java), but as the time passed, we started feeling
comfortable with python and its ways.
The project became bigger and bigger, and it was at that time that we
started missing Java and its "bureaucratic" way of coding. Don't
get me wrong, bureaucracy is not a bad thing. The issue is unnecessary
bureaucracy. We started missing static types, lack of module variables,
long package names, and inversion of control to rule them all.
At that time we started using "the java way of coding" in python, in order
to make the project more maintainable. For example, we quit using the
sqlAlchemy session as a module variable, and started injecting it. We
erased every static reference to a module variable, and we also wanted
to get good package names (at first they seem to be the same as java,
but that's misguided).
So, we needed an inversion of control (IoC) library and started to look for one.
Why a new one?
When we started looking for an IoC library, we didn't find one that filled
ours needs. The exception was Spring-Python, but from our point of view,
it was a port one on one of the Java Spring framework; and we didn't want
something that complex.
What we were looking for, was something that with much of the Spring
IoC capabilities, but taking into consideration that this was python
and not Java: There are things that are simpler to do in python,
and other that are simpler to do in Java. Above all, we wanted
to say "inject this instance on that other
instance" in the simplest way possible. That was the reason we decided to do our own. I named it "ControlFreak"
How and When?
This is kind of a disclaimer, because ControlFreak was built on spare
hours, spare and sparse hours. And it's something I would like to
refactor, probably as a whole, when I found the time for it.
Enough of introducction, let's me show what is controlfreak!
ControlFreak: The library
Controlfreak is a IoC library with capabilities similar to what
spring does in Java. But there are some changes on it's design
that are worth to mention.
The source code can be found in a SVN repository
. Also, controlfreak depends on pycommons, that can be found in this
svn repo.
Components
When you code, you tend to think about interfaces and the different
implementations of them. It's like you have a contract, and several
providers of that contract.
Discussing with a colleague of mine, we thought that it would be
interesting to take that concept into the IoC configurations. So, that
you won't be working just with beans, but with components. A component
is a group of beans, that, one can think, provides (or exports) some
beans. For example, a database component, will provide a session
bean. And one could have different implementations of this database
component. In order to take advantage of this metaphor is that
components where created. A component, may require other component to
work properly. For example, the DAO components will need the database
component to function properly, no matter who or how is this other
component "implemented"
So, in controlfreak, the application is a bundle of components, wired
together and configuration files for everything that is not beans.
Python Static Beans
When considering different alternatives to inject model objects, we
arrived to a different approach to the one we would took in java.
To inject model objects with services and others beans, one would need
to intercept the model object creation, so as to let the IoC library
to create the object. The ORM is the one usually creating model
objects, so one would need to intercept the model creation there.
When making queries that return hundreds of objects, objects that
won't probably use any of their injected properties, one starts to
wonder if the overhead of creating those objects with IoC makes
sense. But that's a question I don't plan to answer here.
Our solution, doesn't involve injecting model objects, because we end
up injecting model classes. This is what we call static beans. Classes
in python are the same as instances, just objects. So it's pretty easy
to inject them with properties, not on creation, but afterwards.
There's a catch on using class injection, instead of instance
injection. The injected properties must be stateless, not dependant of
the current instance that is calling them. But that's usually the case
with services.
ControlFreak ApplicationContext
The ApplicationContext class is the same as Spring's one. That is, it
is given an application's configuration; and we can use it as a
Bean's Locator using the get() method.
The static beans are initialized at the same time as the
ApplicationContext, cause they inject already created objects such as
class o modules.
All other beans are created on demand, and they are cached for later
use.
Right now the library supports the following kind of beans:
- bean: Constructs and inject a class
- factory: Creates a bean by calling a function (callable)
- alias: An alias of another bean
- map: A map of beans
- list: a list of beans
- static: Inject on already created objects (on ApplicationContext
initialization), such as class or module objects.
Configuration
At this moment the only supported configuration format is yaml files.
There are 3 kinds of yaml files to define:
- components definition
- configuration variables for components
- application definition
Component's File
A component is defined by a name, required config keys, required
components and beans definitions. Several components can be defined
in the same file. That is, a component file is something like:
component-name:
config-keys:
- a-config-key
- another-config-key
required-components:
- other-comp-name
definitions:
# beans definitions
other-component-name:
#same as before
# ...
Inside 'definitions' goes all components beans, next I'll show how to
configure each of the bean's types.
A bean of type 'bean' can have setter or construction injection, so it
goes:
renderer:
type: bean
path: renderers.html.impl:HtmlRenderer
constructor-args:
debug_mode: config:debug_mode
renderer:
type: bean
path: renderers.rest.impl:ReStructuredTextRenderer
properties:
syntax: bean:syntax_rules
The bean's name in each case is 'renderer', one uses constructor
injection, and the other setter injection. The first has a
constructor's keyword argument named 'debug_mode' and the value to set
is the config-key of the component 'debug_mode'. The second bean, has
a property named 'syntax' set with another bean, named
'syntax_rules'. This bean, must be in the same component. If a bean
from another component is needed, first you need to declare the other
component in 'required-components' of the component, and then, when
referring to the bean, you should use it's absolute name, that is:
'comp_name::bean_name'.
A bean of type 'factory', has only constructor-args, so it goes:
syntax_rules:
type: factory
path: renderers.rest.rules:create_rules
constructor-args:
keywords: config:accepted_keywords
If instead of a keyword argument, you want to use positional
arguments, you would place a keyword the position of the argument,
beginning with 0.
A bean of type 'alias' is just a link to another bean, so it goes:
documents_generator:
type: alias
target: simple_generator
A bean of type 'map' or 'list' needs you to define 'properties' as a
dictionary or list depending the case. Examples of this are:
rules_map:
type: map
properties:
rule1: bean:some_rule
rule2: bean:some_other_rule
exceptions_list:
type: list
properties:
- bean:some_exception
- my-string-exception
A bean fo type 'static' has only setter injection, because
controlfreak is not involved in the object creation. For example:
api-accounts:
type: static
path: popserver.controllers.api.accounts:AccountsController
properties:
service_dao: bean:dao::serviceDAO
account_dao: bean:dao::accountDAO
user_dao: bean:dao::userDAO
account_service: bean:pop-services::accountService
Configuration Variables File
A configuration variables file, defines a values for component's
required configuration values. So if we have a component named
'database' that requires a config value 'database_url' the config file
would look like
database:
database_url: postgres://user:password@mydbserver:5432/dbname
In the same configuration file one can place configuration values for
several components.
Application File
An application file, is where we bundle several components files, with
their configuration files. The reason to separate the config values
from the component's bean is because the config values will probably
change more often than the bean's wiring structure. Also, I believe
that is clearer to see configuration values on a separate place
than seeing them within the beans that are, in a way,
more coupled with the programming task itself.
Another thing one can do in the application file is to redefine the
components wiring. That is, to decide witch components will fulfill
the required components of other component. For example, suppose that
we have two components that "implements" the same contract, one it's
called 'dummy-cache' and the other 'standard-cache'. Now, we also have
a component, let's say 'services' that requires a 'cache'
component. The application file would be the place to wire one of the
cache components to the services components: dummy for development,
and standard for production environment.
Another feature of the application file is to have a same component,
configured with two different config values at the same time. Think on
the component as a class definition, that we would want to instantiate
with different values each time. For example, we have a database
component, and we are using two databases, and we need to use the same
component with two different config values. To do this, we define two
aliases for 'database' in application context: 'database1' and
'database2'. In the config file, we will then set the values for 'database1' and
'database2'; and not for 'database'.
An example of an application file is:
includes: #component files to include
- "%(here)s/../../components/general/amazon-mock.yaml"
- "%(here)s/../../components/general/cache.yaml"
- "%(here)s/../../components/general/database.yaml"
- "%(here)s/../../components/general/eye-services.yaml"
- "%(here)s/../../components/general/pop-services.yaml"
- "%(here)s/../../components/general/core-model.yaml"
- "%(here)s/../../components/general/dao.yaml"
- "%(here)s/../../components/general/domains.yaml"
- "%(here)s/../../components/general/extauth-services.yaml"
customization: #aliases and rewiring as explained
cache:
required-components:
cache-customization: dummy-cache-customization
eye-services:
required-components:
item-age-customization: testing-item-age-customization
config: #config files to read
- "%(here)s/../base/config.yaml"
- "%(here)s/config.yaml"
Using the library
To load a control freak set up, one would to something like:
from controlfreak.config.yaml import createApplicationContext
appctx = createApplicationContext('my-app-file.yaml')
To use the Application Context as a bean's locator, one need to give
it the absolute path of a bean, that is, the component and the bean
name together. For example:
session = appctx.get('database::session')
...
This would result on the 'session' bean from the 'database' component.
Final Example
Here's an example of each file:
component file:
reSt-renderer:
config-keys:
- accepted_keywords
definitions:
renderer:
type: bean
path: renderers.rest.impl:ReStructuredTextRenderer
properties:
syntax: bean:syntax_rules
syntax_rules:
type: factory
path: renderers.rest.rules:create_rules
constructor-args:
keywords: config:accepted_keywords
html-renderer:
config-keys:
- debug_mode
definitions:
renderer:
type: bean
path: renderers.html.impl:HtmlRenderer
constructor-args:
debug_mode: config:debug_mode
documents-generator:
required-components:
- renderer
definitions:
documents_generator:
type: alias
target: simple_generator
simple_generator:
type: bean
path: generators.simple:SimpleGenerator
propreties:
renderer: bean:renderer:renderer
complex_generator:
type: bean
path: generators.complex:ComplexGenerator
constructor-args:
rules: bean:rules_map
exceptions: bean:exceptions_list
rules_map:
type: map
properties:
rule1: bean:some_rule
rule2: bean:some_other_rule
exceptions_list:
type: list
properties:
- bean:some_exception
- my-string-exception
some_rule:
type: bean
path: rules:SomeRule
some_other_rule:
type: bean
path: rules:SomeOtherRule
some_exception:
type: bean
path: excpetions:MyGeneratorException
config file:
reSt-renderer:
accepted_keywords:
- hey
- jo
html-renderer:
debug_mode: True
application file:
includes:
- "%(here)s/my-components.yaml"
config:
- "%(here)s/my-config.yaml"
Well, that's all, I hope you will find this IoC library useful!