[Web-SIG] [WSGI] mod_python wrapper: minimal first attempt

Robert Brewer fumanchu at amor.org
Thu Oct 14 23:49:29 CEST 2004


Phillip J. Eby wrote:
> At 10:13 PM 10/13/04 -0700, Robert Brewer wrote:
> >Phillip J. Eby wrote:
> > >      def wsgi_handler(req):
> > >          handler = ModPyHandler(req)
> > >          options = req.get_options()
> > >          appmod,appname = options['application'].split('::')
> > >          d = {}
> > >          exec ("from %(appmod)s import %(appname) as 
> application" %
> > > locals()) in d
> > >          handler.run(d[application])
> > >          from mod_python import apache
> > >          return apache.OK
> >
> >Eeew. exec. Smelly. :)
> 
> The "correct" way to do it would be to swipe whatever code mod_python 
> itself uses for that, although I wouldn't be surprised if it 
> uses exec also.  :)
> 
> More likely, it uses '__import__', but for the prototype 
> version, why bother?

Because it's easy:

def wsgi_handler(req):
    from mod_python import apache
    
    handler = ModPyHandler(req)
    options = req.get_options()
    modname, objname = options['application'].split('::')
    module = apache.import_module(modname, autoreload=False, log=debug)
    app = apache.resolve_object(module, objname, arg=None, silent=False)
    handler.run(app)
    return apache.OK

> >I'll stick with plain Python code over
> >PythonOption, thanks, and make my app developers do a few 
> lines of extra
> >work *once* instead of every deployer on every install.
> 
> I'm confused.  One of the main points of WSGI is to "write once, run 
> anywhere".  Assuming most WSGI apps end up as a callable that can be 
> imported from somewhere, then the path of least resistance 
> for a deployer 
> is to be able to pop an extra line or two in an .htaccess or 
> httpd.conf.  They're going to have to touch that file anyway, 
> even to set 
> up a wrapper script.  Why should they have to edit the 
> configuration *and* 
> write a script?  Especially if they're just deploying the 
> app.  That makes 
> no sense to me at all.  Likewise, it makes no sense to have 
> the application 
> developer have to write a mod_python wrapper for their WSGI 
> applications, 
> since they might not have or care about mod_python specifically.

You're right. It was just one more level of indirection and my brain was
on overload with all the callbacks, etc. Turns out I can take what would
have been a handler:

def handler(req):

...and change it to:

def get_wsgi_app(environ, start_response):

...and make that my "application" callable.

> >         if req.path_info:
> >             env["SCRIPT_NAME"] = req.uri[:-len(req.path_info)]
> >         else:
> >             env["SCRIPT_NAME"] = req.uri
> 
> Does the 'req.uri' attribute include a query string?

The docs say "uri: The path portion of the URI." Helpful.

I'd guess req.uri does not include query string, since path_info comes
before query args. I copied the above 4 lines from mod_python/apache.py

> 
> >         # you may want to comment this out for better security
> 
> No, you don't want to.  :)  If you don't trust the WSGI app, 
> you shouldn't run it.  It would be trivial for it to inspect
> Python stack frames until it finds the request object and pull
> out the authorization on its own.  So, it might give someone a
> warm fuzzy feeling to take it out, it won't really help anything.

That comment was also copied from mod_python.

I realized you don't need a separate handler function called
wsgi_handler; mod_python is smart enough to notice when your handler is
an unbound class method, and automatically forms an instance of your
class (passing the request object), and then calling the bound method
(again, passing the request). So I folded the handler code directly into
ModPyHandler. Here's the latest version:


class ModPythonInputWrapper(object):
    
    def __init__(self, req):
        self.req = req
    
    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):
        return iter(self.req.readlines())

import sys
from wsgiref.handlers import BaseCGIHandler

class ModPyHandler(BaseCGIHandler):
    
    def __init__(self, req):
        from mod_python import apache
        options = req.get_options()
        
        try:
            q = apache.mpm_query
        except AttributeError:
            # Threading and forking
            threaded = options.get('multithread', '')
            forked = options.get('multiprocess', '')
            if not (threaded and forked):
                raise ValueError("You must provide 'multithread' and "
                                 "'multiprocess' PythonOptions when "
                                 "running mod_python < 3.1")
            threaded = threaded.lower() in ('on', 't', 'true', '1')
            forked = forked.lower() in ('on', 't', 'true', '1')
        else:
            threaded = q(apache.AP_MPMQ_IS_THREADED)
            forked = q(apache.AP_MPMQ_IS_FORKED)
        
        req.add_common_vars()
        env = req.subprocess_env.copy()
        
        if req.path_info:
            env["SCRIPT_NAME"] = req.uri[:-len(req.path_info)]
        else:
            env["SCRIPT_NAME"] = req.uri
        
        env["GATEWAY_INTERFACE"] = "Python-CGI/1.1"
        
        if req.headers_in.has_key("authorization"):
            env["HTTP_AUTHORIZATION"] = req.headers_in["authorization"]
        
        BaseCGIHandler.__init__(self,
                                stdin=ModPythonInputWrapper(req),
                                stdout=None,
                                stderr=sys.stderr,
                                environ=env,
                                multiprocess=forked,
                                multithread=threaded
                                )
        self.request = req
        self._write = req.write
        
        config = req.get_config()
        debug = int(config.get("PythonDebug", 0))
        
        modname, objname = options['application'].split('::')
        module = apache.import_module(modname, autoreload=False,
log=debug)
        self.app = apache.resolve_object(module, objname, arg=None,
silent=False)
    
    def run_app(self, req):
        self.run(self.app)
        return 0 # = apache.OK
    
    def _flush(self):
        pass
    
    def send_headers(self):
        self.cleanup_headers()
        self.headers_sent = True
        self.request.status = int(self.status[:3])
        for key, val in self.headers.items():
            self.request.headers_out[key] = val

------------

and a sample .conf:


<Directory D:\htdocs\myprog>
  PythonHandler wsgiref.handlers::ModPyHandler.run_app
  PythonOption application myproggie.startup::get_wsgi_app
  # These options are required if you're using a version of mod_python <
3.1
  # multithread = On
  # multiprocess = Off
</Directory>


Robert Brewer
MIS
Amor Ministries
fumanchu at amor.org


More information about the Web-SIG mailing list