summaryrefslogtreecommitdiff
path: root/repoze/bfg/urldispatch.py
blob: 4088cef129fd404acce84ede3d62b02520666196 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import re
from urllib import unquote

from zope.interface import directlyProvides

from repoze.bfg.interfaces import IRouteRequest

from repoze.bfg.traversal import traversal_path
from repoze.bfg.traversal import quote_path_segment
from repoze.bfg.encode import url_quote

_marker = object()

class Route(object):
    def __init__(self, path, name=None, factory=None):
        self.path = path
        self.match, self.generate = _compile_route(path)
        self.name = name
        self.factory = factory

class RoutesRootFactory(object):
    def __init__(self, default_root_factory):
        self.default_root_factory = default_root_factory
        self.routelist = []
        self.routes = {}

    def has_routes(self):
        return bool(self.routelist)

    def get_routes(self):
        return self.routelist

    def connect(self, path, name, factory=None):
        route = Route(path, name, factory)
        self.routelist.append(route)
        self.routes[name] = route
        return route

    def generate(self, name, kw):
        return self.routes[name].generate(kw)

    def __call__(self, request):
        try:
            # As of BFG 1.1a9, a root factory is now typically called
            # with a request object (instead of a WSGI environ, as in
            # previous versions) by the router.  Simultaneously, as of
            # 1.1a9, the RoutesRootFactory *requires* that the object
            # passed to it be a request, instead of an environ, as it
            # uses both the ``registry`` attribute of the request, and
            # if a route is found, it decorates the object with an
            # interface using directlyProvides.  However, existing app
            # code "in the wild" calls the root factory explicitly
            # with a dictionary argument (e.g. a subscriber to
            # WSGIApplicationCreatedEvent does
            # ``app.root_factory({})``).  It makes no sense for such
            # code to depend on the side effects of a
            # RoutesRootFactory, for bw compat purposes we catch the
            # exception that will be raised when passed a dictionary
            # and just return the result of the default root factory.
            environ = request.environ
            registry = request.registry
        except AttributeError:
            return self.default_root_factory(request)

        try:
            path = environ['PATH_INFO']
        except KeyError:
            path = '/'
        for route in self.routelist:
            match = route.match(path)
            if match is not None:
                environ['wsgiorg.routing_args'] = ((), match)
                environ['bfg.routes.route'] = route
                environ['bfg.routes.matchdict'] = match
                request.matchdict = match
                iface = registry.queryUtility(IRouteRequest, name=route.name)
                if iface is not None:
                    directlyProvides(request, iface)
                factory = route.factory or self.default_root_factory
                return factory(environ)

        return self.default_root_factory(environ)

# stolen from bobo and modified
route_re = re.compile(r'(/:[a-zA-Z]\w*)')
def _compile_route(route):
    if not route.startswith('/'):
        route = '/' + route
    star = None
    if '*' in route:
        route, star = route.rsplit('*', 1)
    pat = route_re.split(route)
    pat.reverse()
    rpat = []
    gen = []
    prefix = pat.pop()
    if prefix:
        rpat.append(re.escape(prefix))
        gen.append(prefix)
    while pat:
        name = pat.pop()
        name = name[2:]
        gen.append('/%%(%s)s' % name)
        name = '/(?P<%s>[^/]*)' % name
        rpat.append(name)
        s = pat.pop()
        if s:
            rpat.append(re.escape(s))
            gen.append(s)

    if star:
        rpat.append('(?P<%s>.*?)' % star)
        gen.append('%%(%s)s' % star)

    pattern = ''.join(rpat) + '$'

    match = re.compile(pattern).match
    def matcher(path):
        m = match(path)
        if m is None:
            return m
        d = {}
        for k,v in m.groupdict().iteritems():
            if k is not None:
                if k == star:
                    d[k] = traversal_path(v)
                else:
                    d[k] = unquote(v).decode('utf-8')
        return d
                    

    gen = ''.join(gen)
    def generator(dict):
        newdict = {}
        for k, v in dict.items():
            if isinstance(v, unicode):
                v = v.encode('utf-8')
            if k == star and hasattr(v, '__iter__'):
                v = '/'.join([quote_path_segment(x) for x in v])
            elif k != star:
                try:
                    v = url_quote(v)
                except TypeError:
                    pass
            newdict[k] = v
        return gen % newdict

    return matcher, generator