Random Tip: Designing for extensibility...

2 views
Skip to first unread message

Adam Jones

unread,
Nov 18, 2006, 10:30:30 PM11/18/06
to TurboGears
Well, came up with something kind of useful that I thought I would
share with the groups. One of the hidden gems of TG is that it is
actually almost ridiculously easy to make your applications extensible.
Plugins, Themes, Extensions, entire new site components, you name it TG
probably has an easy way to make it happen. (and that easy way probably
uses setuptools to do the dirty work.)

Here, let's take a look at an easy way to make your application
themeable. Although you can get a lot done with just CSS, I am going to
be showing how to replace templates to accomplish theming as it is more
useful. I'll be linking to useful resources if I am covering anything
that might not be immediately easy to grasp. All of these examples come
from a webmail project I am working on (currently dubbed ThoughtMail).

The first thing we are going to do is make a change to our setup.py
file that declares (and fills) an entry point for themes. (for more
info see the first section of:
http://docs.turbogears.org/1.0/CommandPlugins)

Here is a sample entry point:

"""
[thoughtmail.themes]
default = thoughtmail.themepackages:Default
"""

Briefly what we are doing here is declaring a name (thoughtmail.themes)
that we will use later to have setuptools look for code that is
available to use. We are also informing setuptools that the "Default"
class in the "thoughtmail.themepackages" module should be included in
the list for "thoughtmail.themes" and named "default".

Now we are just a quick package registration (python setup.py develop)
away from getting started.

Since I don't want to keep a list of every template name, lets cheat a
bit. In the themepackages.py file we'll build our Default class so all
it does is put the appropriate namespace on the name it is given. Here
is what it looks like:

class Default:
desc = "Default theme for ThoughtMail" # This is not required by
setuptools, but is useful.
def __getattr__(self, item):
"""
Overload getattr so calls to Default().attr return
"kid:thoughtmail.templates.default.attr"
"""
if item[0]=='_':
return item
return "kid:thoughtmail.templates.default." + item

So a call to Default().index will return
"kid:thoughtmail.templates.default.index".

This is only half the equation though, as we still need to allow our
users to select a theme. One way of solving that is to rely on a
combination of our previously defined entry points and the TurboGears
config system. When it is started ThoughtMail will look for a
thoughtmail.theme config item grab a list of available themes through
setuptools, and use the match if it is found. Here is the code that
does that:

import turbogears.config
import pkg_resources
class ThoughtMail:
def __init__(self):
get = turbogears.config.get
# look for any provided theme libraries, if none are given use
the default
theme = get("thoughtmail.theme",None)
if not theme:
for entrypoint in
pkg_resources.iter_entry_points("thoughtmail.themes"):
if entrypoint.name == theme:
my = entrypoint.load()
else:
my =
pkg_resources.load_entry_point('thoughtmail','thoughtmail.themes','default')
self.theme = my()

Here's the important bits:
We are using turbogears.config.get to retrieve the value (if any) of
thoughtmail.theme from our application's config files. Then, if we
found a match the relevant class is imported and set to the wonderfully
descriptive (but usefully short) name 'my'. The above mentioned
'Default' theme is used if we can't find anything else.

Now that that is done we are just about home free. Since I am storing
this information as a member variable of the ThoughtMail class, it will
normally be invisible to @expose. One way around this is to set the
template in the returned dictionary through the 'tg_template' key. Here
is a sample index using this method.

@expose()
def index(self):
return dict(data="foo", tg_template=self.templates.index)

Now the template for the index page will be set as though you had used
@expose to directly reference the template library for the theme you
are using. This is kind of messy though, and I would hate to add this
tg_template key to each of my methods, so lets cheat, again.

def tdict(self, name, **kw):
"""
Create a dictionary with the given args and a tg_template key set
to 'name'
"""
return dict(tg_template=name).update(kw)

Now we can change our index to make it a little shorter:

@expose()
def index(self):
return tdict(self.templates.index, data="foo")

You can possibly get something shorter as well, if you want to spend
the effort writing a custom decorator.

Writing a new theme package now becomes a matter of creating an egg
containing your theme files, classes that point to those files, and
some entry_point listings that point to those classes.

That is pretty much it. A smattering of little bits of code in a few
places and all someone has to do is install a theme library and set a
config variable to change the look of your application. The nice part
about this solution is that, since it is all done at runtime, it is
pretty easy to allow for per-user themes as well.

Although the above code only deals with themes; plugins and extensions
are conceptually similar. You define an entry point and find a way to
choose a specific item out of the list for that entry point, and it is
pretty much strait Python classes from there.

Hope you enjoyed it,

-Adam

Ian Charnas

unread,
Nov 19, 2006, 1:06:34 AM11/19/06
to TurboGears
Adam, thanks for sharing that! I think I'll use that concept in the
CMS I'm working on. After a quick glance though: in the 'init' method
for ThoughtMail, shouldn't that say "if theme:" instead of "if not
theme:" ??? Perhaps I read the code the wrong way?

thanks again!
-ian

Adam Jones

unread,
Nov 19, 2006, 4:10:56 AM11/19/06
to TurboGears
Yes, that should be "if theme:" I was paring down the full class into
this example and evidently messed up the translation a bit.

Lee McFadden

unread,
Nov 19, 2006, 7:57:27 AM11/19/06
to turbo...@googlegroups.com
Excellent work Adam. Do you mind if this gets added to docs.turbogears.org?

Lee

On 11/19/06, Adam Jones <ajo...@gmail.com> wrote:
>
> Yes, that should be "if theme:" I was paring down the full class into
> this example and evidently messed up the translation a bit.
>
>
> >
>


--
Lee McFadden

blog: http://www.splee.co.uk
work: http://fireflisystems.com

Adam Jones

unread,
Nov 19, 2006, 12:13:36 PM11/19/06
to TurboGears

Lee McFadden wrote:
> Excellent work Adam. Do you mind if this gets added to docs.turbogears.org?

No, I don't mind. It could probably be less chatty, and it would be
nice to have a separate page explaining just how entry_points are (or
can be, as the case may be) used in TurboGears. I'll work on it later
today.

-Adam

Reply all
Reply to author
Forward
0 new messages