Package web :: Package web :: Module application'
[hide private]
[frames] | no frames]

Source Code for Module web.web.application'

  1  #!/usr/bin/python 
  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 # don't break people with old Pythons 
 24   
 25  __all__ = [ 
 26      "application", "auto_application", 
 27      "subdir_application", "subdomain_application",  
 28      "loadhook", "unloadhook", 
 29      "autodelegate" 
 30  ] 
 31   
32 -class application:
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) # make sure this works even from python interpreter 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 # Since the __main__ module can't be reloaded, the module has 68 # to be imported using its file name. 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 # load __main__ module usings its filename, so that it can be reloaded. 88 if main_module_name() and '__main__' in sys.argv: 89 try: 90 __import__(main_module_name()) 91 except ImportError: 92 pass 93
94 - def _load(self):
95 web.ctx.app_stack.append(self)
96
97 - def _unload(self):
98 web.ctx.app_stack = web.ctx.app_stack[:-1] 99 100 if web.ctx.app_stack: 101 # this is a sub-application, revert ctx to earlier state. 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
109 - def _cleanup(self):
110 #@@@ 111 # Since the CherryPy Webserver uses thread pool, the thread-local state is never cleared. 112 # This interferes with the other requests. 113 # clearing the thread-local storage to avoid that. 114 # see utils.ThreadedDict for details 115 import threading 116 t = threading.currentThread() 117 if hasattr(t, '_d'): 118 del t._d
119
120 - def add_mapping(self, pattern, classname):
121 self.mapping += (pattern, classname)
122
123 - def add_processor(self, processor):
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
227 - def browser(self):
228 import browser 229 return browser.AppBrowser(self)
230
231 - def handle(self):
232 fn, args = self._match(self.mapping, web.ctx.path) 233 return self._delegate(fn, self.fvars, args)
234
235 - def handle_with_processors(self):
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 # processors must be applied in the resvere order. (??) 252 return process(self.processors) 253
254 - def wsgifunc(self, *middleware):
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 # wsgi requires the headers first 261 # so we need to do an iteration 262 # and save the result for later 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 # clear threadlocal to avoid inteference of previous requests 274 self._cleanup() 275 276 self.load(env) 277 try: 278 # allow uppercase methods only 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 '' # force this function to be a generator 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 # we're not running from within Google App Engine 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 #@@ home is changed when the request is handled to a sub-application. 354 #@@ but the real home is required for doing absolute redirects. 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 # http://trac.lighttpd.net/trac/ticket/406 requires: 360 if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'): 361 ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0], ctx.homepath) 362 # Apache and CherryPy webservers unquote the url but lighttpd doesn't. 363 # unquote explicitly for lighttpd to make ctx.path uniform across all servers. 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 # status must always be str 378 ctx.status = '200 OK' 379 380 ctx.app_stack = []
381
382 - def _delegate(self, f, fvars, args=[]):
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: # it's a match 435 return what, [x for x in result.groups()] 436 return None, None
437
438 - def _delegate_sub_application(self, dir, app):
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
452 - def get_parent_app(self):
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
458 - def notfound(self):
459 """Returns HTTPError with '404 not found' message""" 460 parent = self.get_parent_app() 461 if parent: 462 return parent.notfound() 463 else: 464 return web._NotFound()
465
466 - def internalerror(self):
467 """Returns HTTPError with '500 internal error' message""" 468 parent = self.get_parent_app() 469 if parent: 470 return parent.internalerror() 471 elif web.config.get('debug'): 472 import debugerror 473 return debugerror.debugerror() 474 else: 475 return web._InternalError()
476
477 -class auto_application(application):
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 """
493 - def __init__(self):
494 application.__init__(self) 495 496 class metapage(type): 497 def __init__(klass, name, bases, attrs): 498 type.__init__(klass, name, bases, attrs) 499 path = attrs.get('path', '/' + name) 500 501 # path can be specified as None to ignore that class 502 # typically required to create a abstract base class. 503 if path is not None: 504 self.add_mapping(path, klass)
505 506 class page: 507 path = None 508 __metaclass__ = metapage 509 510 self.page = page 511 512 # The application class already has the required functionality of subdir_application 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 """
534 - def handle(self):
535 host = web.ctx.host.split(':')[0] #strip port 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: # it's a match 547 return what, [x for x in result.groups()] 548 return None, None
549
550 -def loadhook(h):
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
565 -def unloadhook(h):
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 # run the hook even when handler raises some exception 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 # call the hook at the and of iterator 595 h() 596 raise 597 598 result = iter(result) 599 while True: 600 yield next() 601 602 return processor 603
604 -def autodelegate(prefix=''):
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
640 -class Reloader:
641 """Checks to see if any loaded modules have changed on disk and, 642 if so, reloads them. 643 """
644 - def __init__(self):
645 self.mtimes = {}
646
647 - def __call__(self):
648 for mod in sys.modules.values(): 649 self.check(mod)
650
651 - def check(self, mod):
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