« Sorry about the recent outagesURL-rewriting in CherryPy 2.1 »

WSGI wrapper for mod_python

11/06/05

Permalink 01:18:00 am, by fumanchu Email , 879 words   English (US)
Categories: Python, WSGI

WSGI wrapper for mod_python

Update Nov 6, 2005: Finally got it to work with Apache2-prefork on Unix (it only worked on mpm_winnt until now).

Update Oct 25, 2005: I was having a problem setting up a new install of my CherryPy application, using this recipe. It turned out that I didn't have the right interpreter_name in my PythonImport directive:

PythonImport module interpreter_name

Therefore, the CherryPy server started in a different intepreter than the one being used for the requests. It must exactly match the value of req.interpreter, and is case-sensitive. I've updated the code with comments to that effect (just to have it all in one place).

Update Aug 11, 2005: I was having a problem serving .css and .js pages. CherryPy's standalone WSGI server did fine, but mod_python did not. I finally tracked it down to the fact that I was both setting apache's req.status and returning req.status from the handler. Funky. It worked when I chose to simply return the value, and not set it.

Update June 5, 2005:

  1. Pages take forever to terminate when returning a status of 200--apache.OK must be returned instead in that case.

  2. Added code for hotshot profiling.

  3. Added code for using paste.lint

As I mentioned I was doing last week, I wrote a more complete WSGI wrapper for modpython. Here it is. Feedback welcome. Phil Eby told me he'd like a mod_python wrapper for inclusion in wsgiref; he should feel free to use this one however he sees fit. ;)


"""
WSGI wrapper for mod_python. Requires Python 2.2 or greater.


Example httpd.conf section for a CherryPy app called "mcontrol":

<Directory D:\htdocs\mcontrol>
    SetHandler python-program
    PythonHandler wsgiref.modpy_wrapper::handler
    PythonOption application cherrypy.wsgiapp::wsgiApp
    PythonOption import mcontrol.cherry::startup
</Directory>

"""

import sys
from mod_python import apache
from wsgiref.handlers import BaseCGIHandler


class InputWrapper(object):
    
    def __init__(self, req):
        self.req = req
    
    def close(self):
        pass
    
    def read(self, size=-1):
        return self.req.read(size)
    
    def readline(self):
        return self.req.readline()
    
    def readlines(self, hint=-1):
        return self.req.readlines(hint)
    
    def __iter__(self):
        line = self.readline()
        while line:
            yield line
            # Notice this won't prefetch the next line; it only
            # gets called if the generator is resumed.
            line = self.readline()


class ErrorWrapper(object):
    
    def __init__(self, req):
        self.req = req
    
    def flush(self):
        pass
    
    def write(self, msg):
        self.req.log_error(msg)
    
    def writelines(self, seq):
        self.write(''.join(seq))


bad_value = ("You must provide a PythonOption '%s', either 'on' or 'off', "
             "when running a version of mod_python < 3.1")

class Handler(BaseCGIHandler):
    
    def __init__(self, req):
        options = req.get_options()
        
        # Threading and forking
        try:
            q = apache.mpm_query
        except AttributeError:
            threaded = options.get('multithread', '').lower()
            if threaded == 'on':
                threaded = True
            elif threaded == 'off':
                threaded = False
            else:
                raise ValueError(bad_value % "multithread")
            
            forked = options.get('multiprocess', '').lower()
            if forked == 'on':
                forked = True
            elif forked == 'off':
                forked = False
            else:
                raise ValueError(bad_value % "multiprocess")
        else:
            threaded = q(apache.AP_MPMQ_IS_THREADED)
            forked = q(apache.AP_MPMQ_IS_FORKED)
        
        env = dict(apache.build_cgi_env(req))
        
        if req.headers_in.has_key("authorization"):
            env["HTTP_AUTHORIZATION"] = req.headers_in["authorization"]
        
        BaseCGIHandler.__init__(self,
                                stdin=InputWrapper(req),
                                stdout=None,
                                stderr=ErrorWrapper(req),
                                environ=env,
                                multiprocess=forked,
                                multithread=threaded
                                )
        self.request = req
        self._write = req.write
    
    def _flush(self):
        pass
    
    def send_headers(self):
        self.cleanup_headers()
        self.headers_sent = True
        # Can't just return 200 or the page will hang until timeout
        s = int(self.status[:3])
        if s == 200:
            self.finalstatus = apache.OK
        else:
            self.finalstatus = s
        # the headers.Headers class doesn't have an iteritems method...
        for key, val in self.headers.items():
            if key.lower() == 'content-length':
                if val is not None:
                    self.request.set_content_length(int(val))
            elif key.lower() == 'content-type':
                self.request.content_type = val
            else:
                self.request.headers_out[key] = val


_counter = 0

def profile(req):
    # Call this function instead of handler
    # to get profiling data for each call.
    import hotshot, os.path
    ppath = os.path.dirname(__file__)
    if not os.path.exists(ppath):
        os.makedirs(ppath)
    global _counter
    _counter += 1
    ppath = os.path.join(ppath, "cp_%s.prof" % _counter)
    prof = hotshot.Profile(ppath)
    result = prof.runcall(handler, req)
    prof.close()
    return result

def handler(req):
    config = req.get_config()
    debug = int(config.get("PythonDebug", 0))
    
    options = req.get_options()
    
    # Because PythonImport cannot be specified per Directory or Location,
    # take any 'import' PythonOption's and import them. If a function name
    # in that module is provided (after the "::"), it will be called with
    # the request as an argument. The module and function, if any, should
    # be re-entrant (i.e., handle multiple threads), and, since they will
    # be called per request, must be designed to run setup code only on the
    # first request (a global 'first_request' flag is usually enough).
    import_opt = options.get('import')
    if import_opt:
        atoms = import_opt.split('::', 1)
        modname = atoms.pop(0)
        module = __import__(modname, globals(), locals(), [''])
        if atoms:
            func = getattr(module, atoms[0])
            func(req)
    
    # Import the wsgi 'application' callable and pass it to Handler.run
    modname, objname = options['application'].split('::', 1)
    module = __import__(modname, globals(), locals(), [''])
    app = getattr(module, objname)
    
    h = Handler(req)
##    from paste import lint
##    app = lint.middleware(app)
    h.run(app)
    
    # finalstatus was set in Handler.send_headers()
    return h.finalstatus

7 comments

Comment from: Adrian Holovaty [Visitor] · http://www.holovaty.com/

Thanks very much for this. Are you interested in working with me to get this to work with Paste? Please contact me to chat about it. (I couldn't find your e-mail address on this site.)

05/31/05 @ 15:33
Comment from: alikat [Member] Email

Bob - it's really great to understand now - what all this means...not that I really understand any of it - but now I understand why you post it. Thanks.

06/06/05 @ 13:52
Comment from: Nicolas Lehuen [Visitor] · http://nicolas.lehuen.com/

Hi, I'm on the developers team at mod_python, and I've found your implementation of the WSGI spec for mod_python. I've seen that there are requests to integrate your code in application frameworks, but would not it be more logical to integrate it into mod_python ? I'm just speaking on my behalf for now, but would you consider donating this code to the mod_python community (in exchange of due reference and huge thanks) ? Contact me if you're interested. Thanks !

06/08/05 @ 15:43
Comment from: Leandro Lucarella [Visitor] · http://www.mazziblog.com.ar/blog/

It would be really great to include a wsgi handler in mod_python distribution!

07/02/05 @ 13:25
Comment from: fumanchu [Member] Email

Leandro,

Yes, it would be great! I was contacted a few weeks ago by the mod_python team, and have given them full permission to use my handler in the standard distribution. I'm just waiting now for it to show up on their site. ;)

07/03/05 @ 13:36
Comment from: Lethalman [Visitor] · http://www.lethalman.net

I get Segmantation fault in apache logs. I'm using apache 2.0.49, Python 2.4.2, CherryPy 2.1.0, ModPython 3.1.4... it happens when cherrypy is imported.
In fact i tried to do a simple "import cherrypy" in modpython_handler.handler function and i get segfault. Imports work with other modules, what about?

11/09/05 @ 02:59
Comment from: fumanchu [Member] Email

Lethalman,

If you could make a ticket on the CherryPy Trac site, it would be a big help. http://www.cherrypy.org/newticket Please include the full text of the log when the segfault occurs.

11/09/05 @ 09:04

Leave a comment


Your email address will not be revealed on this site.

Your URL will be displayed.

Please enter the phrase "I am a real human." in the textbox above.
(Line breaks become <br />)
(Name, email & website)
(Allow users to contact you through a message form (your email will not be revealed.)
December 2016
Sun Mon Tue Wed Thu Fri Sat
 << <   > >>
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31

Search

The requested Blog doesn't exist any more!

XML Feeds

open source blog tool