python: Documented the ashd.wsgidir module.
[ashd.git] / python / ashd / wsgidir.py
1 """WSGI handler for serving chained WSGI modules from physical files
2
3 The WSGI handler in this module examines the SCRIPT_FILENAME variable
4 of the requests it handles -- that is, the physical file corresponding
5 to the request, as determined by the webserver -- determining what to
6 do with the request based on the extension of that file.
7
8 By default, it handles files named `.wsgi' by compiling them into
9 Python modules and using them, in turn, as chained WSGI handlers, but
10 handlers for other extensions can be installed as well.
11
12 When handling `.wsgi' files, the compiled modules are cached and
13 reused until the file is modified, in which case the previous module
14 is discarded and the new file contents are loaded into a new module in
15 its place. When chaining such modules, an object named `wmain' is
16 first looked for and called with no arguments if found. The object it
17 returns is then used as the WSGI application object for that module,
18 which is reused until the module is reloaded. If `wmain' is not found,
19 an object named `application' is looked for instead. If found, it is
20 used directly as the WSGI application object.
21
22 This module itself contains both an `application' and a `wmain'
23 object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that
24 its wmain function is called, arguments can be specified to it to
25 install handlers for other file extensions. Such arguments take the
26 form `.EXT=MODULE.HANDLER', where EXT is the file extension to be
27 handled, and the MODULE.HANDLER string is treated by splitting it
28 along its last constituent dot. The part left of the dot is the name
29 of a module which is imported, and the part right of the dot is the
30 name of an object in that module, which should be a callable of three
31 arguments. When files of the given extension are handled, that
32 callable is called with the file's absolute path, the WSGI environment
33 and the WSGI `start_response' function, in that order. For example,
34 the argument `.fpy=my.module.foohandler' can be given to pass requests
35 for `.fpy' files to the function `foohandler' in the module
36 `my.module' (which must, of course, be importable). When writing such
37 handler functions, you will probably want to use the getmod() function
38 in this module.
39 """
40
41 import os, threading, types
42 import wsgiutil
43
44 __all__ = ["application", "wmain", "getmod", "cachedmod"]
45
46 class cachedmod(object):
47     """Cache entry for modules loaded by getmod()
48
49     Instances of this class are returned by the getmod()
50     function. They contain three data attributes:
51      * mod - The loaded module
52      * lock - A threading.Lock object, which can be used for
53        manipulating this instance in a thread-safe manner
54      * mtime - The time the file was last modified
55
56     Additional data attributes can be arbitrarily added for recording
57     any meta-data about the module.
58     """
59     def __init__(self, mod, mtime):
60         self.lock = threading.Lock()
61         self.mod = mod
62         self.mtime = mtime
63
64 exts = {}
65 modcache = {}
66 cachelock = threading.Lock()
67
68 def mangle(path):
69     ret = ""
70     for c in path:
71         if c.isalnum():
72             ret += c
73         else:
74             ret += "_"
75     return ret
76
77 def getmod(path):
78     """Load the given file as a module, caching it appropriately
79
80     The given file is loaded and compiled into a Python module. The
81     compiled module is cached and returned upon subsequent requests
82     for the same file, unless the file has changed (as determined by
83     its mtime), in which case the cached module is discarded and the
84     new file contents are reloaded in its place.
85
86     The return value is an instance of the cachedmod class, which can
87     be used for locking purposes and for storing arbitrary meta-data
88     about the module. See its documentation for details.
89     """
90     sb = os.stat(path)
91     cachelock.acquire()
92     try:
93         if path in modcache:
94             entry = modcache[path]
95             if sb.st_mtime <= entry.mtime:
96                 return entry
97         
98         f = open(path)
99         try:
100             text = f.read()
101         finally:
102             f.close()
103         code = compile(text, path, "exec")
104         mod = types.ModuleType(mangle(path))
105         mod.__file__ = path
106         exec code in mod.__dict__
107         entry = cachedmod(mod, sb.st_mtime)
108         modcache[path] = entry
109         return entry
110     finally:
111         cachelock.release()
112
113 def chain(path, env, startreq):
114     mod = getmod(path)
115     entry = None
116     if mod is not None:
117         mod.lock.acquire()
118         try:
119             if hasattr(mod, "entry"):
120                 entry = mod.entry
121             else:
122                 if hasattr(mod.mod, "wmain"):
123                     entry = mod.mod.wmain()
124                 elif hasattr(mod.mod, "application"):
125                     entry = mod.mod.application
126                 mod.entry = entry
127         finally:
128             mod.lock.release()
129     if entry is not None:
130         return entry(env, startreq)
131     return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
132 exts["wsgi"] = chain
133
134 def addext(ext, handler):
135     p = handler.rindex('.')
136     mname = handler[:p]
137     hname = handler[p + 1:]
138     mod = __import__(mname, fromlist = ["dummy"])
139     exts[ext] = getattr(mod, hname)
140
141 def application(env, startreq):
142     """WSGI handler function
143
144     Handles WSGI requests as per the module documentation.
145     """
146     if not "SCRIPT_FILENAME" in env:
147         return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
148     path = env["SCRIPT_FILENAME"]
149     base = os.path.basename(path)
150     p = base.rfind('.')
151     if p < 0 or not os.access(path, os.R_OK):
152         return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
153     ext = base[p + 1:]
154     if not ext in exts:
155         return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
156     return(exts[ext](path, env, startreq))
157
158 def wmain(*argv):
159     """Main function for ashd(7)-compatible WSGI handlers
160
161     Returns the `application' function. If any arguments are given,
162     they are parsed according to the module documentation.
163     """
164     for arg in argv:
165         if arg[0] == '.':
166             p = arg.index('=')
167             addext(arg[1:p], arg[p + 1:])
168     return application