Python/Pylons/What's Happening

From Jonathan Gardner's Tech Wiki
< Python‎ | Pylons
Jump to: navigation, search

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
Courtesy of the bash manpage:
_      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!