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!
