Commits

Lynn Rees committed f85f32e

- update to 0.7

  • Participants
  • Parent commits 4e98d7c
  • Branches urlrelay

Comments (0)

Files changed (4)

File urlrelay/ez_setup.py

 This file can also be run as a script to install or upgrade setuptools.
 """
 import sys
-DEFAULT_VERSION = "0.6c7"
+DEFAULT_VERSION = "0.6c9"
 DEFAULT_URL     = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
 
 md5_data = {
     'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
     'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
     'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+    'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+    'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+    'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+    'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+    'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+    'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+    'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
 }
 
 import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
 
 def _validate_md5(egg_name, data):
     if egg_name in md5_data:
-        from md5 import md5
         digest = md5(data).hexdigest()
         if digest != md5_data[egg_name]:
             print >>sys.stderr, (
             sys.exit(2)
     return data
 
-
 def use_setuptools(
     version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
     download_delay=15
     this routine will print a message to ``sys.stderr`` and raise SystemExit in
     an attempt to abort the calling script.
     """
-    try:
-        import setuptools
-        if setuptools.__version__ == '0.0.1':
-            print >>sys.stderr, (
-            "You have an obsolete version of setuptools installed.  Please\n"
-            "remove it from your system entirely before rerunning this script."
-            )
-            sys.exit(2)
-    except ImportError:
+    was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+    def do_download():
         egg = download_setuptools(version, download_base, to_dir, download_delay)
         sys.path.insert(0, egg)
         import setuptools; setuptools.bootstrap_install_from = egg
-
-    import pkg_resources
     try:
-        pkg_resources.require("setuptools>="+version)
-
+        import pkg_resources
+    except ImportError:
+        return do_download()       
+    try:
+        pkg_resources.require("setuptools>="+version); return
     except pkg_resources.VersionConflict, e:
-        # XXX could we install in a subprocess here?
-        print >>sys.stderr, (
+        if was_imported:
+            print >>sys.stderr, (
             "The required version of setuptools (>=%s) is not available, and\n"
             "can't be installed while this script is running. Please install\n"
-            " a more recent version first.\n\n(Currently using %r)"
-        ) % (version, e.args[0])
-        sys.exit(2)
+            " a more recent version first, using 'easy_install -U setuptools'."
+            "\n\n(Currently using %r)"
+            ) % (version, e.args[0])
+            sys.exit(2)
+        else:
+            del pkg_resources, sys.modules['pkg_resources']    # reload ok
+            return do_download()
+    except pkg_resources.DistributionNotFound:
+        return do_download()
 
 def download_setuptools(
     version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
             if dst: dst.close()
     return os.path.realpath(saveto)
 
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
 def main(argv, version=DEFAULT_VERSION):
     """Install or upgrade setuptools and EasyInstall"""
-
     try:
         import setuptools
     except ImportError:
                 os.unlink(egg)
     else:
         if setuptools.__version__ == '0.0.1':
-            # tell the user to uninstall obsolete version
-            use_setuptools(version)
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
 
     req = "setuptools>="+version
     import pkg_resources
             print "Setuptools version",version,"or greater has been installed."
             print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
 
-
-
 def update_md5(filenames):
     """Update our built-in md5 registry"""
 
     import re
-    from md5 import md5
 
     for name in filenames:
         base = os.path.basename(name)
 
 
 
+

File urlrelay/setup.py

     from distutils.core import setup
 
 setup(name='urlrelay',
-      version='0.6',
+      version='0.7',
       description='''RESTful WSGI URL dispatcher.''',
       long_description='''Simple URL dispatcher that passes HTTP
 requests to a WSGI application based on a matching URL path regex
       author='L. C. Rees',
       author_email='lcrees@gmail.com',
       license='BSD',
-      py_modules=['urlrelay'],
-      packages = ['tests'],
+      py_modules=['urlrelay', 'ez_setup'],
+      packages=['tests'],
       test_suite='tests.test_urlrelay',
       zip_safe = True,
       keywords='WSGI URL dispatch relay route middleware web HTTP',

File urlrelay/tests/test_urlrelay.py

 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 '''Unit tests for urlrelay.'''
-
-import re
+
 import urlrelay
 import unittest
 
     return ['PUT']    
 
 @urlrelay.url(r'^/argtest/(?P<kwarg1>\w+)/(?P<kwarg2>\w+)/(\w+)/(\w+)$')
-def args(environ, start_response):
+def argtests(environ, start_response):
     start_response('200 Ok', [('Content-Type', 'text/plain')])
     args, kwargs = environ['wsgiorg.routing_args']
     return [' '.join([args[0], args[1], kwargs['kwarg1'], kwargs['kwarg2']])]
 def meth_get(environ, start_response):
     start_response('200 Ok', [('Content-Type', 'text/plain')])
     args, kwargs = environ['wsgiorg.routing_args']
-    return [' '.join(['GET', args[0], args[1], kwargs['kwarg1'], kwargs['kwarg2']])] 
+    return [' '.join(
+        ['GET', args[0], args[1], kwargs['kwarg1'], kwargs['kwarg2']]
+    )] 
 
 @urlrelay.url(r'^/methtest/(?P<kwarg1>\w+)/(?P<kwarg2>\w+)/(\w+)/(\w+)$', 'POST')
-def meth_get(environ, start_response):
+def meth_post2(environ, start_response):
     start_response('200 Ok', [('Content-Type', 'text/plain')])
     args, kwargs = environ['wsgiorg.routing_args']
-    return [' '.join(['POST', args[0], args[1], kwargs['kwarg1'], kwargs['kwarg2']])] 
+    return [' '.join(
+        ['POST', args[0], args[1], kwargs['kwarg1'], kwargs['kwarg2']]
+    )] 
 
 @urlrelay.url(r'^/methtest/(?P<kwarg1>\w+)/(?P<kwarg2>\w+)/(\w+)/(\w+)$', 'PUT')
-def meth_get(environ, start_response):
+def meth_put(environ, start_response):
     start_response('200 Ok', [('Content-Type', 'text/plain')])
     args, kwargs = environ['wsgiorg.routing_args']
-    return [' '.join(['PUT', args[0], args[1], kwargs['kwarg1'], kwargs['kwarg2']])]
+    return [' '.join(
+        ['PUT', args[0], args[1], kwargs['kwarg1'], kwargs['kwarg2']]
+    )]
+
+@urlrelay.url(r'/spectest/', 'POST')
+def meth_post(environ, start_response):
+    start_response('200 Ok', [('Content-Type', 'text/plain')])
+    return ['Empty post']
+
+@urlrelay.url(r'/spectest/(.*)', 'GET')
+def meth_get2(environ, start_response):
+    start_response('200 Ok', [('Content-Type', 'text/plain')])
+    args, kwargs = environ['wsgiorg.routing_args']
+    return [' '.join(['GET', args[0]])]
 
 urlrelay.register('^/notfound$', 'urlrelay._handler')
 urlrelay.register('^/notfound2$', 'urlrelay._handler', 'GET')
         environ = {'PATH_INFO':'/notfound',
             'REQUEST_METHOD':'GET'}
         result = urlrelay.URLRelay()(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /notfound was not found on this server.')
+        self.assertEqual(
+            result[0], 
+            'Requested URL /notfound was not found on this server.'
+        )
 
     def test_offdisk_method_get(self):
         '''Checks loading handler off of a disk + get method.'''
         environ = {'PATH_INFO':'/notfound2',
             'REQUEST_METHOD':'GET'}
         result = urlrelay.URLRelay()(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /notfound2 was not found on this server.')
+        self.assertEqual(
+            result[0], 
+            'Requested URL /notfound2 was not found on this server.'
+        )
 
     def test_offdisk_method_put(self):
         '''Checks loading handler off of a disk + put method.'''
         environ = {'PATH_INFO':'/notfound2',
             'REQUEST_METHOD':'PUT'}
         result = urlrelay.URLRelay()(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /notfound2 was not found on this server.')
+        self.assertEqual(
+            result[0], 
+            'Requested URL /notfound2 was not found on this server.'
+        )
 
     def test_offdisk_method_post(self):
         '''Checks loading handler off of a disk + post method.'''
         environ = {'PATH_INFO':'/notfound2',
             'REQUEST_METHOD':'POST'}
         result = urlrelay.URLRelay()(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /notfound2 was not found on this server.')         
+        self.assertEqual(
+            result[0], 
+            'Requested URL /notfound2 was not found on this server.'
+        )         
 
     def test_offdisk_method_modpath(self):
         '''Checks loading handler off of a disk.'''
         environ = {'PATH_INFO':'/notfound4',
             'REQUEST_METHOD':'GET'}
         result = urlrelay.URLRelay(modpath='urlrelay')(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /notfound4 was not found on this server.')
+        self.assertEqual(
+            result[0], 
+            'Requested URL /notfound4 was not found on this server.'
+        )
 
     def test_offdisk_method_get_modpath(self):
         '''Checks loading handler off of a disk + get method.'''
         environ = {'PATH_INFO':'/notfound3',
             'REQUEST_METHOD':'GET'}
         result = urlrelay.URLRelay(modpath='urlrelay')(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /notfound3 was not found on this server.')
+        self.assertEqual(
+            result[0], 
+            'Requested URL /notfound3 was not found on this server.'
+        )
 
     def test_offdisk_method_put_modpath(self):
         '''Checks loading handler off of a disk + put method.'''
         environ = {'PATH_INFO':'/notfound3',
             'REQUEST_METHOD':'PUT'}
         result = urlrelay.URLRelay(modpath='urlrelay')(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /notfound3 was not found on this server.')
+        self.assertEqual(
+            result[0], 
+            'Requested URL /notfound3 was not found on this server.'
+        )
 
     def test_offdisk_method_post_modpath(self):
         '''Checks loading handler off of a disk + post method.'''
         environ = {'PATH_INFO':'/notfound3',
             'REQUEST_METHOD':'POST'}
         result = urlrelay.URLRelay(modpath='urlrelay')(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /notfound3 was not found on this server.')
+        self.assertEqual(
+            result[0], 
+            'Requested URL /notfound3 was not found on this server.'
+        )
 
     def test_inmem_defaultapp(self):
         '''Checks using default app url.'''
     def test_inmem_defaultapp_args(self):
         '''Checks arg/kwarg extraction from URL + put method.'''
         environ = {'PATH_INFO':'/methkwarg1', 'REQUEST_METHOD':'GET'}
-        result = urlrelay.URLRelay(default=args, kwargs={'kwarg1':'kwarg1',
+        result = urlrelay.URLRelay(default=argtests, kwargs={'kwarg1':'kwarg1',
             'kwarg2':'kwarg2'}, args=('arg1','arg2'))(environ, dummy_sr)
         self.assertEqual(result[0], 'arg1 arg2 kwarg1 kwarg2')
 
-    def test_inmem_defaultapp(self):
+    def test_inmem_defaultapp2(self):
         '''Checks using default app url.'''
         environ = {'PATH_INFO':'/plt', 'REQUEST_METHOD':'GET'}
         result = urlrelay.URLRelay(default=index)(environ, dummy_sr)
     def test_ondisk_defaultapp(self):
         '''Checks arg/kwarg extraction from URL + put method.'''
         environ = {'PATH_INFO':'/methkwarg1', 'REQUEST_METHOD':'GET'}
-        result = urlrelay.URLRelay(default='urlrelay._handler')(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /methkwarg1 was not found on this server.')
+        result = urlrelay.URLRelay(
+            default='urlrelay._handler'
+        )(environ, dummy_sr)
+        self.assertEqual(
+            result[0], 
+            'Requested URL /methkwarg1 was not found on this server.'
+        )
 
     def test_ondisk_defaultapp_modpath(self):
         '''Checks arg/kwarg extraction from URL + put method.'''
         environ = {'PATH_INFO':'/methkwarg1', 'REQUEST_METHOD':'GET'}
-        result = urlrelay.URLRelay(default='_handler', modpath='urlrelay')(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /methkwarg1 was not found on this server.')
+        result = urlrelay.URLRelay(
+            default='_handler', 
+            modpath='urlrelay'
+        )(environ, dummy_sr)
+        self.assertEqual(
+            result[0], 
+            'Requested URL /methkwarg1 was not found on this server.'
+        )
 
     def test_handler_override(self):
         '''Checks overriding the default 404 handler.'''
         '''Checks that default 404 handler responds.'''
         environ = {'PATH_INFO':'/methkwarg1', 'REQUEST_METHOD':'GET'}
         result = urlrelay.URLRelay()(environ, dummy_sr)
-        self.assertEqual(result[0], 'Requested URL /methkwarg1 was not found on this server.')
+        self.assertEqual(
+            result[0], 
+            'Requested URL /methkwarg1 was not found on this server.'
+        )
 
     def test_nonregistry_paths_index(self):
         '''Checks use of non-global path registry.'''
         environ = {'PATH_INFO':'/', 'REQUEST_METHOD':'GET'}        
-        tpaths = ((r'^/$', index), (r'^/handle$', {'GET':get_handle,'POST':post_handle,
-            'PUT':put_handle}))
+        tpaths = (
+            (r'^/$', index), 
+            (r'^/handle$', {
+                'GET':get_handle,'POST':post_handle, 'PUT':put_handle
+            })
+        )
         result = urlrelay.URLRelay(paths=tpaths)(environ, dummy_sr)
         self.assertEqual(result[0], 'index')
 
     def test_nonregistry_paths_get(self):
         '''Checks use of non-global path registry.'''
         environ = {'PATH_INFO':'/handle', 'REQUEST_METHOD':'GET'}        
-        tpaths = ((r'^/$', index), (r'^/handle$', {'GET':get_handle,'POST':post_handle,
-            'PUT':put_handle}))
+        tpaths = (
+            (r'^/$', index),
+            (r'^/handle$', {
+                'GET':get_handle,'POST':post_handle, 'PUT':put_handle
+            })
+        )
         result = urlrelay.URLRelay(paths=tpaths)(environ, dummy_sr)
         self.assertEqual(result[0], 'GET')
 
     def test_nonregistry_paths_post(self):
         '''Checks use of non-global path registry.'''
         environ = {'PATH_INFO':'/handle', 'REQUEST_METHOD':'POST'}        
-        tpaths = ((r'^/$', index), (r'^/handle$', {'GET':get_handle,'POST':post_handle,
-            'PUT':put_handle}))
+        tpaths = (
+            (r'^/$', index), 
+            (r'^/handle$', {
+                'GET':get_handle,'POST':post_handle, 'PUT':put_handle
+            })
+        )
         result = urlrelay.URLRelay(paths=tpaths)(environ, dummy_sr)
         self.assertEqual(result[0], 'POST')
 
     def test_nonregistry_paths_put(self):
         '''Checks use of non-global path registry.'''
         environ = {'PATH_INFO':'/handle', 'REQUEST_METHOD':'PUT'}        
-        tpaths = ((r'^/$', index), (r'^/handle$', {'GET':get_handle,'POST':post_handle,
-            'PUT':put_handle}))
+        tpaths = (
+            (r'^/$', index),
+            (r'^/handle$', {
+                'GET':get_handle,'POST':post_handle, 'PUT':put_handle
+            })
+        )
         result = urlrelay.URLRelay(paths=tpaths)(environ, dummy_sr)
         self.assertEqual(result[0], 'PUT')
 
         def tempfunc(): 
             test = urlrelay.URLRelay(paths=((r'^/$', []),))
             test(environ, dummy_sr)
-        self.assertRaises(AssertionError, tempfunc)           
-        
-
-if __name__ == '__main__': unittest.main()
-
+        self.assertRaises(AssertionError, tempfunc)   
+        
+    def test_call_more_specific(self):
+        '''Checks calling a more-specific URL than one that has a method
+        associated with it.'''
+        environ = {'PATH_INFO': '/spectest/foo', 'REQUEST_METHOD': 'GET'}
+        result = urlrelay.URLRelay()(environ, dummy_sr)
+        self.assertEquals('GET foo', result[0])        
+    
+if __name__ == '__main__': unittest.main()

File urlrelay/urlrelay.py

-# Copyright (c) 2006 L. C. Rees
+# Copyright (c) 2006-2008 L. C. Rees
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
 
 '''RESTful WSGI URL dispatcher.'''
 
+import re
+import time
+import copy
+import random
+import threading
+from fnmatch import translate
+
 __author__ = 'L.C. Rees (lcrees@gmail.com)'
-__revision__ = '0.6'
+__revision__ = '0.7'
+__all__ = ['URLRelay', 'url', 'register']
 
-import re
+def synchronized(func):
+    '''Decorator to lock and unlock a method (Phillip J. Eby).
 
-__all__ = ['URLRelay', 'url', 'register']
+    @param func Method to decorate
+    '''
+    def wrapper(self, *__args, **__kw):
+        self._lock.acquire()
+        try:
+            return func(self, *__args, **__kw)
+        finally:
+            self._lock.release()
+    wrapper.__name__ = func.__name__
+    wrapper.__dict__ = func.__dict__
+    wrapper.__doc__ = func.__doc__
+    return wrapper
+
+def _handler(environ, start_response):
+    start_response('404 Not Found', [('content-type', 'text/plain')])
+    return ['Requested URL was not found on this server.']
+
+def pattern_compile(pattern, pattern_type):
+    '''Compile pattern.'''
+    # Convert glob expression to regex
+    if pattern_type == 'glob': pattern = translate(pattern)
+    return re.compile(pattern)
 
 
 class _Registry(object):
 
     def __iter__(self):
         '''Iterator for registry.'''
-        return iter(self._register)
+        return iter(self._register)   
 
     def add(self, pattern, mapping):
         '''Add tuple to registry.
 
         @param pattern URL pattern
         @param mapping WSGI application mapping
-        '''
+        '''        
         self._register.append((pattern, mapping))
 
     def get(self):
         return tuple(self._register)
 
 
-# URL registry and cache
-_reg, _cache = _Registry(), dict()
-
-def _handler(environ, start_response):
-    '''Default HTTP 404 handler.'''
-    path = environ['PATH_INFO']
-    start_response('404 Not Found', [('content-type', 'text/plain')])
-    return ['Requested URL %s was not found on this server.' % path]
+# URL registry
+_reg = _Registry()
 
 def register(pattern, application, method=None):
     '''Registers a pattern, application, and optional HTTP method.
         for entry in _reg:
             if entry[0] == pattern:
                 entry[1][method] = application
-                return
+                return None
         # Add new registry entry
         _reg.add(pattern, {method:application})
 
     return decorator
 
 
+class lazy(object):
+
+    '''Lazily assign attributes on an instance upon first use.'''
+
+    def __init__(self, method):
+        self.method = method
+        self.name = method.__name__
+
+    def __get__(self, instance, cls):
+        if instance is None: return self
+        value = self.method(instance)
+        setattr(instance, self.name, value)
+        return value
+
+
+class MemoryCache(object):
+
+    '''Base Cache class.'''    
+    
+    def __init__(self, **kw):
+        # Set timeout
+        timeout = kw.get('timeout', 300)
+        try:
+            timeout = int(timeout)
+        except (ValueError, TypeError):
+            timeout = 300
+        self.timeout = timeout
+        # Get random seed
+        random.seed()
+        self._cache = dict()
+        # Set max entries
+        max_entries = kw.get('max_entries', 300)
+        try:
+            self._max_entries = int(max_entries)
+        except (ValueError, TypeError):
+            self._max_entries = 300
+        # Set maximum number of items to cull if over max
+        self._maxcull = kw.get('maxcull', 10)
+        self._lock = threading.Condition()
+
+    def __getitem__(self, key):
+        '''Fetch a given key from the cache.'''
+        return self.get(key)
+
+    def __setitem__(self, key, value):
+        '''Set a value in the cache. '''
+        self.set(key, value)
+
+    def __delitem__(self, key):
+        '''Delete a key from the cache.'''
+        self.delete(key) 
+
+    def __contains__(self, key):
+        '''Tell if a given key is in the cache.'''
+        return self.get(key) is not None   
+
+    def get(self, key, default=None):
+        '''Fetch a given key from the cache.  If the key does not exist, return
+        default, which itself defaults to None.
+
+        @param key Keyword of item in cache.
+        @param default Default value (default: None)
+        '''
+        values = self._cache.get(key)
+        if values is None: 
+            value = default
+        elif values[0] < time.time():
+            self.delete(key)
+            value = default
+        else:
+            value = values[1]
+        return copy.deepcopy(value)
+
+    def set(self, key, value):
+        '''Set a value in the cache.
+
+        @param key Keyword of item in cache.
+        @param value Value to be inserted in cache.
+        '''
+        # Cull timed out values if over max # of entries
+        if len(self._cache) >= self._max_entries: self._cull()
+        # Set value and timeout in cache
+        self._cache[key] = (time.time() + self.timeout, value)
+
+    def delete(self, key):
+        '''Delete a key from the cache, failing silently.
+
+        @param key Keyword of item in cache.
+        '''
+        try:
+            del self._cache[key]
+        except KeyError: 
+            pass
+
+    def keys(self):
+        '''Returns a list of keys in the cache.'''
+        return self._cache.keys()
+
+    def _cull(self):
+        '''Remove items in cache to make room.'''
+        num, maxcull = 0, self._maxcull
+        # Cull number of items allowed (set by self._maxcull)
+        for key in self.keys():
+            # Remove only maximum # of items allowed by maxcull
+            if num <= maxcull:
+                # Remove items if expired
+                if self.get(key) is None: num += 1
+            else: break
+        # Remove any additional items up to max # of items allowed by maxcull
+        while len(self.keys()) >= self._max_entries and num <= maxcull:
+            # Cull remainder of allowed quota at random
+            self.delete(random.choice(self.keys()))
+            num += 1   
+
+
 class URLRelay(object):
 
     '''Passes HTTP requests to a WSGI callable based on URL path component and
     HTTP request method.
     '''
 
-    def __init__(self, **k):
+    def __init__(self, **kw):
+        # Values can be 'regex' or 'glob'
+        pattern_type = kw.get('pattern_type', 'regex')
         # Add any iterable of pairs consisting of a path pattern and either a
         # callback name or a dictionary of HTTP method/callback names
-        self.paths = tuple((re.compile(u), v) for u, v in k.get('paths', _reg.get()))
+        self._paths = tuple(
+            (pattern_compile(u, pattern_type), v) 
+            for u, v in kw.get('paths', _reg.get())
+        )
         # Shortcut for full module search path
-        self.modpath = k.get('modpath', '')
+        self._modpath = kw.get('modpath', '')
         # 404 handler
-        self.handler = k.get('handler', _handler)
+        self._response = kw.get('handler', _handler)
         # Default function
-        self.default = k.get('default')
-        # Default positional and keyword arguments
-        self.args, self.kwargs = k.get('args', ()), k.get('kwargs', {})
-        # routing_args environ keyword
-        self.key = k.get('key', 'wsgiorg.routing_args')
-        # Key for passing kwargs individually
-        self.kkey = k.get('kkey', 'wsgize.kwargs')
-        # Key for passing args individually
-        self.akey = k.get('akey', 'wsgize.args')
-        # URL <-> callable mapping Cache 
-        self.cache = k.get('cache', _cache)
+        self._default = kw.get('default')
+        # Set maximum number of items to cull from cache if over max
+        self._maxcull = kw.get('maxcull', 10)
+        # Set cache max entries
+        self._max_entries = kw.get('max_entries', 300)
+        # Set cache time out
+        self._timeout = kw.get('timeout', 300)        
 
     def __call__(self, env, start_response):
         try:
             # Fetch app and any positional or keyword arguments in path
             app, arg, kw = self.resolve(env['PATH_INFO'], env['REQUEST_METHOD'])
             # Place args in environ dictionary
-            env[self.key], env[self.akey], env[self.kkey] = (arg, kw), arg, kw
-            return app(env, start_response)
+            env['wsgiorg.routing_args'] = (arg, kw)            
         except (ImportError, AttributeError):
             # Return 404 handler for any exceptions
-            return self.handler(env, start_response)    
+            return self._response(env, start_response) 
+        return app(env, start_response)
+    
+    @lazy
+    def _cache(self):
+        '''URL <-> callable mapping Cache.'''
+        return MemoryCache(
+            timeout=self._timeout, 
+            maxcull=self._maxcull, 
+            max_entries=self._max_entries,
+        )
 
-    def getapp(self, app):
+    def _getapp(self, app):
         '''Loads a callable based on its name
+    
+        @param app An WSGI application'''
+        if isinstance(app, basestring):
+            try:
+                # Add shortcut to module if present
+                dot = app.rindex('.')
+                # Import module
+                return getattr(__import__(app[:dot], '', '', ['']), app[dot+1:])
+            # If nothing but module name, import the module
+            except ValueError:
+                return __import__(app, '', '', [''])
+        return app        
+    
+    def _loadapp(self, app):
+        '''Loads an application based on its name.
 
-        @param app An WSGI application's name'''
-        # Add shortcut to module if present
-        if self.modpath != '': app = '.'.join([self.modpath, app])
-        dot = app.rindex('.')
-        # Import module
-        return getattr(__import__(app[:dot], '', '', ['']), app[dot+1:])        
-
-    def relay(self, environ, start_response):
-        '''Legacy method.'''
-        return self.__call__(environ, start_response)
+        @param app Web application name'''
+        # Add module shortcut to module string
+        if self._modpath != '' and isinstance(app, basestring):
+            app = '.'.join([self._modpath, app])
+        newapp = self._getapp(app)
+        # Cache for future use
+        self._cache[app] = newapp
+        return newapp
 
     def resolve(self, path, method):
         '''Fetches a WSGI app based on URL path component and method.
         key = ':'.join([path, method])
         # Try fetching app and positional and keyword args from cache
         try:
-            return self.cache[key]
+            return self._cache[key]
         except KeyError:
-            # Loop through path patterns <-> app mappings
-            for pattern, app in self.paths:
+            # Loop through path patterns -> applications
+            for pattern, app in self._paths:
                 # Test path for match
                 search = pattern.search(path)
-                # Continue with next iteration if not a match
-                if not search: continue                
-                # Load app from storage if module name string
-                if isinstance(app, basestring):
-                    app = self.getapp(app)
-                # Get app linked to HTTP method
-                elif isinstance(app, dict):
+                # Continue with next iteration if no match
+                if not search: continue
+                # Get any app specifically linked to an HTTP method
+                if isinstance(app, dict): 
+                    if app is None: continue
                     app = app.get(method)
-                    # Load app from storage if module name string
-                    if isinstance(app, basestring): app = self.getapp(app)
-                # Ensure we have a callable object
+                app = self._loadapp(app)
+                # Ensure we have a callable
                 assert hasattr(app, '__call__')
-                # Extract any positional or keywords arguments in the path
-                args, kwargs = search.groups(), search.groupdict()
-                # Eliminate keyword values in args
-                args = tuple(i for i in args if i not in kwargs.itervalues())
-                # Cache callable, positional and keyword arguments
-                self.cache[key] = (app, args, kwargs)                
-                return app, args, kwargs
-            # Return defaults if no matching path and default app is set
-            if self.default is not None:
-                default = self.default
-                # Load default app from storage if module name string
-                if isinstance(default, basestring):
-                    default = self.getapp(default)
-                return default, self.args, self.kwargs
-            else:
-                raise ImportError()
+                # Extract any keyword arguments in the path
+                kw = search.groupdict()
+                # Extract any positional arguments, eliminating any duplicates
+                args = tuple(i for i in search.groups())
+                # Cache app, positional and keyword arguments
+                self._cache[key] = (app, args, kw)
+                return app, args, kw
+            # Return default app if no matching path and default app is set
+            if self._default is not None:
+                default = self._loadapp(self._default)
+                return default, (), {}
+            raise ImportError()