~azzar1/unity/add-show-desktop-key

1294.2.1 by William Grant
Add an object-traversal-based router.
1
# IVLE - Informatics Virtual Learning Environment
2
# Copyright (C) 2007-2009 The University of Melbourne
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18
"""Object traversal URL utilities."""
19
20
import os.path
21
22
ROOT = object() # Marker object for the root.
1294.2.15 by William Grant
Allow routes that take infinitely many arguments.
23
INF = object()
1294.2.1 by William Grant
Add an object-traversal-based router.
24
25
class RoutingError(Exception):
26
    pass
27
28
class NotFound(RoutingError):
29
    """The path did not resolve to an object."""
30
    pass
31
32
class InsufficientPathSegments(NotFound):
33
    """The path led to a route that expected more arguments."""
34
    pass
35
36
class NoPath(RoutingError):
37
    """There is no path from the given object to the root."""
38
    pass
39
40
class RouteConflict(RoutingError):
41
    """A route with the same discriminator is already registered."""
42
    pass
43
44
def _segment_path(path):
45
    """Split a path into its segments, after normalisation.
46
47
       >>> _segment_path('/path/to/something')
48
       ['path', 'to', 'something']
49
       >>> _segment_path('/path/to/something/')
50
       ['path', 'to', 'something']
51
       >>> _segment_path('/')
52
       []
53
    """
54
55
    segments = os.path.normpath(path).split('/')
56
57
    # Remove empty segments caused by leading and trailing seperators.
58
    if segments[0] == '':
59
        segments.pop(0)
60
    if segments[-1] == '':
61
        segments.pop()
62
    return segments
63
64
class Router(object):
65
    '''Router to resolve and generate paths.
66
67
    Maintains a registry of forward and reverse routes, dealing with paths
68
    to objects and views published in the URL space.
69
    '''
70
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
71
    def __init__(self, root, default='+index', viewset=None):
1294.2.1 by William Grant
Add an object-traversal-based router.
72
        self.fmap = {} # Forward map.
73
        self.rmap = {} # Reverse map.
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
74
        self.smap = {}
1294.2.4 by William Grant
Support generation of view URLs.
75
        self.srmap = {}
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
76
        self.vmap = {}
1294.2.4 by William Grant
Support generation of view URLs.
77
        self.vrmap = {}
1294.2.1 by William Grant
Add an object-traversal-based router.
78
        self.root = root
79
        self.default = default
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
80
        self.viewset = viewset
1294.2.1 by William Grant
Add an object-traversal-based router.
81
82
    def add_forward(self, src, segment, func, argc):
83
        """Register a forward (path resolution) route."""
84
85
        if src not in self.fmap:
86
            self.fmap[src] = {}
87
88
        # If a route already exists with the same source and name, we have a
89
        # conflict. We don't like conflicts.
90
        if segment in self.fmap[src]:
91
            raise RouteConflict((src, segment, func),
92
                                (src, segment, self.fmap[src][segment][0]))
93
94
        self.fmap[src][segment] = (func, argc)
95
96
    def add_reverse(self, src, func):
97
        """Register a reverse (path generation) route."""
98
99
        if src in self.rmap:
100
             raise RouteConflict((src, func), (src, self.rmap[src]))
101
        self.rmap[src] = func
102
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
103
    def add_view(self, src, name, cls, viewset=None):
104
        """Add a named view for a class, in the specified view set."""
105
106
        if src not in self.vmap:
107
            self.vmap[src] = {}
108
109
        if viewset not in self.vmap[src]:
110
            self.vmap[src][viewset] = {}
111
1294.2.4 by William Grant
Support generation of view URLs.
112
        if src not in self.vrmap:
113
            self.vrmap[src] = {}
114
115
        if name in self.vmap[src][viewset] or cls in self.vrmap[src]:
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
116
            raise RouteConflict((src, name, cls, viewset),
117
                         (src, name, self.vmap[src][viewset][name], viewset))
118
119
        self.vmap[src][viewset][name] = cls
1294.2.4 by William Grant
Support generation of view URLs.
120
        self.vrmap[src][cls] = (name, viewset)
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
121
122
    def add_set_switch(self, segment, viewset):
123
        """Register a leading path segment to switch to a view set."""
124
1294.2.7 by William Grant
Fix conflict checking in Router.add_set_switch.
125
        if segment in self.smap:
126
            raise RouteConflict((segment, viewset),
127
                                (segment, self.smap[segment]))
128
129
        if viewset in self.srmap:
130
            raise RouteConflict((segment, viewset),
131
                                (self.srmap[viewset], viewset))
1294.2.4 by William Grant
Support generation of view URLs.
132
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
133
        self.smap[segment] = viewset
1294.2.4 by William Grant
Support generation of view URLs.
134
        self.srmap[viewset] = segment
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
135
1294.2.1 by William Grant
Add an object-traversal-based router.
136
    def resolve(self, path):
137
        """Resolve a path into an object.
138
139
        Traverses the tree of routes using the given path.
140
        """
141
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
142
        viewset = self.viewset
143
        todo = _segment_path(path)
144
145
        # Override the viewset if the first segment matches.
1294.2.16 by William Grant
Don't crash when resolving a path with no segments.
146
        if len(todo) > 0 and todo[0] in self.smap:
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
147
            viewset = self.smap[todo[0]]
148
            del todo[0]
149
150
        (obj, view, subpath) = self._traverse(todo, self.root, viewset)
151
1294.2.12 by William Grant
Implement subpaths in resolution.
152
        return obj, view, subpath
1294.2.1 by William Grant
Add an object-traversal-based router.
153
1294.2.14 by William Grant
Implement subpaths in generation.
154
    def generate(self, obj, view=None, subpath=None):
1294.2.1 by William Grant
Add an object-traversal-based router.
155
        """Resolve an object into a path.
156
157
        Traverse up the tree of reverse routes, generating a path which
158
        resolves to the object.
159
        """
160
161
        # Attempt to get all the way to the top. Each reverse route should
162
        # return a (parent, pathsegments) tuple.
163
        curobj = obj
164
        names = []
165
166
        # None represents the root.
167
        while curobj is not ROOT:
168
            route = self.rmap.get(type(curobj))
169
            if route is None:
170
                raise NoPath(obj, curobj)
171
            (curobj, newnames) = route(curobj)
172
173
            # The reverse route can return either a string of one segment,
174
            # or a tuple of many.
175
            if isinstance(newnames, basestring):
176
                names.insert(0, newnames)
177
            else:
178
                names = list(newnames) + list(names)
179
1294.2.4 by William Grant
Support generation of view URLs.
180
        if view is not None:
181
            # If we don't have the view registered for this type, we can do
182
            # nothing.
183
            if type(obj) not in self.vrmap or \
184
               view not in self.vrmap[type(obj)]:
185
                raise NoPath(obj, view)
186
187
            (viewname, viewset) = self.vrmap[type(obj)][view]
188
189
            # If the view's set isn't the default one, we need to have it in
190
            # the map.
191
            if viewset != self.viewset:
192
                if viewset not in self.srmap:
193
                    raise NoPath(obj, view)
194
                else:
195
                    names = [self.srmap[viewset]] + names
196
197
            # Generate nice URLs for the default route, if it is the last.
198
            if viewname != self.default:
1294.2.44 by William Grant
Implement deep view generation.
199
                # Deep views may have multiple segments in their name.
200
                if isinstance(viewname, basestring):
201
                    names += [viewname]
1294.2.47 by William Grant
Implement default deep views.
202
                elif viewname[-1] == '+index' and not subpath:
203
                    # If the last segment of the path is the default view, we
204
                    # can omit it.
205
                    names += viewname[:-1]
1294.2.44 by William Grant
Implement deep view generation.
206
                else:
207
                    names += viewname
1294.2.1 by William Grant
Add an object-traversal-based router.
208
1294.2.14 by William Grant
Implement subpaths in generation.
209
        if subpath is not None:
210
            if isinstance(subpath, basestring):
211
                return os.path.join(os.path.join('/', *names), subpath)
212
            else:
213
                names += subpath
1294.2.1 by William Grant
Add an object-traversal-based router.
214
        return os.path.join('/', *names)
215
1294.2.14 by William Grant
Implement subpaths in generation.
216
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
217
    def _traverse(self, todo, obj, viewset):
1294.2.1 by William Grant
Add an object-traversal-based router.
218
        """Populate the object stack given a list of path segments.
219
220
        Traverses the forward route tree, using the given path segments.
221
222
        Intended to be used by route(), and nobody else.
223
        """
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
224
        while True:
225
            # Attempt views first, then routes.
226
            if type(obj) in self.vmap and \
227
               viewset in self.vmap[type(obj)]:
228
                # If there are no segments left, attempt the default view.
1294.2.26 by William Grant
Refactor view finding and subsequent NotFound checks.
229
                # Otherwise, look for a view with the name in the first
230
                # remaining path segment.
1294.2.42 by William Grant
Implement deep views.
231
                vnames = self.vmap[type(obj)][viewset]
232
                view = vnames.get(self.default if len(todo) == 0 else todo[0])
233
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
234
                if view is not None:
1294.2.12 by William Grant
Implement subpaths in resolution.
235
                    return (obj, view, tuple(todo[1:]))
1294.2.47 by William Grant
Implement default deep views.
236
                # Now we must check for deep views.
237
                # A deep view is one that has a name consisting of
238
                # multiple segments. It's messier than it could be, because
239
                # we also allow omission of the final segment if it is the
240
                # default view name.
1294.2.42 by William Grant
Implement deep views.
241
                elif len(todo) >= 2:
242
                    view = vnames.get(tuple(todo[:2]))
243
                    if view is not None:
244
                        return (obj, view, tuple(todo[2:]))
1294.2.47 by William Grant
Implement default deep views.
245
                elif len(todo) == 1:
246
                    # Augment it with the default view name, and look it up.
247
                    view = vnames.get((todo[0], self.default))
248
                    if view is not None:
249
                        return (obj, view, tuple(todo[2:]))
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
250
1294.2.26 by William Grant
Refactor view finding and subsequent NotFound checks.
251
            # If there are no segments left to use, or there are no routes, we
252
            # get out.
1294.2.45 by William Grant
Fix the NotFound args when a class has no forward routes.
253
            if len(todo) == 0:
1294.2.24 by William Grant
Always raise a NotFound if the path doesn't resolve to a view.
254
                raise NotFound(obj, '+index', ())
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
255
1294.2.45 by William Grant
Fix the NotFound args when a class has no forward routes.
256
            if type(obj) not in self.fmap:
257
                raise NotFound(obj, todo[0], todo[1:])
258
1294.2.30 by William Grant
Rename 'names' to 'routenames', so I stop confusing myself.
259
            routenames = self.fmap[type(obj)]
1294.2.1 by William Grant
Add an object-traversal-based router.
260
1294.2.31 by William Grant
Collapse route existence check.
261
            if todo[0] in routenames:
1294.2.32 by William Grant
De-duplicate route retrieval.
262
                routename = todo[0]
1294.2.1 by William Grant
Add an object-traversal-based router.
263
                # The first path segment is the route identifier, so we skip
264
                # it when identifying arguments.
1294.2.29 by William Grant
Refactor forward route argument calculation.
265
                argoffset = 1
1294.2.30 by William Grant
Rename 'names' to 'routenames', so I stop confusing myself.
266
            elif None in routenames:
1294.2.1 by William Grant
Add an object-traversal-based router.
267
                # Attempt traversal directly (with no intermediate segment)
268
                # as a last resort.
1294.2.32 by William Grant
De-duplicate route retrieval.
269
                routename = None
1294.2.29 by William Grant
Refactor forward route argument calculation.
270
                argoffset = 0
1294.2.27 by William Grant
Replace an if,else(if,else) with an if,elif,else.
271
            else:
1294.2.39 by William Grant
Make tests pass.
272
                raise NotFound(obj, todo[0], tuple(todo[1:]))
1294.2.1 by William Grant
Add an object-traversal-based router.
273
1294.2.32 by William Grant
De-duplicate route retrieval.
274
            route, argc = routenames[routename]
275
1294.2.29 by William Grant
Refactor forward route argument calculation.
276
            if argc is INF:
277
                args = todo[argoffset:]
278
                todo = []
279
            else:
280
                args = todo[argoffset:argc + argoffset]
281
                todo = todo[argc + argoffset:]
282
1294.2.15 by William Grant
Allow routes that take infinitely many arguments.
283
            if argc is not INF and len(args) != argc:
1294.2.1 by William Grant
Add an object-traversal-based router.
284
                # There were too few path segments left. Die.
1294.2.25 by William Grant
Raise more sensible NotFounds where multiple arguments are involved.
285
                raise InsufficientPathSegments(
286
                                obj,
287
                                tuple(args) if len(args) != 1 else args[0],
288
                                tuple(todo)
289
                                )
290
291
            newobj = route(obj, *args)
292
293
            if newobj is None:
294
                raise NotFound(obj, tuple(args) if len(args) != 1 else args[0],
295
                               tuple(todo))
296
297
            obj = newobj
1294.2.1 by William Grant
Add an object-traversal-based router.
298