Over various pints of beer, emails, and late-night twitter tweets, I’ve alone and with others wondered about whether a smart, well-adjusted programmer would use Pylons or CherryPy for all his web programming needs (and whether such a programmer would take the time to convert from CherryPy to Pylons). Pylons is newish to me, but I’ve been using CherryPy (on and off) for years now. What troubles me about CherryPy is that despite all those years of experience, there are still parts of CherryPy I struggle with (and not just this horrible while-true-except-pass loop). Here are my partially collected thoughts. I’ll start with what bugs me enough about CherryPy for me to seek alternatives.
If you visit the URI /foo/bar/spam, CherryPy looks for a foo.bar controller with a spam action (sort of). There are other mostly irrelevant complexities (mount points for quasi-apps and such), but that’s the gist. You can write your own “dispatcher” in CherryPy to map URLs to executable code, or use ones such as the RoutesDispatcher, which uses Groovie Routes to map URLs to code. But using your own component is always kind of a sour-tasting afterthought in CherryPy and you’re sometimes reminded of that, unpleasantly (for example, routes for static content do not work in RoutesDispatcher).
In version 3, CherryPy’s previously simple configuration system got kind of confusing. I started writing a lot of code like this:
cherrypy.config.update(myConfig) app = cherrypy.tree.mount(None, moreConfig) cherrypy.config.update(someFile) cherrypy.quickstart(app)
CherryPy lets you pass in configuration options a number of ways: In Python: dicts passed to config.update, dicts passed to tree.mount, or dicts on your controller object. In ConfigParser syntax: as filenames passed to config.update or tree.mount. The problem is that I’m always left with trial-and-error in figuring out what options I have; configuration directives don’t work everywhere, and you’re given relatively little guidance finding out where. If the documentation were better, that might help, but it’s confusing no matter what.
Which brings me to my most significant grievance with CherryPy: Its documentation is mediocre and because it takes such a novel approach to so many things, it needs good documentation. Most of CherryPy’s documentation is on a loosely organized wiki that resembles someone’s lecture notes more than it resembles documentation. This makes finding specific pieces of information difficult; for example, the RoutesDispatcher from above is buried deep down on a page called “PageHandlers” and is otherwise mostly unmentioned. Sometimes the wiki looks helpful, such as on this file upload page, but in practice the examples given are apparently built for an old version of the framework (“from cherrypy.lib.filter import basefilter” results in an ImportError).
You can always ask CherryPy’s mailing list, but be forewarned: it’s not very inhabited now that TurboGears moved to Pylons.
Where is CherryPy superior?
After all these gripes, you might think I’m fully convinced that move to Pylons is good. A simple “Hello World” comparison reveals why that’s not the case. In CherryPy:
import cherrypy class HelloWorld(object): def index(self): return "Hello World!" index.exposed = True cherrypy.quickstart(HelloWorld())
In Pylons: … this is going to take some time. Pylons doesn’t let you create a Hello World app in one file. Or even one directory. You start by running this command (for convenience and comic effect, there are scroll bars):
kkinder@kkinder-laptop ~> paster create MyHelloWorldProject -t pylons Selected and implied templates: Pylons#pylons Pylons application template Variables: egg: MyHelloWorldProject package: myhelloworldproject project: MyHelloWorldProject Enter template_engine (mako/genshi/jinja2/etc: Template language) ['mako']: Enter sqlalchemy (True/False: Include SQLAlchemy 0.5 configuration) [False]: Creating template pylons Creating directory ./MyHelloWorldProject Recursing into +package+ Creating ./MyHelloWorldProject/myhelloworldproject/ Copying templates/default_project/+package+/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/__init__.py Recursing into config Creating ./MyHelloWorldProject/myhelloworldproject/config/ Copying templates/default_project/+package+/config/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/__init__.py Copying templates/default_project/+package+/config/deployment.ini_tmpl_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/deployment.ini_tmpl Copying templates/default_project/+package+/config/environment.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/environment.py Copying templates/default_project/+package+/config/middleware.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/middleware.py Copying templates/default_project/+package+/config/routing.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/config/routing.py Recursing into controllers Creating ./MyHelloWorldProject/myhelloworldproject/controllers/ Copying templates/default_project/+package+/controllers/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/controllers/__init__.py Copying templates/default_project/+package+/controllers/error.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/controllers/error.py Recursing into lib Creating ./MyHelloWorldProject/myhelloworldproject/lib/ Copying templates/default_project/+package+/lib/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/lib/__init__.py Copying templates/default_project/+package+/lib/app_globals.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/lib/app_globals.py Copying templates/default_project/+package+/lib/base.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/lib/base.py Copying templates/default_project/+package+/lib/helpers.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/lib/helpers.py Recursing into model Creating ./MyHelloWorldProject/myhelloworldproject/model/ Copying templates/default_project/+package+/model/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/model/__init__.py Recursing into public Creating ./MyHelloWorldProject/myhelloworldproject/public/ Copying templates/default_project/+package+/public/bg.png to ./MyHelloWorldProject/myhelloworldproject/public/bg.png Copying templates/default_project/+package+/public/favicon.ico to ./MyHelloWorldProject/myhelloworldproject/public/favicon.ico Copying templates/default_project/+package+/public/index.html_tmpl to ./MyHelloWorldProject/myhelloworldproject/public/index.html Copying templates/default_project/+package+/public/pylons-logo.gif to ./MyHelloWorldProject/myhelloworldproject/public/pylons-logo.gif Recursing into templates Creating ./MyHelloWorldProject/myhelloworldproject/templates/ Recursing into tests Creating ./MyHelloWorldProject/myhelloworldproject/tests/ Copying templates/default_project/+package+/tests/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/tests/__init__.py Recursing into functional Creating ./MyHelloWorldProject/myhelloworldproject/tests/functional/ Copying templates/default_project/+package+/tests/functional/__init__.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/tests/functional/__init__.py Copying templates/default_project/+package+/tests/test_models.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/tests/test_models.py Copying templates/default_project/+package+/websetup.py_tmpl to ./MyHelloWorldProject/myhelloworldproject/websetup.py Copying templates/default_project/MANIFEST.in_tmpl to ./MyHelloWorldProject/MANIFEST.in Copying templates/default_project/README.txt_tmpl to ./MyHelloWorldProject/README.txt Copying templates/default_project/development.ini_tmpl to ./MyHelloWorldProject/development.ini Recursing into docs Creating ./MyHelloWorldProject/docs/ Copying templates/default_project/docs/index.txt_tmpl to ./MyHelloWorldProject/docs/index.txt Copying templates/default_project/ez_setup.py to ./MyHelloWorldProject/ez_setup.py Copying templates/default_project/setup.cfg_tmpl to ./MyHelloWorldProject/setup.cfg Copying templates/default_project/setup.py_tmpl to ./MyHelloWorldProject/setup.py Copying templates/default_project/test.ini_tmpl to ./MyHelloWorldProject/test.ini Running /usr/bin/python setup.py egg_info
I of course went for the lighter option that doesn’t include SQLAlchemy. Okay, we’re almost there. Now I’m going to create a Hello controller:
kkinder@kkinder-laptop ~/MyHelloWorldProject> paster controller Hello Creating /home/kkinder/MyHelloWorldProject/myhelloworldproject/controllers/Hello.py Creating /home/kkinder/MyHelloWorldProject/myhelloworldproject/tests/functional/test_Hello.py
This is where Pylons READS MY MIND and does the work for me. Take a look at the new Hello.py controller it made for me:
import logging from pylons import request, response, session, tmpl_context as c from pylons.controllers.util import abort, redirect_to from myhelloworldproject.lib.base import BaseController, render log = logging.getLogger(__name__) class HelloController(BaseController): def index(self): # Return a rendered template #return render('/Hello.mako') # or, return a response return 'Hello World'
But I’m not done yet. Now I need to edit my routing.py file, check my development.ini for any settings, run “setup.py develop” and then use “paster serve” to finally test my app. By the time I’m done printing “Hello World”, I have 42 files in 11 directories:
. |-- MANIFEST.in |-- MyHelloWorldProject.egg-info | |-- PKG-INFO | |-- SOURCES.txt | |-- dependency_links.txt | |-- entry_points.txt | |-- not-zip-safe | |-- paster_plugins.txt | |-- requires.txt | `-- top_level.txt |-- README.txt |-- development.ini |-- docs | `-- index.txt |-- ez_setup.py |-- myhelloworldproject | |-- __init__.py | |-- __init__.pyc | |-- config | | |-- __init__.py | | |-- deployment.ini_tmpl | | |-- environment.py | | |-- middleware.py | | `-- routing.py | |-- controllers | | |-- Hello.py | | |-- __init__.py | | `-- error.py | |-- lib | | |-- __init__.py | | |-- __init__.pyc | | |-- app_globals.py | | |-- base.py | | |-- base.pyc | | `-- helpers.py | |-- model | | `-- __init__.py | |-- public | | |-- bg.png | | |-- favicon.ico | | |-- index.html | | `-- pylons-logo.gif | |-- templates | |-- tests | | |-- __init__.py | | |-- functional | | | |-- __init__.py | | | `-- test_Hello.py | | `-- test_models.py | `-- websetup.py |-- setup.cfg |-- setup.py `-- test.ini 11 directories, 42 files
The Django/Pylons/TurboGears frameworks all follow a teeth gnashing design pattern that came out of Java and was made ugly in Ruby: Code generation. This is just my opinion, but in a language as dynamic and meta-programmative as Python, code generation is obsolete at best. What happens when the basic layout for a project changes a bit in the next version of Pylons? Do I “diff” the paste template and compare it to the old version to see what changed? And half of the configuration is in Pylon’s WSGI-obsessed middleware.py file, which is itself both a product of code generation and subsequent edits by the developer:
"""Pylons middleware initialization""" from beaker.middleware import CacheMiddleware, SessionMiddleware from paste.cascade import Cascade from paste.registry import RegistryManager from paste.urlparser import StaticURLParser from paste.deploy.converters import asbool from pylons import config from pylons.middleware import ErrorHandler, StatusCodeRedirect from pylons.wsgiapp import PylonsApp from routes.middleware import RoutesMiddleware from myhelloworldproject.config.environment import load_environment def make_app(global_conf, full_stack=True, static_files=True, **app_conf): """Create a Pylons WSGI application and return it ``global_conf`` The inherited configuration for this application. Normally from the [DEFAULT] section of the Paste ini file. ``full_stack`` Whether this application provides a full WSGI stack (by default, meaning it handles its own exceptions and errors). Disable full_stack when this application is "managed" by another WSGI middleware. ``static_files`` Whether this application serves its own static files; disable when another web server is responsible for serving them. ``app_conf`` The application's local configuration. Normally specified in the [app:<name>] section of the Paste ini file (where <name> defaults to main). """ # Configure the Pylons environment load_environment(global_conf, app_conf) # The Pylons WSGI app app = PylonsApp() # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map']) app = SessionMiddleware(app, config) app = CacheMiddleware(app, config) # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) # Establish the Registry for this application app = RegistryManager(app) if asbool(static_files): # Serve static files static_app = StaticURLParser(config['pylons.paths']['static_files']) app = Cascade([static_app, app]) return app
Beyond its sanity in not generating piles of code, CherryPy has other advantages. Its testing framework is very easy to use, but and also documented with almost no examples. Pylons, in contrast, tells you where to put the tests but doesn’t make it quite as easy to run them.
But there are places where Pylons is amazing…
One more thing: What’s missing from CherryPy?
Pylons error handling is the one killer feature it has that CherryPy just lacks. Skip to line 598 of the “Hello, World” pylons project and try throwing an error:
def index(self): raise ValueError
Then use the controller. In CherryPy, I would get a Python traceback wrapped in a <pre> tag. In Pylons, I get an ajax debugger with expression evaluation and variable inspection:
I can inspect variables and evaluate expressions at each point in the traceback:
Along with view environment info, complete source code of running file and template, and more.
Wrapping it up
I normally would have a strong aversion to “glue” frameworks that try (usually badly) to integrate unrelated toolsets. Having said that, when I sit down and build my own framework in CherryPy, I want to use SQLAlchemy, Groovie Routes, and Mako templates. (Pylons’ “webhelpers” are nice too.)
Pylons offers all those those tools in a bundle. Also, its mailing list is far more active. While CherryPy manages a post or two a day, Pylons has a high quality post every hour or more. I’m not saying I’m giving up on CherryPy, but if I started a project today, I would seriously consider Pylons as a more friendly alternative, even if it relies on code generation.