OpenSecurity/install/web.py-0.37/web/application.py
author om
Mon, 02 Dec 2013 14:02:05 +0100
changeset 3 65432e6c6042
permissions -rwxr-xr-x
initial deployment and project layout commit
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()