Bumped version number.
[ashd.git] / python / ashd / wsgidir.py
CommitLineData
fc2aa563
FT
1"""WSGI handler for serving chained WSGI modules from physical files
2
3The WSGI handler in this module examines the SCRIPT_FILENAME variable
4of the requests it handles -- that is, the physical file corresponding
5to the request, as determined by the webserver -- determining what to
6do with the request based on the extension of that file.
7
8By default, it handles files named `.wsgi' by compiling them into
9Python modules and using them, in turn, as chained WSGI handlers, but
10handlers for other extensions can be installed as well.
11
12When handling `.wsgi' files, the compiled modules are cached and
13reused until the file is modified, in which case the previous module
14is discarded and the new file contents are loaded into a new module in
15its place. When chaining such modules, an object named `wmain' is
16first looked for and called with no arguments if found. The object it
17returns is then used as the WSGI application object for that module,
18which is reused until the module is reloaded. If `wmain' is not found,
19an object named `application' is looked for instead. If found, it is
20used directly as the WSGI application object.
21
22This module itself contains both an `application' and a `wmain'
23object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that
24its wmain function is called, arguments can be specified to it to
25install handlers for other file extensions. Such arguments take the
26form `.EXT=MODULE.HANDLER', where EXT is the file extension to be
27handled, and the MODULE.HANDLER string is treated by splitting it
28along its last constituent dot. The part left of the dot is the name
29of a module which is imported, and the part right of the dot is the
30name of an object in that module, which should be a callable of three
31arguments. When files of the given extension are handled, that
32callable is called with the file's absolute path, the WSGI environment
33and the WSGI `start_response' function, in that order. For example,
34the argument `.fpy=my.module.foohandler' can be given to pass requests
35for `.fpy' files to the function `foohandler' in the module
36`my.module' (which must, of course, be importable). When writing such
37handler functions, you will probably want to use the getmod() function
38in this module.
39"""
40
c06db49a
FT
41import os, threading, types
42import wsgiutil
43
fc2aa563
FT
44__all__ = ["application", "wmain", "getmod", "cachedmod"]
45
f677567f 46class cachedmod(object):
fc2aa563
FT
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 """
14ef1e0e
FT
59 def __init__(self, mod, mtime):
60 self.lock = threading.Lock()
61 self.mod = mod
62 self.mtime = mtime
63
c06db49a
FT
64exts = {}
65modcache = {}
66cachelock = threading.Lock()
67
68def mangle(path):
69 ret = ""
70 for c in path:
71 if c.isalnum():
72 ret += c
73 else:
74 ret += "_"
75 return ret
76
77def getmod(path):
fc2aa563
FT
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 """
c06db49a
FT
90 sb = os.stat(path)
91 cachelock.acquire()
92 try:
93 if path in modcache:
14ef1e0e
FT
94 entry = modcache[path]
95 if sb.st_mtime <= entry.mtime:
96 return entry
97
c06db49a
FT
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__
14ef1e0e
FT
107 entry = cachedmod(mod, sb.st_mtime)
108 modcache[path] = entry
109 return entry
c06db49a
FT
110 finally:
111 cachelock.release()
112
113def chain(path, env, startreq):
114 mod = getmod(path)
14ef1e0e
FT
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"):
adb11d5f 123 entry = mod.mod.wmain()
14ef1e0e
FT
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)
36ea06a6 131 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
c06db49a
FT
132exts["wsgi"] = chain
133
48a91dd9
FT
134def 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
c06db49a 141def application(env, startreq):
fc2aa563
FT
142 """WSGI handler function
143
144 Handles WSGI requests as per the module documentation.
145 """
c06db49a 146 if not "SCRIPT_FILENAME" in env:
36ea06a6 147 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
c06db49a
FT
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):
36ea06a6 152 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
c06db49a
FT
153 ext = base[p + 1:]
154 if not ext in exts:
36ea06a6 155 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
c06db49a
FT
156 return(exts[ext](path, env, startreq))
157
adb11d5f 158def wmain(*argv):
fc2aa563
FT
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 """
48a91dd9
FT
164 for arg in argv:
165 if arg[0] == '.':
166 p = arg.index('=')
167 addext(arg[1:p], arg[p + 1:])
c06db49a 168 return application