om@3
|
1 |
"""
|
om@3
|
2 |
Web application
|
om@3
|
3 |
(from web.py)
|
om@3
|
4 |
"""
|
om@3
|
5 |
import webapi as web
|
om@3
|
6 |
import webapi, wsgi, utils
|
om@3
|
7 |
import debugerror
|
om@3
|
8 |
import httpserver
|
om@3
|
9 |
|
om@3
|
10 |
from utils import lstrips, safeunicode
|
om@3
|
11 |
import sys
|
om@3
|
12 |
|
om@3
|
13 |
import urllib
|
om@3
|
14 |
import traceback
|
om@3
|
15 |
import itertools
|
om@3
|
16 |
import os
|
om@3
|
17 |
import types
|
om@3
|
18 |
from exceptions import SystemExit
|
om@3
|
19 |
|
om@3
|
20 |
try:
|
om@3
|
21 |
import wsgiref.handlers
|
om@3
|
22 |
except ImportError:
|
om@3
|
23 |
pass # don't break people with old Pythons
|
om@3
|
24 |
|
om@3
|
25 |
__all__ = [
|
om@3
|
26 |
"application", "auto_application",
|
om@3
|
27 |
"subdir_application", "subdomain_application",
|
om@3
|
28 |
"loadhook", "unloadhook",
|
om@3
|
29 |
"autodelegate"
|
om@3
|
30 |
]
|
om@3
|
31 |
|
om@3
|
32 |
class application:
|
om@3
|
33 |
"""
|
om@3
|
34 |
Application to delegate requests based on path.
|
om@3
|
35 |
|
om@3
|
36 |
>>> urls = ("/hello", "hello")
|
om@3
|
37 |
>>> app = application(urls, globals())
|
om@3
|
38 |
>>> class hello:
|
om@3
|
39 |
... def GET(self): return "hello"
|
om@3
|
40 |
>>>
|
om@3
|
41 |
>>> app.request("/hello").data
|
om@3
|
42 |
'hello'
|
om@3
|
43 |
"""
|
om@3
|
44 |
def __init__(self, mapping=(), fvars={}, autoreload=None):
|
om@3
|
45 |
if autoreload is None:
|
om@3
|
46 |
autoreload = web.config.get('debug', False)
|
om@3
|
47 |
self.init_mapping(mapping)
|
om@3
|
48 |
self.fvars = fvars
|
om@3
|
49 |
self.processors = []
|
om@3
|
50 |
|
om@3
|
51 |
self.add_processor(loadhook(self._load))
|
om@3
|
52 |
self.add_processor(unloadhook(self._unload))
|
om@3
|
53 |
|
om@3
|
54 |
if autoreload:
|
om@3
|
55 |
def main_module_name():
|
om@3
|
56 |
mod = sys.modules['__main__']
|
om@3
|
57 |
file = getattr(mod, '__file__', None) # make sure this works even from python interpreter
|
om@3
|
58 |
return file and os.path.splitext(os.path.basename(file))[0]
|
om@3
|
59 |
|
om@3
|
60 |
def modname(fvars):
|
om@3
|
61 |
"""find name of the module name from fvars."""
|
om@3
|
62 |
file, name = fvars.get('__file__'), fvars.get('__name__')
|
om@3
|
63 |
if file is None or name is None:
|
om@3
|
64 |
return None
|
om@3
|
65 |
|
om@3
|
66 |
if name == '__main__':
|
om@3
|
67 |
# Since the __main__ module can't be reloaded, the module has
|
om@3
|
68 |
# to be imported using its file name.
|
om@3
|
69 |
name = main_module_name()
|
om@3
|
70 |
return name
|
om@3
|
71 |
|
om@3
|
72 |
mapping_name = utils.dictfind(fvars, mapping)
|
om@3
|
73 |
module_name = modname(fvars)
|
om@3
|
74 |
|
om@3
|
75 |
def reload_mapping():
|
om@3
|
76 |
"""loadhook to reload mapping and fvars."""
|
om@3
|
77 |
mod = __import__(module_name, None, None, [''])
|
om@3
|
78 |
mapping = getattr(mod, mapping_name, None)
|
om@3
|
79 |
if mapping:
|
om@3
|
80 |
self.fvars = mod.__dict__
|
om@3
|
81 |
self.init_mapping(mapping)
|
om@3
|
82 |
|
om@3
|
83 |
self.add_processor(loadhook(Reloader()))
|
om@3
|
84 |
if mapping_name and module_name:
|
om@3
|
85 |
self.add_processor(loadhook(reload_mapping))
|
om@3
|
86 |
|
om@3
|
87 |
# load __main__ module usings its filename, so that it can be reloaded.
|
om@3
|
88 |
if main_module_name() and '__main__' in sys.argv:
|
om@3
|
89 |
try:
|
om@3
|
90 |
__import__(main_module_name())
|
om@3
|
91 |
except ImportError:
|
om@3
|
92 |
pass
|
om@3
|
93 |
|
om@3
|
94 |
def _load(self):
|
om@3
|
95 |
web.ctx.app_stack.append(self)
|
om@3
|
96 |
|
om@3
|
97 |
def _unload(self):
|
om@3
|
98 |
web.ctx.app_stack = web.ctx.app_stack[:-1]
|
om@3
|
99 |
|
om@3
|
100 |
if web.ctx.app_stack:
|
om@3
|
101 |
# this is a sub-application, revert ctx to earlier state.
|
om@3
|
102 |
oldctx = web.ctx.get('_oldctx')
|
om@3
|
103 |
if oldctx:
|
om@3
|
104 |
web.ctx.home = oldctx.home
|
om@3
|
105 |
web.ctx.homepath = oldctx.homepath
|
om@3
|
106 |
web.ctx.path = oldctx.path
|
om@3
|
107 |
web.ctx.fullpath = oldctx.fullpath
|
om@3
|
108 |
|
om@3
|
109 |
def _cleanup(self):
|
om@3
|
110 |
# Threads can be recycled by WSGI servers.
|
om@3
|
111 |
# Clearing up all thread-local state to avoid interefereing with subsequent requests.
|
om@3
|
112 |
utils.ThreadedDict.clear_all()
|
om@3
|
113 |
|
om@3
|
114 |
def init_mapping(self, mapping):
|
om@3
|
115 |
self.mapping = list(utils.group(mapping, 2))
|
om@3
|
116 |
|
om@3
|
117 |
def add_mapping(self, pattern, classname):
|
om@3
|
118 |
self.mapping.append((pattern, classname))
|
om@3
|
119 |
|
om@3
|
120 |
def add_processor(self, processor):
|
om@3
|
121 |
"""
|
om@3
|
122 |
Adds a processor to the application.
|
om@3
|
123 |
|
om@3
|
124 |
>>> urls = ("/(.*)", "echo")
|
om@3
|
125 |
>>> app = application(urls, globals())
|
om@3
|
126 |
>>> class echo:
|
om@3
|
127 |
... def GET(self, name): return name
|
om@3
|
128 |
...
|
om@3
|
129 |
>>>
|
om@3
|
130 |
>>> def hello(handler): return "hello, " + handler()
|
om@3
|
131 |
...
|
om@3
|
132 |
>>> app.add_processor(hello)
|
om@3
|
133 |
>>> app.request("/web.py").data
|
om@3
|
134 |
'hello, web.py'
|
om@3
|
135 |
"""
|
om@3
|
136 |
self.processors.append(processor)
|
om@3
|
137 |
|
om@3
|
138 |
def request(self, localpart='/', method='GET', data=None,
|
om@3
|
139 |
host="0.0.0.0:8080", headers=None, https=False, **kw):
|
om@3
|
140 |
"""Makes request to this application for the specified path and method.
|
om@3
|
141 |
Response will be a storage object with data, status and headers.
|
om@3
|
142 |
|
om@3
|
143 |
>>> urls = ("/hello", "hello")
|
om@3
|
144 |
>>> app = application(urls, globals())
|
om@3
|
145 |
>>> class hello:
|
om@3
|
146 |
... def GET(self):
|
om@3
|
147 |
... web.header('Content-Type', 'text/plain')
|
om@3
|
148 |
... return "hello"
|
om@3
|
149 |
...
|
om@3
|
150 |
>>> response = app.request("/hello")
|
om@3
|
151 |
>>> response.data
|
om@3
|
152 |
'hello'
|
om@3
|
153 |
>>> response.status
|
om@3
|
154 |
'200 OK'
|
om@3
|
155 |
>>> response.headers['Content-Type']
|
om@3
|
156 |
'text/plain'
|
om@3
|
157 |
|
om@3
|
158 |
To use https, use https=True.
|
om@3
|
159 |
|
om@3
|
160 |
>>> urls = ("/redirect", "redirect")
|
om@3
|
161 |
>>> app = application(urls, globals())
|
om@3
|
162 |
>>> class redirect:
|
om@3
|
163 |
... def GET(self): raise web.seeother("/foo")
|
om@3
|
164 |
...
|
om@3
|
165 |
>>> response = app.request("/redirect")
|
om@3
|
166 |
>>> response.headers['Location']
|
om@3
|
167 |
'http://0.0.0.0:8080/foo'
|
om@3
|
168 |
>>> response = app.request("/redirect", https=True)
|
om@3
|
169 |
>>> response.headers['Location']
|
om@3
|
170 |
'https://0.0.0.0:8080/foo'
|
om@3
|
171 |
|
om@3
|
172 |
The headers argument specifies HTTP headers as a mapping object
|
om@3
|
173 |
such as a dict.
|
om@3
|
174 |
|
om@3
|
175 |
>>> urls = ('/ua', 'uaprinter')
|
om@3
|
176 |
>>> class uaprinter:
|
om@3
|
177 |
... def GET(self):
|
om@3
|
178 |
... return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT']
|
om@3
|
179 |
...
|
om@3
|
180 |
>>> app = application(urls, globals())
|
om@3
|
181 |
>>> app.request('/ua', headers = {
|
om@3
|
182 |
... 'User-Agent': 'a small jumping bean/1.0 (compatible)'
|
om@3
|
183 |
... }).data
|
om@3
|
184 |
'your user-agent is a small jumping bean/1.0 (compatible)'
|
om@3
|
185 |
|
om@3
|
186 |
"""
|
om@3
|
187 |
path, maybe_query = urllib.splitquery(localpart)
|
om@3
|
188 |
query = maybe_query or ""
|
om@3
|
189 |
|
om@3
|
190 |
if 'env' in kw:
|
om@3
|
191 |
env = kw['env']
|
om@3
|
192 |
else:
|
om@3
|
193 |
env = {}
|
om@3
|
194 |
env = dict(env, HTTP_HOST=host, REQUEST_METHOD=method, PATH_INFO=path, QUERY_STRING=query, HTTPS=str(https))
|
om@3
|
195 |
headers = headers or {}
|
om@3
|
196 |
|
om@3
|
197 |
for k, v in headers.items():
|
om@3
|
198 |
env['HTTP_' + k.upper().replace('-', '_')] = v
|
om@3
|
199 |
|
om@3
|
200 |
if 'HTTP_CONTENT_LENGTH' in env:
|
om@3
|
201 |
env['CONTENT_LENGTH'] = env.pop('HTTP_CONTENT_LENGTH')
|
om@3
|
202 |
|
om@3
|
203 |
if 'HTTP_CONTENT_TYPE' in env:
|
om@3
|
204 |
env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE')
|
om@3
|
205 |
|
om@3
|
206 |
if method not in ["HEAD", "GET"]:
|
om@3
|
207 |
data = data or ''
|
om@3
|
208 |
import StringIO
|
om@3
|
209 |
if isinstance(data, dict):
|
om@3
|
210 |
q = urllib.urlencode(data)
|
om@3
|
211 |
else:
|
om@3
|
212 |
q = data
|
om@3
|
213 |
env['wsgi.input'] = StringIO.StringIO(q)
|
om@3
|
214 |
if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env:
|
om@3
|
215 |
env['CONTENT_LENGTH'] = len(q)
|
om@3
|
216 |
response = web.storage()
|
om@3
|
217 |
def start_response(status, headers):
|
om@3
|
218 |
response.status = status
|
om@3
|
219 |
response.headers = dict(headers)
|
om@3
|
220 |
response.header_items = headers
|
om@3
|
221 |
response.data = "".join(self.wsgifunc()(env, start_response))
|
om@3
|
222 |
return response
|
om@3
|
223 |
|
om@3
|
224 |
def browser(self):
|
om@3
|
225 |
import browser
|
om@3
|
226 |
return browser.AppBrowser(self)
|
om@3
|
227 |
|
om@3
|
228 |
def handle(self):
|
om@3
|
229 |
fn, args = self._match(self.mapping, web.ctx.path)
|
om@3
|
230 |
return self._delegate(fn, self.fvars, args)
|
om@3
|
231 |
|
om@3
|
232 |
def handle_with_processors(self):
|
om@3
|
233 |
def process(processors):
|
om@3
|
234 |
try:
|
om@3
|
235 |
if processors:
|
om@3
|
236 |
p, processors = processors[0], processors[1:]
|
om@3
|
237 |
return p(lambda: process(processors))
|
om@3
|
238 |
else:
|
om@3
|
239 |
return self.handle()
|
om@3
|
240 |
except web.HTTPError:
|
om@3
|
241 |
raise
|
om@3
|
242 |
except (KeyboardInterrupt, SystemExit):
|
om@3
|
243 |
raise
|
om@3
|
244 |
except:
|
om@3
|
245 |
print >> web.debug, traceback.format_exc()
|
om@3
|
246 |
raise self.internalerror()
|
om@3
|
247 |
|
om@3
|
248 |
# processors must be applied in the resvere order. (??)
|
om@3
|
249 |
return process(self.processors)
|
om@3
|
250 |
|
om@3
|
251 |
def wsgifunc(self, *middleware):
|
om@3
|
252 |
"""Returns a WSGI-compatible function for this application."""
|
om@3
|
253 |
def peep(iterator):
|
om@3
|
254 |
"""Peeps into an iterator by doing an iteration
|
om@3
|
255 |
and returns an equivalent iterator.
|
om@3
|
256 |
"""
|
om@3
|
257 |
# wsgi requires the headers first
|
om@3
|
258 |
# so we need to do an iteration
|
om@3
|
259 |
# and save the result for later
|
om@3
|
260 |
try:
|
om@3
|
261 |
firstchunk = iterator.next()
|
om@3
|
262 |
except StopIteration:
|
om@3
|
263 |
firstchunk = ''
|
om@3
|
264 |
|
om@3
|
265 |
return itertools.chain([firstchunk], iterator)
|
om@3
|
266 |
|
om@3
|
267 |
def is_generator(x): return x and hasattr(x, 'next')
|
om@3
|
268 |
|
om@3
|
269 |
def wsgi(env, start_resp):
|
om@3
|
270 |
# clear threadlocal to avoid inteference of previous requests
|
om@3
|
271 |
self._cleanup()
|
om@3
|
272 |
|
om@3
|
273 |
self.load(env)
|
om@3
|
274 |
try:
|
om@3
|
275 |
# allow uppercase methods only
|
om@3
|
276 |
if web.ctx.method.upper() != web.ctx.method:
|
om@3
|
277 |
raise web.nomethod()
|
om@3
|
278 |
|
om@3
|
279 |
result = self.handle_with_processors()
|
om@3
|
280 |
if is_generator(result):
|
om@3
|
281 |
result = peep(result)
|
om@3
|
282 |
else:
|
om@3
|
283 |
result = [result]
|
om@3
|
284 |
except web.HTTPError, e:
|
om@3
|
285 |
result = [e.data]
|
om@3
|
286 |
|
om@3
|
287 |
result = web.safestr(iter(result))
|
om@3
|
288 |
|
om@3
|
289 |
status, headers = web.ctx.status, web.ctx.headers
|
om@3
|
290 |
start_resp(status, headers)
|
om@3
|
291 |
|
om@3
|
292 |
def cleanup():
|
om@3
|
293 |
self._cleanup()
|
om@3
|
294 |
yield '' # force this function to be a generator
|
om@3
|
295 |
|
om@3
|
296 |
return itertools.chain(result, cleanup())
|
om@3
|
297 |
|
om@3
|
298 |
for m in middleware:
|
om@3
|
299 |
wsgi = m(wsgi)
|
om@3
|
300 |
|
om@3
|
301 |
return wsgi
|
om@3
|
302 |
|
om@3
|
303 |
def run(self, *middleware):
|
om@3
|
304 |
"""
|
om@3
|
305 |
Starts handling requests. If called in a CGI or FastCGI context, it will follow
|
om@3
|
306 |
that protocol. If called from the command line, it will start an HTTP
|
om@3
|
307 |
server on the port named in the first command line argument, or, if there
|
om@3
|
308 |
is no argument, on port 8080.
|
om@3
|
309 |
|
om@3
|
310 |
`middleware` is a list of WSGI middleware which is applied to the resulting WSGI
|
om@3
|
311 |
function.
|
om@3
|
312 |
"""
|
om@3
|
313 |
return wsgi.runwsgi(self.wsgifunc(*middleware))
|
om@3
|
314 |
|
om@3
|
315 |
def stop(self):
|
om@3
|
316 |
"""Stops the http server started by run.
|
om@3
|
317 |
"""
|
om@3
|
318 |
if httpserver.server:
|
om@3
|
319 |
httpserver.server.stop()
|
om@3
|
320 |
httpserver.server = None
|
om@3
|
321 |
|
om@3
|
322 |
def cgirun(self, *middleware):
|
om@3
|
323 |
"""
|
om@3
|
324 |
Return a CGI handler. This is mostly useful with Google App Engine.
|
om@3
|
325 |
There you can just do:
|
om@3
|
326 |
|
om@3
|
327 |
main = app.cgirun()
|
om@3
|
328 |
"""
|
om@3
|
329 |
wsgiapp = self.wsgifunc(*middleware)
|
om@3
|
330 |
|
om@3
|
331 |
try:
|
om@3
|
332 |
from google.appengine.ext.webapp.util import run_wsgi_app
|
om@3
|
333 |
return run_wsgi_app(wsgiapp)
|
om@3
|
334 |
except ImportError:
|
om@3
|
335 |
# we're not running from within Google App Engine
|
om@3
|
336 |
return wsgiref.handlers.CGIHandler().run(wsgiapp)
|
om@3
|
337 |
|
om@3
|
338 |
def load(self, env):
|
om@3
|
339 |
"""Initializes ctx using env."""
|
om@3
|
340 |
ctx = web.ctx
|
om@3
|
341 |
ctx.clear()
|
om@3
|
342 |
ctx.status = '200 OK'
|
om@3
|
343 |
ctx.headers = []
|
om@3
|
344 |
ctx.output = ''
|
om@3
|
345 |
ctx.environ = ctx.env = env
|
om@3
|
346 |
ctx.host = env.get('HTTP_HOST')
|
om@3
|
347 |
|
om@3
|
348 |
if env.get('wsgi.url_scheme') in ['http', 'https']:
|
om@3
|
349 |
ctx.protocol = env['wsgi.url_scheme']
|
om@3
|
350 |
elif env.get('HTTPS', '').lower() in ['on', 'true', '1']:
|
om@3
|
351 |
ctx.protocol = 'https'
|
om@3
|
352 |
else:
|
om@3
|
353 |
ctx.protocol = 'http'
|
om@3
|
354 |
ctx.homedomain = ctx.protocol + '://' + env.get('HTTP_HOST', '[unknown]')
|
om@3
|
355 |
ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
|
om@3
|
356 |
ctx.home = ctx.homedomain + ctx.homepath
|
om@3
|
357 |
#@@ home is changed when the request is handled to a sub-application.
|
om@3
|
358 |
#@@ but the real home is required for doing absolute redirects.
|
om@3
|
359 |
ctx.realhome = ctx.home
|
om@3
|
360 |
ctx.ip = env.get('REMOTE_ADDR')
|
om@3
|
361 |
ctx.method = env.get('REQUEST_METHOD')
|
om@3
|
362 |
ctx.path = env.get('PATH_INFO')
|
om@3
|
363 |
# http://trac.lighttpd.net/trac/ticket/406 requires:
|
om@3
|
364 |
if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
|
om@3
|
365 |
ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0], ctx.homepath)
|
om@3
|
366 |
# Apache and CherryPy webservers unquote the url but lighttpd doesn't.
|
om@3
|
367 |
# unquote explicitly for lighttpd to make ctx.path uniform across all servers.
|
om@3
|
368 |
ctx.path = urllib.unquote(ctx.path)
|
om@3
|
369 |
|
om@3
|
370 |
if env.get('QUERY_STRING'):
|
om@3
|
371 |
ctx.query = '?' + env.get('QUERY_STRING', '')
|
om@3
|
372 |
else:
|
om@3
|
373 |
ctx.query = ''
|
om@3
|
374 |
|
om@3
|
375 |
ctx.fullpath = ctx.path + ctx.query
|
om@3
|
376 |
|
om@3
|
377 |
for k, v in ctx.iteritems():
|
om@3
|
378 |
# convert all string values to unicode values and replace
|
om@3
|
379 |
# malformed data with a suitable replacement marker.
|
om@3
|
380 |
if isinstance(v, str):
|
om@3
|
381 |
ctx[k] = v.decode('utf-8', 'replace')
|
om@3
|
382 |
|
om@3
|
383 |
# status must always be str
|
om@3
|
384 |
ctx.status = '200 OK'
|
om@3
|
385 |
|
om@3
|
386 |
ctx.app_stack = []
|
om@3
|
387 |
|
om@3
|
388 |
def _delegate(self, f, fvars, args=[]):
|
om@3
|
389 |
def handle_class(cls):
|
om@3
|
390 |
meth = web.ctx.method
|
om@3
|
391 |
if meth == 'HEAD' and not hasattr(cls, meth):
|
om@3
|
392 |
meth = 'GET'
|
om@3
|
393 |
if not hasattr(cls, meth):
|
om@3
|
394 |
raise web.nomethod(cls)
|
om@3
|
395 |
tocall = getattr(cls(), meth)
|
om@3
|
396 |
return tocall(*args)
|
om@3
|
397 |
|
om@3
|
398 |
def is_class(o): return isinstance(o, (types.ClassType, type))
|
om@3
|
399 |
|
om@3
|
400 |
if f is None:
|
om@3
|
401 |
raise web.notfound()
|
om@3
|
402 |
elif isinstance(f, application):
|
om@3
|
403 |
return f.handle_with_processors()
|
om@3
|
404 |
elif is_class(f):
|
om@3
|
405 |
return handle_class(f)
|
om@3
|
406 |
elif isinstance(f, basestring):
|
om@3
|
407 |
if f.startswith('redirect '):
|
om@3
|
408 |
url = f.split(' ', 1)[1]
|
om@3
|
409 |
if web.ctx.method == "GET":
|
om@3
|
410 |
x = web.ctx.env.get('QUERY_STRING', '')
|
om@3
|
411 |
if x:
|
om@3
|
412 |
url += '?' + x
|
om@3
|
413 |
raise web.redirect(url)
|
om@3
|
414 |
elif '.' in f:
|
om@3
|
415 |
mod, cls = f.rsplit('.', 1)
|
om@3
|
416 |
mod = __import__(mod, None, None, [''])
|
om@3
|
417 |
cls = getattr(mod, cls)
|
om@3
|
418 |
else:
|
om@3
|
419 |
cls = fvars[f]
|
om@3
|
420 |
return handle_class(cls)
|
om@3
|
421 |
elif hasattr(f, '__call__'):
|
om@3
|
422 |
return f()
|
om@3
|
423 |
else:
|
om@3
|
424 |
return web.notfound()
|
om@3
|
425 |
|
om@3
|
426 |
def _match(self, mapping, value):
|
om@3
|
427 |
for pat, what in mapping:
|
om@3
|
428 |
if isinstance(what, application):
|
om@3
|
429 |
if value.startswith(pat):
|
om@3
|
430 |
f = lambda: self._delegate_sub_application(pat, what)
|
om@3
|
431 |
return f, None
|
om@3
|
432 |
else:
|
om@3
|
433 |
continue
|
om@3
|
434 |
elif isinstance(what, basestring):
|
om@3
|
435 |
what, result = utils.re_subm('^' + pat + '$', what, value)
|
om@3
|
436 |
else:
|
om@3
|
437 |
result = utils.re_compile('^' + pat + '$').match(value)
|
om@3
|
438 |
|
om@3
|
439 |
if result: # it's a match
|
om@3
|
440 |
return what, [x for x in result.groups()]
|
om@3
|
441 |
return None, None
|
om@3
|
442 |
|
om@3
|
443 |
def _delegate_sub_application(self, dir, app):
|
om@3
|
444 |
"""Deletes request to sub application `app` rooted at the directory `dir`.
|
om@3
|
445 |
The home, homepath, path and fullpath values in web.ctx are updated to mimic request
|
om@3
|
446 |
to the subapp and are restored after it is handled.
|
om@3
|
447 |
|
om@3
|
448 |
@@Any issues with when used with yield?
|
om@3
|
449 |
"""
|
om@3
|
450 |
web.ctx._oldctx = web.storage(web.ctx)
|
om@3
|
451 |
web.ctx.home += dir
|
om@3
|
452 |
web.ctx.homepath += dir
|
om@3
|
453 |
web.ctx.path = web.ctx.path[len(dir):]
|
om@3
|
454 |
web.ctx.fullpath = web.ctx.fullpath[len(dir):]
|
om@3
|
455 |
return app.handle_with_processors()
|
om@3
|
456 |
|
om@3
|
457 |
def get_parent_app(self):
|
om@3
|
458 |
if self in web.ctx.app_stack:
|
om@3
|
459 |
index = web.ctx.app_stack.index(self)
|
om@3
|
460 |
if index > 0:
|
om@3
|
461 |
return web.ctx.app_stack[index-1]
|
om@3
|
462 |
|
om@3
|
463 |
def notfound(self):
|
om@3
|
464 |
"""Returns HTTPError with '404 not found' message"""
|
om@3
|
465 |
parent = self.get_parent_app()
|
om@3
|
466 |
if parent:
|
om@3
|
467 |
return parent.notfound()
|
om@3
|
468 |
else:
|
om@3
|
469 |
return web._NotFound()
|
om@3
|
470 |
|
om@3
|
471 |
def internalerror(self):
|
om@3
|
472 |
"""Returns HTTPError with '500 internal error' message"""
|
om@3
|
473 |
parent = self.get_parent_app()
|
om@3
|
474 |
if parent:
|
om@3
|
475 |
return parent.internalerror()
|
om@3
|
476 |
elif web.config.get('debug'):
|
om@3
|
477 |
import debugerror
|
om@3
|
478 |
return debugerror.debugerror()
|
om@3
|
479 |
else:
|
om@3
|
480 |
return web._InternalError()
|
om@3
|
481 |
|
om@3
|
482 |
class auto_application(application):
|
om@3
|
483 |
"""Application similar to `application` but urls are constructed
|
om@3
|
484 |
automatiacally using metaclass.
|
om@3
|
485 |
|
om@3
|
486 |
>>> app = auto_application()
|
om@3
|
487 |
>>> class hello(app.page):
|
om@3
|
488 |
... def GET(self): return "hello, world"
|
om@3
|
489 |
...
|
om@3
|
490 |
>>> class foo(app.page):
|
om@3
|
491 |
... path = '/foo/.*'
|
om@3
|
492 |
... def GET(self): return "foo"
|
om@3
|
493 |
>>> app.request("/hello").data
|
om@3
|
494 |
'hello, world'
|
om@3
|
495 |
>>> app.request('/foo/bar').data
|
om@3
|
496 |
'foo'
|
om@3
|
497 |
"""
|
om@3
|
498 |
def __init__(self):
|
om@3
|
499 |
application.__init__(self)
|
om@3
|
500 |
|
om@3
|
501 |
class metapage(type):
|
om@3
|
502 |
def __init__(klass, name, bases, attrs):
|
om@3
|
503 |
type.__init__(klass, name, bases, attrs)
|
om@3
|
504 |
path = attrs.get('path', '/' + name)
|
om@3
|
505 |
|
om@3
|
506 |
# path can be specified as None to ignore that class
|
om@3
|
507 |
# typically required to create a abstract base class.
|
om@3
|
508 |
if path is not None:
|
om@3
|
509 |
self.add_mapping(path, klass)
|
om@3
|
510 |
|
om@3
|
511 |
class page:
|
om@3
|
512 |
path = None
|
om@3
|
513 |
__metaclass__ = metapage
|
om@3
|
514 |
|
om@3
|
515 |
self.page = page
|
om@3
|
516 |
|
om@3
|
517 |
# The application class already has the required functionality of subdir_application
|
om@3
|
518 |
subdir_application = application
|
om@3
|
519 |
|
om@3
|
520 |
class subdomain_application(application):
|
om@3
|
521 |
"""
|
om@3
|
522 |
Application to delegate requests based on the host.
|
om@3
|
523 |
|
om@3
|
524 |
>>> urls = ("/hello", "hello")
|
om@3
|
525 |
>>> app = application(urls, globals())
|
om@3
|
526 |
>>> class hello:
|
om@3
|
527 |
... def GET(self): return "hello"
|
om@3
|
528 |
>>>
|
om@3
|
529 |
>>> mapping = (r"hello\.example\.com", app)
|
om@3
|
530 |
>>> app2 = subdomain_application(mapping)
|
om@3
|
531 |
>>> app2.request("/hello", host="hello.example.com").data
|
om@3
|
532 |
'hello'
|
om@3
|
533 |
>>> response = app2.request("/hello", host="something.example.com")
|
om@3
|
534 |
>>> response.status
|
om@3
|
535 |
'404 Not Found'
|
om@3
|
536 |
>>> response.data
|
om@3
|
537 |
'not found'
|
om@3
|
538 |
"""
|
om@3
|
539 |
def handle(self):
|
om@3
|
540 |
host = web.ctx.host.split(':')[0] #strip port
|
om@3
|
541 |
fn, args = self._match(self.mapping, host)
|
om@3
|
542 |
return self._delegate(fn, self.fvars, args)
|
om@3
|
543 |
|
om@3
|
544 |
def _match(self, mapping, value):
|
om@3
|
545 |
for pat, what in mapping:
|
om@3
|
546 |
if isinstance(what, basestring):
|
om@3
|
547 |
what, result = utils.re_subm('^' + pat + '$', what, value)
|
om@3
|
548 |
else:
|
om@3
|
549 |
result = utils.re_compile('^' + pat + '$').match(value)
|
om@3
|
550 |
|
om@3
|
551 |
if result: # it's a match
|
om@3
|
552 |
return what, [x for x in result.groups()]
|
om@3
|
553 |
return None, None
|
om@3
|
554 |
|
om@3
|
555 |
def loadhook(h):
|
om@3
|
556 |
"""
|
om@3
|
557 |
Converts a load hook into an application processor.
|
om@3
|
558 |
|
om@3
|
559 |
>>> app = auto_application()
|
om@3
|
560 |
>>> def f(): "something done before handling request"
|
om@3
|
561 |
...
|
om@3
|
562 |
>>> app.add_processor(loadhook(f))
|
om@3
|
563 |
"""
|
om@3
|
564 |
def processor(handler):
|
om@3
|
565 |
h()
|
om@3
|
566 |
return handler()
|
om@3
|
567 |
|
om@3
|
568 |
return processor
|
om@3
|
569 |
|
om@3
|
570 |
def unloadhook(h):
|
om@3
|
571 |
"""
|
om@3
|
572 |
Converts an unload hook into an application processor.
|
om@3
|
573 |
|
om@3
|
574 |
>>> app = auto_application()
|
om@3
|
575 |
>>> def f(): "something done after handling request"
|
om@3
|
576 |
...
|
om@3
|
577 |
>>> app.add_processor(unloadhook(f))
|
om@3
|
578 |
"""
|
om@3
|
579 |
def processor(handler):
|
om@3
|
580 |
try:
|
om@3
|
581 |
result = handler()
|
om@3
|
582 |
is_generator = result and hasattr(result, 'next')
|
om@3
|
583 |
except:
|
om@3
|
584 |
# run the hook even when handler raises some exception
|
om@3
|
585 |
h()
|
om@3
|
586 |
raise
|
om@3
|
587 |
|
om@3
|
588 |
if is_generator:
|
om@3
|
589 |
return wrap(result)
|
om@3
|
590 |
else:
|
om@3
|
591 |
h()
|
om@3
|
592 |
return result
|
om@3
|
593 |
|
om@3
|
594 |
def wrap(result):
|
om@3
|
595 |
def next():
|
om@3
|
596 |
try:
|
om@3
|
597 |
return result.next()
|
om@3
|
598 |
except:
|
om@3
|
599 |
# call the hook at the and of iterator
|
om@3
|
600 |
h()
|
om@3
|
601 |
raise
|
om@3
|
602 |
|
om@3
|
603 |
result = iter(result)
|
om@3
|
604 |
while True:
|
om@3
|
605 |
yield next()
|
om@3
|
606 |
|
om@3
|
607 |
return processor
|
om@3
|
608 |
|
om@3
|
609 |
def autodelegate(prefix=''):
|
om@3
|
610 |
"""
|
om@3
|
611 |
Returns a method that takes one argument and calls the method named prefix+arg,
|
om@3
|
612 |
calling `notfound()` if there isn't one. Example:
|
om@3
|
613 |
|
om@3
|
614 |
urls = ('/prefs/(.*)', 'prefs')
|
om@3
|
615 |
|
om@3
|
616 |
class prefs:
|
om@3
|
617 |
GET = autodelegate('GET_')
|
om@3
|
618 |
def GET_password(self): pass
|
om@3
|
619 |
def GET_privacy(self): pass
|
om@3
|
620 |
|
om@3
|
621 |
`GET_password` would get called for `/prefs/password` while `GET_privacy` for
|
om@3
|
622 |
`GET_privacy` gets called for `/prefs/privacy`.
|
om@3
|
623 |
|
om@3
|
624 |
If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
|
om@3
|
625 |
is called.
|
om@3
|
626 |
"""
|
om@3
|
627 |
def internal(self, arg):
|
om@3
|
628 |
if '/' in arg:
|
om@3
|
629 |
first, rest = arg.split('/', 1)
|
om@3
|
630 |
func = prefix + first
|
om@3
|
631 |
args = ['/' + rest]
|
om@3
|
632 |
else:
|
om@3
|
633 |
func = prefix + arg
|
om@3
|
634 |
args = []
|
om@3
|
635 |
|
om@3
|
636 |
if hasattr(self, func):
|
om@3
|
637 |
try:
|
om@3
|
638 |
return getattr(self, func)(*args)
|
om@3
|
639 |
except TypeError:
|
om@3
|
640 |
raise web.notfound()
|
om@3
|
641 |
else:
|
om@3
|
642 |
raise web.notfound()
|
om@3
|
643 |
return internal
|
om@3
|
644 |
|
om@3
|
645 |
class Reloader:
|
om@3
|
646 |
"""Checks to see if any loaded modules have changed on disk and,
|
om@3
|
647 |
if so, reloads them.
|
om@3
|
648 |
"""
|
om@3
|
649 |
|
om@3
|
650 |
"""File suffix of compiled modules."""
|
om@3
|
651 |
if sys.platform.startswith('java'):
|
om@3
|
652 |
SUFFIX = '$py.class'
|
om@3
|
653 |
else:
|
om@3
|
654 |
SUFFIX = '.pyc'
|
om@3
|
655 |
|
om@3
|
656 |
def __init__(self):
|
om@3
|
657 |
self.mtimes = {}
|
om@3
|
658 |
|
om@3
|
659 |
def __call__(self):
|
om@3
|
660 |
for mod in sys.modules.values():
|
om@3
|
661 |
self.check(mod)
|
om@3
|
662 |
|
om@3
|
663 |
def check(self, mod):
|
om@3
|
664 |
# jython registers java packages as modules but they either
|
om@3
|
665 |
# don't have a __file__ attribute or its value is None
|
om@3
|
666 |
if not (mod and hasattr(mod, '__file__') and mod.__file__):
|
om@3
|
667 |
return
|
om@3
|
668 |
|
om@3
|
669 |
try:
|
om@3
|
670 |
mtime = os.stat(mod.__file__).st_mtime
|
om@3
|
671 |
except (OSError, IOError):
|
om@3
|
672 |
return
|
om@3
|
673 |
if mod.__file__.endswith(self.__class__.SUFFIX) and os.path.exists(mod.__file__[:-1]):
|
om@3
|
674 |
mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
|
om@3
|
675 |
|
om@3
|
676 |
if mod not in self.mtimes:
|
om@3
|
677 |
self.mtimes[mod] = mtime
|
om@3
|
678 |
elif self.mtimes[mod] < mtime:
|
om@3
|
679 |
try:
|
om@3
|
680 |
reload(mod)
|
om@3
|
681 |
self.mtimes[mod] = mtime
|
om@3
|
682 |
except ImportError:
|
om@3
|
683 |
pass
|
om@3
|
684 |
|
om@3
|
685 |
if __name__ == "__main__":
|
om@3
|
686 |
import doctest
|
om@3
|
687 |
doctest.testmod()
|