6 import webapi, wsgi, utils
10 from utils import lstrips, safeunicode
18 from exceptions import SystemExit
21 import wsgiref.handlers
23 pass # don't break people with old Pythons
26 "application", "auto_application",
27 "subdir_application", "subdomain_application",
28 "loadhook", "unloadhook",
34 Application to delegate requests based on path.
36 >>> urls = ("/hello", "hello")
37 >>> app = application(urls, globals())
39 ... def GET(self): return "hello"
41 >>> app.request("/hello").data
44 def __init__(self, mapping=(), fvars={}, autoreload=None):
45 if autoreload is None:
46 autoreload = web.config.get('debug', False)
47 self.init_mapping(mapping)
51 self.add_processor(loadhook(self._load))
52 self.add_processor(unloadhook(self._unload))
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]
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:
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()
72 mapping_name = utils.dictfind(fvars, mapping)
73 module_name = modname(fvars)
76 """loadhook to reload mapping and fvars."""
77 mod = __import__(module_name, None, None, [''])
78 mapping = getattr(mod, mapping_name, None)
80 self.fvars = mod.__dict__
81 self.init_mapping(mapping)
83 self.add_processor(loadhook(Reloader()))
84 if mapping_name and module_name:
85 self.add_processor(loadhook(reload_mapping))
87 # load __main__ module usings its filename, so that it can be reloaded.
88 if main_module_name() and '__main__' in sys.argv:
90 __import__(main_module_name())
95 web.ctx.app_stack.append(self)
98 web.ctx.app_stack = web.ctx.app_stack[:-1]
100 if web.ctx.app_stack:
101 # this is a sub-application, revert ctx to earlier state.
102 oldctx = web.ctx.get('_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
110 # Threads can be recycled by WSGI servers.
111 # Clearing up all thread-local state to avoid interefereing with subsequent requests.
112 utils.ThreadedDict.clear_all()
114 def init_mapping(self, mapping):
115 self.mapping = list(utils.group(mapping, 2))
117 def add_mapping(self, pattern, classname):
118 self.mapping.append((pattern, classname))
120 def add_processor(self, processor):
122 Adds a processor to the application.
124 >>> urls = ("/(.*)", "echo")
125 >>> app = application(urls, globals())
127 ... def GET(self, name): return name
130 >>> def hello(handler): return "hello, " + handler()
132 >>> app.add_processor(hello)
133 >>> app.request("/web.py").data
136 self.processors.append(processor)
138 def request(self, localpart='/', method='GET', data=None,
139 host="0.0.0.0:8080", headers=None, https=False, **kw):
140 """Makes request to this application for the specified path and method.
141 Response will be a storage object with data, status and headers.
143 >>> urls = ("/hello", "hello")
144 >>> app = application(urls, globals())
147 ... web.header('Content-Type', 'text/plain')
150 >>> response = app.request("/hello")
155 >>> response.headers['Content-Type']
158 To use https, use https=True.
160 >>> urls = ("/redirect", "redirect")
161 >>> app = application(urls, globals())
163 ... def GET(self): raise web.seeother("/foo")
165 >>> response = app.request("/redirect")
166 >>> response.headers['Location']
167 'http://0.0.0.0:8080/foo'
168 >>> response = app.request("/redirect", https=True)
169 >>> response.headers['Location']
170 'https://0.0.0.0:8080/foo'
172 The headers argument specifies HTTP headers as a mapping object
175 >>> urls = ('/ua', 'uaprinter')
178 ... return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT']
180 >>> app = application(urls, globals())
181 >>> app.request('/ua', headers = {
182 ... 'User-Agent': 'a small jumping bean/1.0 (compatible)'
184 'your user-agent is a small jumping bean/1.0 (compatible)'
187 path, maybe_query = urllib.splitquery(localpart)
188 query = maybe_query or ""
194 env = dict(env, HTTP_HOST=host, REQUEST_METHOD=method, PATH_INFO=path, QUERY_STRING=query, HTTPS=str(https))
195 headers = headers or {}
197 for k, v in headers.items():
198 env['HTTP_' + k.upper().replace('-', '_')] = v
200 if 'HTTP_CONTENT_LENGTH' in env:
201 env['CONTENT_LENGTH'] = env.pop('HTTP_CONTENT_LENGTH')
203 if 'HTTP_CONTENT_TYPE' in env:
204 env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE')
206 if method not in ["HEAD", "GET"]:
209 if isinstance(data, dict):
210 q = urllib.urlencode(data)
213 env['wsgi.input'] = StringIO.StringIO(q)
214 if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env:
215 env['CONTENT_LENGTH'] = len(q)
216 response = web.storage()
217 def start_response(status, headers):
218 response.status = status
219 response.headers = dict(headers)
220 response.header_items = headers
221 response.data = "".join(self.wsgifunc()(env, start_response))
226 return browser.AppBrowser(self)
229 fn, args = self._match(self.mapping, web.ctx.path)
230 return self._delegate(fn, self.fvars, args)
232 def handle_with_processors(self):
233 def process(processors):
236 p, processors = processors[0], processors[1:]
237 return p(lambda: process(processors))
240 except web.HTTPError:
242 except (KeyboardInterrupt, SystemExit):
245 print >> web.debug, traceback.format_exc()
246 raise self.internalerror()
248 # processors must be applied in the resvere order. (??)
249 return process(self.processors)
251 def wsgifunc(self, *middleware):
252 """Returns a WSGI-compatible function for this application."""
254 """Peeps into an iterator by doing an iteration
255 and returns an equivalent iterator.
257 # wsgi requires the headers first
258 # so we need to do an iteration
259 # and save the result for later
261 firstchunk = iterator.next()
262 except StopIteration:
265 return itertools.chain([firstchunk], iterator)
267 def is_generator(x): return x and hasattr(x, 'next')
269 def wsgi(env, start_resp):
270 # clear threadlocal to avoid inteference of previous requests
275 # allow uppercase methods only
276 if web.ctx.method.upper() != web.ctx.method:
279 result = self.handle_with_processors()
280 if is_generator(result):
281 result = peep(result)
284 except web.HTTPError, e:
287 result = web.safestr(iter(result))
289 status, headers = web.ctx.status, web.ctx.headers
290 start_resp(status, headers)
294 yield '' # force this function to be a generator
296 return itertools.chain(result, cleanup())
303 def run(self, *middleware):
305 Starts handling requests. If called in a CGI or FastCGI context, it will follow
306 that protocol. If called from the command line, it will start an HTTP
307 server on the port named in the first command line argument, or, if there
308 is no argument, on port 8080.
310 `middleware` is a list of WSGI middleware which is applied to the resulting WSGI
313 return wsgi.runwsgi(self.wsgifunc(*middleware))
316 """Stops the http server started by run.
318 if httpserver.server:
319 httpserver.server.stop()
320 httpserver.server = None
322 def cgirun(self, *middleware):
324 Return a CGI handler. This is mostly useful with Google App Engine.
325 There you can just do:
329 wsgiapp = self.wsgifunc(*middleware)
332 from google.appengine.ext.webapp.util import run_wsgi_app
333 return run_wsgi_app(wsgiapp)
335 # we're not running from within Google App Engine
336 return wsgiref.handlers.CGIHandler().run(wsgiapp)
339 """Initializes ctx using env."""
342 ctx.status = '200 OK'
345 ctx.environ = ctx.env = env
346 ctx.host = env.get('HTTP_HOST')
348 if env.get('wsgi.url_scheme') in ['http', 'https']:
349 ctx.protocol = env['wsgi.url_scheme']
350 elif env.get('HTTPS', '').lower() in ['on', 'true', '1']:
351 ctx.protocol = 'https'
353 ctx.protocol = 'http'
354 ctx.homedomain = ctx.protocol + '://' + env.get('HTTP_HOST', '[unknown]')
355 ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
356 ctx.home = ctx.homedomain + ctx.homepath
357 #@@ home is changed when the request is handled to a sub-application.
358 #@@ but the real home is required for doing absolute redirects.
359 ctx.realhome = ctx.home
360 ctx.ip = env.get('REMOTE_ADDR')
361 ctx.method = env.get('REQUEST_METHOD')
362 ctx.path = env.get('PATH_INFO')
363 # http://trac.lighttpd.net/trac/ticket/406 requires:
364 if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
365 ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0], ctx.homepath)
366 # Apache and CherryPy webservers unquote the url but lighttpd doesn't.
367 # unquote explicitly for lighttpd to make ctx.path uniform across all servers.
368 ctx.path = urllib.unquote(ctx.path)
370 if env.get('QUERY_STRING'):
371 ctx.query = '?' + env.get('QUERY_STRING', '')
375 ctx.fullpath = ctx.path + ctx.query
377 for k, v in ctx.iteritems():
378 # convert all string values to unicode values and replace
379 # malformed data with a suitable replacement marker.
380 if isinstance(v, str):
381 ctx[k] = v.decode('utf-8', 'replace')
383 # status must always be str
384 ctx.status = '200 OK'
388 def _delegate(self, f, fvars, args=[]):
389 def handle_class(cls):
390 meth = web.ctx.method
391 if meth == 'HEAD' and not hasattr(cls, meth):
393 if not hasattr(cls, meth):
394 raise web.nomethod(cls)
395 tocall = getattr(cls(), meth)
398 def is_class(o): return isinstance(o, (types.ClassType, type))
402 elif isinstance(f, application):
403 return f.handle_with_processors()
405 return handle_class(f)
406 elif isinstance(f, basestring):
407 if f.startswith('redirect '):
408 url = f.split(' ', 1)[1]
409 if web.ctx.method == "GET":
410 x = web.ctx.env.get('QUERY_STRING', '')
413 raise web.redirect(url)
415 mod, cls = f.rsplit('.', 1)
416 mod = __import__(mod, None, None, [''])
417 cls = getattr(mod, cls)
420 return handle_class(cls)
421 elif hasattr(f, '__call__'):
424 return web.notfound()
426 def _match(self, mapping, value):
427 for pat, what in mapping:
428 if isinstance(what, application):
429 if value.startswith(pat):
430 f = lambda: self._delegate_sub_application(pat, what)
434 elif isinstance(what, basestring):
435 what, result = utils.re_subm('^' + pat + '$', what, value)
437 result = utils.re_compile('^' + pat + '$').match(value)
439 if result: # it's a match
440 return what, [x for x in result.groups()]
443 def _delegate_sub_application(self, dir, app):
444 """Deletes request to sub application `app` rooted at the directory `dir`.
445 The home, homepath, path and fullpath values in web.ctx are updated to mimic request
446 to the subapp and are restored after it is handled.
448 @@Any issues with when used with yield?
450 web.ctx._oldctx = web.storage(web.ctx)
452 web.ctx.homepath += dir
453 web.ctx.path = web.ctx.path[len(dir):]
454 web.ctx.fullpath = web.ctx.fullpath[len(dir):]
455 return app.handle_with_processors()
457 def get_parent_app(self):
458 if self in web.ctx.app_stack:
459 index = web.ctx.app_stack.index(self)
461 return web.ctx.app_stack[index-1]
464 """Returns HTTPError with '404 not found' message"""
465 parent = self.get_parent_app()
467 return parent.notfound()
469 return web._NotFound()
471 def internalerror(self):
472 """Returns HTTPError with '500 internal error' message"""
473 parent = self.get_parent_app()
475 return parent.internalerror()
476 elif web.config.get('debug'):
478 return debugerror.debugerror()
480 return web._InternalError()
482 class auto_application(application):
483 """Application similar to `application` but urls are constructed
484 automatiacally using metaclass.
486 >>> app = auto_application()
487 >>> class hello(app.page):
488 ... def GET(self): return "hello, world"
490 >>> class foo(app.page):
492 ... def GET(self): return "foo"
493 >>> app.request("/hello").data
495 >>> app.request('/foo/bar').data
499 application.__init__(self)
501 class metapage(type):
502 def __init__(klass, name, bases, attrs):
503 type.__init__(klass, name, bases, attrs)
504 path = attrs.get('path', '/' + name)
506 # path can be specified as None to ignore that class
507 # typically required to create a abstract base class.
509 self.add_mapping(path, klass)
513 __metaclass__ = metapage
517 # The application class already has the required functionality of subdir_application
518 subdir_application = application
520 class subdomain_application(application):
522 Application to delegate requests based on the host.
524 >>> urls = ("/hello", "hello")
525 >>> app = application(urls, globals())
527 ... def GET(self): return "hello"
529 >>> mapping = (r"hello\.example\.com", app)
530 >>> app2 = subdomain_application(mapping)
531 >>> app2.request("/hello", host="hello.example.com").data
533 >>> response = app2.request("/hello", host="something.example.com")
540 host = web.ctx.host.split(':')[0] #strip port
541 fn, args = self._match(self.mapping, host)
542 return self._delegate(fn, self.fvars, args)
544 def _match(self, mapping, value):
545 for pat, what in mapping:
546 if isinstance(what, basestring):
547 what, result = utils.re_subm('^' + pat + '$', what, value)
549 result = utils.re_compile('^' + pat + '$').match(value)
551 if result: # it's a match
552 return what, [x for x in result.groups()]
557 Converts a load hook into an application processor.
559 >>> app = auto_application()
560 >>> def f(): "something done before handling request"
562 >>> app.add_processor(loadhook(f))
564 def processor(handler):
572 Converts an unload hook into an application processor.
574 >>> app = auto_application()
575 >>> def f(): "something done after handling request"
577 >>> app.add_processor(unloadhook(f))
579 def processor(handler):
582 is_generator = result and hasattr(result, 'next')
584 # run the hook even when handler raises some exception
599 # call the hook at the and of iterator
603 result = iter(result)
609 def autodelegate(prefix=''):
611 Returns a method that takes one argument and calls the method named prefix+arg,
612 calling `notfound()` if there isn't one. Example:
614 urls = ('/prefs/(.*)', 'prefs')
617 GET = autodelegate('GET_')
618 def GET_password(self): pass
619 def GET_privacy(self): pass
621 `GET_password` would get called for `/prefs/password` while `GET_privacy` for
622 `GET_privacy` gets called for `/prefs/privacy`.
624 If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
627 def internal(self, arg):
629 first, rest = arg.split('/', 1)
630 func = prefix + first
636 if hasattr(self, func):
638 return getattr(self, func)(*args)
646 """Checks to see if any loaded modules have changed on disk and,
650 """File suffix of compiled modules."""
651 if sys.platform.startswith('java'):
660 for mod in sys.modules.values():
663 def check(self, mod):
664 # jython registers java packages as modules but they either
665 # don't have a __file__ attribute or its value is None
666 if not (mod and hasattr(mod, '__file__') and mod.__file__):
670 mtime = os.stat(mod.__file__).st_mtime
671 except (OSError, IOError):
673 if mod.__file__.endswith(self.__class__.SUFFIX) and os.path.exists(mod.__file__[:-1]):
674 mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
676 if mod not in self.mtimes:
677 self.mtimes[mod] = mtime
678 elif self.mtimes[mod] < mtime:
681 self.mtimes[mod] = mtime
685 if __name__ == "__main__":