Python/Pylons/What's Happening
Contents
Starting up the website
/usr/bin/paster
Serve up the website with a command like the following:
paster server development.ini
What does this do? It calls the paster script.
The paster script is pretty simple:
#!/usr/bin/python # EASY-INSTALL-ENTRY-SCRIPT: 'PasteScript==1.3.6dev-r6755','console_scripts','paster' __requires__ = 'PasteScript==1.3.6dev-r6755' import sys from pkg_resources import load_entry_point sys.exit( load_entry_point('PasteScript==1.3.6dev-r6755', 'console_scripts', 'paster')() )
Pretty basic. What does load_entry_point do? You can take a look at that in the pkg_resources.py file. (It is found zipped up in your setuptools egg.) The short answer is that it calls the paster script that lives in EGG-INFO/scripts within the PasteDeploy egg.
PasteDeploy/EGG-INFO/scripts/paster
This is the paster script in the PasteDeploy egg:
#!/usr/bin/python import os import sys try: here = __file__ except NameError: # Python 2.2 here = sys.argv[0] relative_paste = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(here))), 'paste') if os.path.exists(relative_paste): sys.path.insert(0, os.path.dirname(relative_paste)) from paste.script import command command.run()
This is a bit more sensical than the setuptools magic. However, it is still a bit nonsensical in that we have to muck with sys.path (as if setuptools wasn't crazy enough.) But it actually isn't so hard to understand this.
The core is the last two lines: execute paste.script.command.run(). Pretty simple, huh?
PasteScript/paste/script/command.py
What does command.run() do?
def run(args=None): if (not args and len(sys.argv) >= 2 and os.environ.get('_') and sys.argv[0] != os.environ['_'] and os.environ['_'] == sys.argv[1]): # probably it's an exe execution args = ['exe', os.environ['_']] + sys.argv[2:] if args is None: args = sys.argv[1:] options, args = parser.parse_args(args) options.base_parser = parser system_plugins.extend(options.plugins or []) commands = get_commands() if options.do_help: args = ['help'] + args if not args: print 'Usage: %s COMMAND' % sys.argv[0] args = ['help'] command_name = args[0] if command_name not in commands: command = NotFoundCommand else: command = commands[command_name].load() invoke(command, command_name, options, args[1:])
Has anybody ever heard of comments?
Regardless, this is what we have.
def run(args=None):
We're getting called with no args, so args is None.
if (not args and len(sys.argv) >= 2 and os.environ.get('_') and sys.argv[0] != os.environ['_'] and os.environ['_'] == sys.argv[1]):
Whew! That's a mouthful. Don't write code like this, kids. This is basically if (A and B and C and D and E): All of these have to be true for this if to get executed.
- A: not args: True, because args is None.
- B: len(sys.argv) >= 2: True, because sys.argv is <tt>['/usr/bin/paster', 'serve', 'development.ini']. (Remember how we all got started?)
- C: os.environ.get('_'): '/usr/bin/python', or whatever python we are actually running.
- D: sys.argv[0] != os.environ['_']: True
- E: os.environ['_'] == sys.argv[1]: False
_ At shell startup, set to the absolute pathname used to invoke the shell or shell script being executed as passed in the envi- ronment or argument list. Subsequently, expands to the last argument to the previous command, after expansion. Also set to the full pathname used to invoke each command executed and placed in the environment exported to that command. When check- ing mail, this parameter holds the name of the mail file cur- rently being checked.
This if block fails. What about the next one?
if args is None: args = sys.argv[1:]
This just sets a default value for args -- now it is ['serve', 'development.ini'].
options, args = parser.parse_args(args)
Let's actually parse the args. The parse was setup earlier with this code:
parser = optparse.OptionParser(add_help_option=False, version='%s from %s (python %s)' % (dist, dist.location, python_version), usage='%prog [paster_options] COMMAND [command_options]') parser.add_option( '--plugin', action='append', dest='plugins', help="Add a plugin to the list of commands (plugins are Egg specs; will also require() the Egg)") parser.add_option( '-h', '--help', action='store_true', dest='do_help', help="Show this help message") parser.disable_interspersed_args()
What we get out of the args parse is options set with do_help and plugins set to None. args is unchanged.
options.base_parser = parser
Remembering the parser we used for some reason.
system_plugins.extend(options.plugins or [])
system_plugins is an empty list, initialized earlier. Now it is either going to have the plugins specified by options or nothing for its contents. In our case, it's nothing.
commands = get_commands()
You can dig into this if you like. The commands you get back are:
{'controller': EntryPoint.parse('controller = pylons.commands:ControllerCommand'), 'create': EntryPoint.parse('create = paste.script.create_distro:CreateDistroCommand [templating]'), 'exe': EntryPoint.parse('exe = paste.script.exe:ExeCommand'), 'grep': EntryPoint.parse('grep = paste.script.grep:GrepCommand'), 'help': EntryPoint.parse('help = paste.script.help:HelpCommand'), 'make-config': EntryPoint.parse('make-config = paste.script.appinstall:MakeConfigCommand'), 'points': EntryPoint.parse('points = paste.script.entrypoints:EntryPointCommand'), 'restcontroller': EntryPoint.parse('restcontroller = pylons.commands:RestControllerCommand'), 'serve': EntryPoint.parse('serve = paste.script.serve:ServeCommand [config]'), 'setup-app': EntryPoint.parse('setup-app = paste.script.appinstall:SetupCommand'), 'shell': EntryPoint.parse('shell = pylons.commands:ShellCommand')}
The next bits determine which command to run. The command is run in two stages:
command = commands[command_name].load()
The command is "loaded", and the return value stored. In our case, we get a <class 'paste.script.serve.ServeCommand'>.
invoke(command, command_name, options, args[1:])
The command is "invoked" with the relevant parameters. The invoke function just wraps this line with exception handling code:
runner = command(command_name) exit_code = runner.run(args)
The former instantiates an object. The latter calls the run routine for Command objects.
It collects the arguments specified and does what you asked the paster script to do from the beginning: serve development.ini!
The serve command lives in (wait for it!) paste.script.serve.py. What does it do?
Well, it collects a bunch of arguments beyond what commmand.run() did. But the important bits--the bits that really get our server running--look like this:
server = self.loadserver(server_spec, name=server_name, relative_to=base, global_conf=vars) app = self.loadapp(app_spec, name=app_name, relative_to=base, global_conf=vars) server(app)
So, let's get to the bottom of this. What's server? It's what is given back by self.loadserver, which is really a front for paste.deploy.loadserver. What's app? It's what is given back by self.loadapp, yet another front to paste.deploy.loadapp.
Looking at paste/deploy/__init__.py, I see this:
from loadwsgi import *
And in paste/deploy/loadwsgi.py, I see this:
def loadapp(uri, name=None, **kw): return loadobj(APP, uri, name=name, **kw) def loadserver(uri, name=None, **kw): return loadobj(SERVER, uri, name=name, **kw)
And that's as far as I've gotten. What a mess!