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