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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
|
import re
import warnings
from inspect import getmembers, ismethod
from webob import exc
import six
from .secure import handle_security, cross_boundary
from .util import iscontroller, getargspec, _cfg
__all__ = ['lookup_controller', 'find_object', 'route']
__observed_controllers__ = set()
__custom_routes__ = {}
def route(*args):
"""
This function is used to define an explicit route for a path segment.
You generally only want to use this in situations where your desired path
segment is not a valid Python variable/function name.
For example, if you wanted to be able to route to:
/path/with-dashes/
...the following is invalid Python syntax::
class Controller(object):
with-dashes = SubController()
...so you would instead define the route explicitly::
class Controller(object):
pass
pecan.route(Controller, 'with-dashes', SubController())
"""
def _validate_route(route):
if not isinstance(route, six.string_types):
raise TypeError('%s must be a string' % route)
if not re.match('^[0-9a-zA-Z-_$\(\),;:]+$', route):
raise ValueError(
'%s must be a valid path segment. Keep in mind '
'that path segments should not contain path separators '
'(e.g., /) ' % route
)
if len(args) == 2:
# The handler in this situation is a @pecan.expose'd callable,
# and is generally only used by the @expose() decorator itself.
#
# This sets a special attribute, `custom_route` on the callable, which
# pecan's routing logic knows how to make use of (as a special case)
route, handler = args
if ismethod(handler):
handler = handler.__func__
if not iscontroller(handler):
raise TypeError(
'%s must be a callable decorated with @pecan.expose' % handler
)
obj, attr, value = handler, 'custom_route', route
if handler.__name__ in ('_lookup', '_default', '_route'):
raise ValueError(
'%s is a special method in pecan and cannot be used in '
'combination with custom path segments.' % handler.__name__
)
elif len(args) == 3:
# This is really just a setattr on the parent controller (with some
# additional validation for the path segment itself)
_, route, handler = args
obj, attr, value = args
if hasattr(obj, attr):
raise RuntimeError(
(
"%(module)s.%(class)s already has an "
"existing attribute named \"%(route)s\"." % {
'module': obj.__module__,
'class': obj.__name__,
'route': attr
}
),
)
else:
raise TypeError(
'pecan.route should be called in the format '
'route(ParentController, "path-segment", SubController())'
)
_validate_route(route)
setattr(obj, attr, value)
class PecanNotFound(Exception):
pass
class NonCanonicalPath(Exception):
'''
Exception Raised when a non-canonical path is encountered when 'walking'
the URI. This is typically a ``POST`` request which requires a trailing
slash.
'''
def __init__(self, controller, remainder):
self.controller = controller
self.remainder = remainder
def lookup_controller(obj, remainder, request=None):
'''
Traverses the requested url path and returns the appropriate controller
object, including default routes.
Handles common errors gracefully.
'''
if request is None:
warnings.warn(
(
"The function signature for %s.lookup_controller is changing "
"in the next version of pecan.\nPlease update to: "
"`lookup_controller(self, obj, remainder, request)`." % (
__name__,
)
),
DeprecationWarning
)
notfound_handlers = []
while True:
try:
obj, remainder = find_object(obj, remainder, notfound_handlers,
request)
handle_security(obj)
return obj, remainder
except (exc.HTTPNotFound, exc.HTTPMethodNotAllowed,
PecanNotFound) as e:
if isinstance(e, PecanNotFound):
e = exc.HTTPNotFound()
while notfound_handlers:
name, obj, remainder = notfound_handlers.pop()
if name == '_default':
# Notfound handler is, in fact, a controller, so stop
# traversal
return obj, remainder
else:
# Notfound handler is an internal redirect, so continue
# traversal
result = handle_lookup_traversal(obj, remainder)
if result:
# If no arguments are passed to the _lookup, yet the
# argspec requires at least one, raise a 404
if (
remainder == [''] and
len(obj._pecan['argspec'].args) > 1
):
raise e
obj_, remainder_ = result
return lookup_controller(obj_, remainder_, request)
else:
raise e
def handle_lookup_traversal(obj, args):
try:
result = obj(*args)
if result:
prev_obj = obj
obj, remainder = result
# crossing controller boundary
cross_boundary(prev_obj, obj)
return result
except TypeError as te:
msg = 'Got exception calling lookup(): %s (%s)'
warnings.warn(
msg % (te, te.args),
RuntimeWarning
)
def find_object(obj, remainder, notfound_handlers, request):
'''
'Walks' the url path in search of an action for which a controller is
implemented and returns that controller object along with what's left
of the remainder.
'''
prev_obj = None
while True:
if obj is None:
raise PecanNotFound
if iscontroller(obj):
if getattr(obj, 'custom_route', None) is None:
return obj, remainder
_detect_custom_path_segments(obj)
if remainder:
custom_route = __custom_routes__.get((obj.__class__, remainder[0]))
if custom_route:
return getattr(obj, custom_route), remainder[1:]
# are we traversing to another controller
cross_boundary(prev_obj, obj)
try:
next_obj, rest = remainder[0], remainder[1:]
if next_obj == '':
index = getattr(obj, 'index', None)
if iscontroller(index):
return index, rest
except IndexError:
# the URL has hit an index method without a trailing slash
index = getattr(obj, 'index', None)
if iscontroller(index):
raise NonCanonicalPath(index, [])
default = getattr(obj, '_default', None)
if iscontroller(default):
notfound_handlers.append(('_default', default, remainder))
lookup = getattr(obj, '_lookup', None)
if iscontroller(lookup):
notfound_handlers.append(('_lookup', lookup, remainder))
route = getattr(obj, '_route', None)
if iscontroller(route):
if len(getargspec(route).args) == 2:
warnings.warn(
(
"The function signature for %s.%s._route is changing "
"in the next version of pecan.\nPlease update to: "
"`def _route(self, args, request)`." % (
obj.__class__.__module__,
obj.__class__.__name__
)
),
DeprecationWarning
)
next_obj, next_remainder = route(remainder)
else:
next_obj, next_remainder = route(remainder, request)
cross_boundary(route, next_obj)
return next_obj, next_remainder
if not remainder:
raise PecanNotFound
prev_remainder = remainder
prev_obj = obj
remainder = rest
try:
obj = getattr(obj, next_obj, None)
except UnicodeEncodeError:
obj = None
# Last-ditch effort: if there's not a matching subcontroller, no
# `_default`, no `_lookup`, and no `_route`, look to see if there's
# an `index` that has a generic method defined for the current request
# method.
if not obj and not notfound_handlers and hasattr(prev_obj, 'index'):
if request.method in _cfg(prev_obj.index).get('generic_handlers',
{}):
return prev_obj.index, prev_remainder
def _detect_custom_path_segments(obj):
# Detect custom controller routes (on the initial traversal)
if obj.__class__.__module__ == '__builtin__':
return
attrs = set(dir(obj))
if obj.__class__ not in __observed_controllers__:
for key, val in getmembers(obj):
if iscontroller(val) and isinstance(
getattr(val, 'custom_route', None),
six.string_types
):
route = val.custom_route
# Detect class attribute name conflicts
for conflict in attrs.intersection(set((route,))):
raise RuntimeError(
(
"%(module)s.%(class)s.%(function)s has "
"a custom path segment, \"%(route)s\", "
"but %(module)s.%(class)s already has an "
"existing attribute named \"%(route)s\"." % {
'module': obj.__class__.__module__,
'class': obj.__class__.__name__,
'function': val.__name__,
'route': conflict
}
),
)
existing = __custom_routes__.get(
(obj.__class__, route)
)
if existing:
# Detect custom path conflicts between functions
raise RuntimeError(
(
"%(module)s.%(class)s.%(function)s and "
"%(module)s.%(class)s.%(other)s have a "
"conflicting custom path segment, "
"\"%(route)s\"." % {
'module': obj.__class__.__module__,
'class': obj.__class__.__name__,
'function': val.__name__,
'other': existing,
'route': route
}
),
)
__custom_routes__[
(obj.__class__, route)
] = key
__observed_controllers__.add(obj.__class__)
|