Concepts of Pylons¶
Understanding the basic concepts of Pylons, the flow of a request and response through the stack and how Pylons operates makes it easier to customize when needed, in addition to clearing up misunderstandings about why things behave the way they do.
This section acts as a basic introduction to the concept of a WSGI application, and WSGI Middleware in addition to showing how Pylons utilizes them to assemble a complete working web framework.
To follow along with the explanations below, create a project following the Getting Started Guide.
The ‘Why’ of a Pylons Project¶
A new Pylons project works a little differently than in many other web frameworks. Rather than loading the framework, which then finds a new projects code and runs it, Pylons creates a Python package that does the opposite. That is, when its run, it imports objects from Pylons, assembles the WSGI Application and stack, and returns it.
If desired, a new project could be completely cleared of the Pylons imports and run any arbitrary WSGI application instead. This is done for a greater degree of freedom and flexibility in building a web application that works the way the developer needs it to.
By default, the project is configured to use standard components that most developers will need, such as sessions, template engines, caching, high level request and response objects, and an ORM. By having it all setup in the project (rather than hidden away in ‘framework’ code), the developer is free to tweak and customize as needed.
In this manner, Pylons has setup a project with its opinion of what may be needed by the developer, but the developer is free to use the tools needed to accomplish the projects goals. Pylons offers an unprecedented level of customization by exposing its functionality through the project while still maintaining a remarkable amount of simplicity by retaining a single standard interface between core components (WSGI).
WSGI Applications¶
WSGI is a basic specification known as PEP 333, that describes a method for interacting with a HTTP server. This involves a way to get access to HTTP headers from the request, and how set HTTP headers and return content on the way back out.
A ‘Hello World’ WSGI Application:
def simple_app(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])
return ['<html><body>Hello World</body></html>']
This WSGI application does nothing but set a 200 status code for the response, set the HTTP ‘Content-type’ header, and return some HTML.
The WSGI specification lays out a set of keys that will be set in the environ dict.
The WSGI interface, that is, this method of calling a function (or method of a class) with two arguments, and handling a response as shown above, is used throughout Pylons as a standard interface for passing control to the next component.
Inside a new project’s config/middleware.py
, the make_app function is
responsible for creating a WSGI application, wrapping it in WSGI middleware
(explained below) and returning it so that it may handle requests from a
HTTP server.
WSGI Middleware¶
Within config/middleware.py
a Pylons application is wrapped in successive layers which add functionality. The process of wrapping the Pylons application in middleware results in a structure conceptually similar to the layers in an onion.
Once the middleware has been used to wrap the Pylons application, the make_app function returns the completed app with the following structure (outermost layer listed first):
Registry Manager
Status Code Redirect
Error Handler
Cache Middleware
Session Middleware
Routes Middleware
Pylons App (WSGI Application)
WSGI middleware is used extensively in Pylons to add functionality to the
base WSGI application. In Pylons, the ‘base’ WSGI Application is the
PylonsApp
. It’s responsible for looking in the
environ dict that was passed in (from the Routes Middleware).
To see how this functionality is created, consider a small class that looks at the HTTP_REFERER header to see if it’s Google:
class GoogleRefMiddleware(object):
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
environ['google'] = False
if 'HTTP_REFERER' in environ:
if environ['HTTP_REFERER'].startswith('http://google.com'):
environ['google'] = True
return self.app(environ, start_response)
This is considered WSGI Middleware as it still can be called and returns like a WSGI Application, however, it’s adding something to environ, and then calls a WSGI Application that it is initialized with. That’s how the layers are built up in the WSGI Stack that is configured for a new Pylons project.
Some of the layers, like the Session, Routes, and Cache middleware, only add objects to the environ dict, or add HTTP headers to the response (the Session middleware for example adds the session cookie header). Others, such as the Status Code Redirect, and the Error Handler may fully intercept the request entirely, and change how it’s responded to.
Controller Dispatch¶
When the request passes down the middleware, the incoming URL gets parsed in
the RoutesMiddleware, and if it matches a URL (See URL Configuration), the
information about the controller that should be called is put into the environ dict for use by PylonsApp
.
The PylonsApp
then attempts to find a controller in the controllers
directory that matches the name of the controller, and searches for a class
inside it by a similar scheme (controller name + ‘Controller’, ie,
HelloController). Upon finding a controller, its then called like any other
WSGI application using the same WSGI interface that
PylonsApp
was called with.
New in version 1.0: Controller name can also be a dotted path to the module / callable that should be imported and called. For example, to use a controller named ‘Foo’ that is in the ‘bar.controllers’ package, the controller name would be bar.controllers:Foo.
This is why the BaseController that resides in a project’s
lib/base.py
module inherits from
WSGIController
and has a __call__
method that takes the environ and start_response. The
WSGIController
locates a method in the
class that corresponds to the action that Routes found, calls it, and
returns the response completing the request.
Paster¶
Running the paster command all by itself will show the sets of commands it accepts:
$ paster
Usage: paster [paster_options] COMMAND [command_options]
Options:
--version show program's version number and exit
--plugin=PLUGINS Add a plugin to the list of commands (plugins are Egg
specs; will also require() the Egg)
-h, --help Show this help message
Commands:
create Create the file layout for a Python distribution
grep Search project for symbol
help Display help
make-config Install a package and create a fresh config file/directory
points Show information about entry points
post Run a request for the described application
request Run a request for the described application
serve Serve the described application
setup-app Setup an application, given a config file
pylons:
controller Create a Controller and accompanying functional test
restcontroller Create a REST Controller and accompanying functional test
shell Open an interactive shell with the Pylons app loaded
If paster is run inside of a Pylons project, this should be the output that will be printed. The last section, pylons will be absent if it is not run inside a Pylons project. This is due to a dynamic plugin system the paster script uses, to determine what sets of commands should be made available.
Inside a Pylons project, there is a directory ending in .egg-info, that has
a paster_plugins.txt
file in it. This file is looked for and read by
the paster script, to determine what other packages should be
searched dynamically for commands. Pylons makes several commands available
for use in a Pylons project, as shown above.
Loading the Application¶
Running (and thus loading) an application is done using the paster command:
$ paster serve development.ini
This instructs the paster script to go into a ‘serve’ mode. It will attempt to load both a server and a WSGI application that should be served, by parsing the configuration file specified. It looks for a [server] block to determine what server to use, and an [app] block for what WSGI application should be used.
The basic egg block in the development.ini
for a helloworld project:
[app:main]
use = egg:helloworld
That will tell paster that it should load the helloworld egg to locate
a WSGI application. A new Pylons application includes a line in the
setup.py
that indicates what function should be called to make the
WSGI application:
entry_points="""
[paste.app_factory]
main = helloworld.config.middleware:make_app
[paste.app_install]
main = pylons.util:PylonsInstaller
""",
Here, the make_app function is specified as the main WSGI application that Paste (the package that paster comes from) should use.
The make_app function from the project is then called, and the server (by default, a HTTP server) runs the WSGI application.