Commits

imbolc committed b6df229

routing refactoring

Comments (0)

Files changed (6)

 from routing import urls, add_apps, add_urls, url4, auto_routing
 from decorators import as_response, as_text, as_html, as_json, to_template
 from decorators import route, error, basic_auth, anticache, view
-from template import render_to_string, render_to_response
+from template import render_to_string, render_to_response
+import signal
     CONTEXT_PROCESSORS = []
     REQUEST_MIDDLEWARES = []
     RESPONSE_MIDDLEWARES = []
+    MIDDLEWARES = []
     CHARSET = 'utf-8'
     POST_MAX_SIZE = 1024 * 1024 * 1  # максимальный размер пост-данных, None - не ограничивать
     POST_MAX_MEMFILE = 1024 * 100  # максимальный размер POST-данных, загружаемый в память. Если больше, создаётся временный файл.
 import urllib
 import datetime
 
-from exceptions import NotFound
-from config import cfg
-import wsgi
+def main():
+    # добавление
+    m = Map()
+    m.add_rule('/user/<int:id>/', 'user1', 'user')
+    m.add_rule('/user/<int:id>/<str:name>/', 'user2', 'user')
+    m.add_rule('/user/<*:slug>', 'user3', 'user')
+    
+    from om.pp import pp
+    #~ pp(m.queue)
+    #~ pp(m.rev)
+    #~ return
+    
+    print m.find('/user/41/---/')
+    print m.reverse('user', id=1)
+    print m.reverse('user', id=1, name='pysi', x=4)
+    print m.reverse('user', slug='ddd/bbb/sss/')
 
 
-class UrlMap(object):
+class Map(object):
+    queue = []
+    rev = {}
+    charset = 'utf-8'
     url_prefix = None
-    tree = []
-    rev = {}
-    
-    def add_rule(self, url, target, name=None):
+
+    def add_rule(self, url, target, urlname=None):
         if self.url_prefix:
             url = '/%s/%s' % (self.url_prefix, url.lstrip('/'))
-        dirs = url.split('/')[1:]
-        cur = self.tree
-        vars = []
+        match_funcs = []  # функции проверки папок пути
+        path_size = None  # длина пути, -1 = бесконечный
         rev_chunks = ['']
-        for i, dir in enumerate(dirs):
-            raw_dir = dir
+        var_names = []
+        for dir in url.lstrip('/ ').split('/'):
+            func = get_match(dir)
+            if func.__name__ == 'slug_dir':
+                path_size = -1
+            match_funcs.append(func)
             if dir.startswith('<') and dir.endswith('>'):
-                dir, var = dir[1:-1].split(':')
-                rev_chunks.append(REVERSES[dir](var))
-                dir = '<%s>' % dir
-                vars.append(var)
+                var_type, var_name = dir[1:-1].split(':')
+                rev_chunks.append(REVERSES[var_type](var_name))
+                var_names.append(var_name)
             else:
                 rev_chunks.append(dir)
-            node = dict(cur).get(dir)
-            if not node:
-                node = {
-                    'target': None,
-                    'next': [],
-                    'match': get_match(raw_dir),
-                    'vars': vars,
-                }
-                cur.append((dir, node))
-            if i == len(dirs) - 1 and node['target'] is None:
-                # последняя папка
-                node['target'] = target
-            cur = node['next']
-        if name:
-            self.rev.setdefault(name, []).append(
-                ('/'.join(rev_chunks), vars))
-                
-    def reverse(self, *args, **kwargs):
-        name = args[0]
-        for tpl, vars in self.rev.get(name):
-            for k in vars:
-                if k not in kwargs:
-                    break
+        if path_size is None:
+            path_size = len(match_funcs)
+        self.queue.append((match_funcs, path_size, var_names, target))
+        if urlname:
+            self.rev.setdefault(urlname, []).append(
+                ('/'.join(rev_chunks), var_names))
+            if len(self.rev[urlname]) > 1:
+                # сортируем, чтобы reverse использовал максимум переменных
+                self.rev[urlname].sort(key=lambda x: -len(x[1]))
+
+    def find(self, url):
+        dirs = url.lstrip('/ ').split('/')
+        for match_funcs, path_size, var_names, target in self.queue:
+            if path_size == -1 or path_size == len(dirs):
+                vals = []
+                for i, func in enumerate(match_funcs):
+                    match, val = func(dirs[i])
+                    if match:
+                        if val is not None:
+                            if val == '<//__slug__//>':
+                                vals.append('/'.join(dirs[i:]))
+                            else:
+                                vals.append(val)
+                    else:
+                        break
+                else:
+                    return target, dict(zip(var_names, vals)) if vals else {}
+        return None, None
+
+    def reverse(self, urlname, **kwargs):
+        try:
+            items = self.rev[urlname]
+        except KeyError:
+            assert 0, 'Could not find url for "%s"' % urlname
+        # выбераем с максимальным количеством используемых переменных
+        for tpl, vars in items:
+            try:
+                url = urllib.quote(tpl % kwargs)
+            except (TypeError, KeyError):
+                pass
             else:
-                try:
-                    url = urllib.quote(tpl % kwargs)
-                except (TypeError, KeyError):
-                    pass
-                else:
-                    # GET параметры
-                    if len(kwargs) > len(vars):
-                        ext_vars = {}
-                        for k, v in kwargs.iteritems():
-                            if k not in vars:
-                                if isinstance(v, unicode):
-                                    v = v.encode(cfg.CHARSET)
-                                ext_vars[k] = v
-                        if ext_vars:
-                            url += '?' + urllib.urlencode(ext_vars)
-                    return url
-    
-    def find(self, url):
-        dirs = url.split('/')[1:]
-        cur = self.tree
-        res = None, {}
-        vals = []
-        i = 0
-        while i < len(dirs):
-            for d, node in cur:
-                match, val = node['match'](dirs[i])
-                if match:
-                    if val is not None:
-                        if val == '<//__slug__//>':
-                            vals.append('/'.join(dirs[i:]))
-                            i = len(dirs) - 1
-                        else:
-                            vals.append(val)
-                    break
-            else:
-                break
-            cur = node['next']
-            i += 1
-        else:
-            vars = dict((node['vars'][i], vals[i])
-                for i in xrange(len(vals))) if vals else {}
-            res = node['target'], vars
-        return res
-        
+                # GET параметры
+                if len(kwargs) > len(vars):
+                    ext_vars = {}
+                    for k, v in kwargs.iteritems():
+                        if k not in vars:
+                            if isinstance(v, unicode):
+                                v = v.encode(self.charset)
+                            ext_vars[k] = v
+                    if ext_vars:
+                        url += '?' + urllib.urlencode(ext_vars)
+                return url
+        assert 0, 'Could not find url for "%s" witch kwargs: %s' % (urlname,
+            ', '.join('%s %s' % (k, type(v)) for k, v in kwargs.iteritems()))
+            
 def match_const(var):
-    return lambda dir: (dir == var, None)
+    def const_dir(dir):
+        return dir == var, None
+    return const_dir
 
 def match_str(var):
-    return lambda dir: (True, urllib.unquote_plus(dir))
+    def str_dir(dir):
+        return True, urllib.unquote_plus(dir)
+    return str_dir
     
 def match_int(var):
-    def f(dir):
+    def int_dir(dir):
         if dir.isdigit():
             return True, int(dir)
         else:
             return False, None
-    return f
+    return int_dir
 
 def match_date(var):
-    def f(dir):
+    def data_dir(dir):
         try:
             dt = datetime.datetime.strptime(dir, '%Y-%m-%d')
         except:
             return False, None
         return True, datetime.date(dt.year, dt.month, dt.day)
-    return f
+    return data_dir
 
 def match_slug(var):
-    return lambda dir: (True, '<//__slug__//>')
+    def slug_dir(dir):
+        return (True, '<//__slug__//>')
+    return slug_dir
 
 
 MATCHS = {
 }
 
 
-#######################
-### Привязка к pysi ###
-#######################
-urls = UrlMap()
+if __name__ == '__main__':
+    main()
+else:
+    #######################
+    ### Привязка к pysi ###
+    #######################
+    from exceptions import NotFound
+    from config import cfg
+    import wsgi
 
-def auto_routing(rq):
-    '''
-    Автоматический роутинг
-    '''
-    func, kwargs = urls.find(rq.path)
-    if func is None:
-        if not rq.path.endswith('/'):
-            # забыли закрывающий слеш?
-            func, kwargs = urls.find(rq.path + '/')
-            if func:
-                return wsgi.redirect(rq.path + '/')        
-        raise NotFound
-    return func(rq, **kwargs)
-    
-def add_urls(*rules):
-    ''' Добавление правил списком'''
-    for rule in rules:
-        urls.add_rule(*rule)
+
+    urls = Map()
+    urls.charset = cfg.CHARSET
+
+    def auto_routing(rq):
+        '''
+        Автоматический роутинг
+        '''
+        func, kwargs = urls.find(rq.path)
+        if func is None:
+            if not rq.path.endswith('/'):
+                # забыли закрывающий слеш?
+                func, kwargs = urls.find(rq.path + '/')
+                if func:
+                    return wsgi.redirect(rq.path + '/')        
+            raise NotFound
+        return func(rq, **kwargs)
         
-def url4(*args, **kwargs):
-    return urls.reverse(*args, **kwargs)
-    
-def add_apps(apps):
-    '''
-    Добавление приложений
-        apps = ['mod1', ('mod2', url_prefix), 'mod3']
-    '''
-    for mod in apps:
-        if isinstance(mod, tuple):
-            mod, prefix = mod
-            urls.url_prefix = prefix.strip().strip('/') or None
-        app_name = '%s.%s' % (mod, 'views')
-        __import__(app_name)
-        urls.url_prefix = None
+    def add_urls(*rules):
+        ''' Добавление правил списком'''
+        for rule in rules:
+            urls.add_rule(*rule)
+            
+    def url4(*args, **kwargs):
+        return urls.reverse(*args, **kwargs)
+        
+    def add_apps(apps):
+        '''
+        Добавление приложений
+            apps = ['mod1', ('mod2', url_prefix), 'mod3']
+        '''
+        for mod in apps:
+            if isinstance(mod, tuple):
+                mod, prefix = mod
+                urls.url_prefix = prefix.strip().strip('/') or None
+            app_name = '%s.%s' % (mod, 'views')
+            __import__(app_name)
+            urls.url_prefix = None
+    
 def list_obj_from_str(lst):
     for i, obj in enumerate(lst):
         lst[i] = obj_from_str(obj)
+    return lst
 
 class cached_property(object):
     def __init__(self, f):
         
         return status, list(self.headers.iterallitems()), [self.body]
         
-            
+
 class App(object):
     def __init__(self, cfg_module='cfg'):
         '''
         self.cfg_module = cfg_module
 
 
-    def setup(self):
+    def __call__(self, environ, start_response):
         '''
         Первый запуск
         '''
+        setattr(self.__class__, '__call__', self._call)
         def routing_queue(routing):
             list_obj_from_str(routing)
             def wrapper(rq):
         list_obj_from_str(cfg.RESPONSE_MIDDLEWARES)
 
         self.routing = obj_from_str(cfg.ROUTING)
-        if isinstance(self.routing, tuple) or isinstance(self.routing, list):
+        if isinstance(self.routing, (tuple, list)):
             self.routing = routing_queue(self.routing)
+            
+        return self._call(environ, start_response)
 
-        self.Request = Request
-
-    def __call__(self, environ, start_response):
-        try:
-            rq = self.Request(environ)
-        except AttributeError:
-            # первый запуск
-            self.setup()
-            rq = self.Request(environ)
+    def _call(self, environ, start_response):
+        rq = Request(environ)
         try:
             for middleware in cfg.REQUEST_MIDDLEWARES:
                 middleware(rq)
 from setuptools import setup
 
 PACKAGE = 'pysi'
-VERSION = '0.3'
+VERSION = '0.4'
 
 if __name__ == '__main__':
     # Compile the list of packages available, because distutils doesn't have