web.web.application'

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