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 |