Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/routes/mapper.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author | shellac |
---|---|
date | Sat, 02 May 2020 07:14:21 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:26e78fe6e8c4 |
---|---|
1 """Mapper and Sub-Mapper""" | |
2 import re | |
3 import threading | |
4 | |
5 from repoze.lru import LRUCache | |
6 import six | |
7 | |
8 from routes import request_config | |
9 from routes.util import ( | |
10 controller_scan, | |
11 RoutesException, | |
12 as_unicode | |
13 ) | |
14 from routes.route import Route | |
15 | |
16 | |
17 COLLECTION_ACTIONS = ['index', 'create', 'new'] | |
18 MEMBER_ACTIONS = ['show', 'update', 'delete', 'edit'] | |
19 | |
20 | |
21 def strip_slashes(name): | |
22 """Remove slashes from the beginning and end of a part/URL.""" | |
23 if name.startswith('/'): | |
24 name = name[1:] | |
25 if name.endswith('/'): | |
26 name = name[:-1] | |
27 return name | |
28 | |
29 | |
30 class SubMapperParent(object): | |
31 """Base class for Mapper and SubMapper, both of which may be the parent | |
32 of SubMapper objects | |
33 """ | |
34 | |
35 def submapper(self, **kargs): | |
36 """Create a partial version of the Mapper with the designated | |
37 options set | |
38 | |
39 This results in a :class:`routes.mapper.SubMapper` object. | |
40 | |
41 If keyword arguments provided to this method also exist in the | |
42 keyword arguments provided to the submapper, their values will | |
43 be merged with the saved options going first. | |
44 | |
45 In addition to :class:`routes.route.Route` arguments, submapper | |
46 can also take a ``path_prefix`` argument which will be | |
47 prepended to the path of all routes that are connected. | |
48 | |
49 Example:: | |
50 | |
51 >>> map = Mapper(controller_scan=None) | |
52 >>> map.connect('home', '/', controller='home', action='splash') | |
53 >>> map.matchlist[0].name == 'home' | |
54 True | |
55 >>> m = map.submapper(controller='home') | |
56 >>> m.connect('index', '/index', action='index') | |
57 >>> map.matchlist[1].name == 'index' | |
58 True | |
59 >>> map.matchlist[1].defaults['controller'] == 'home' | |
60 True | |
61 | |
62 Optional ``collection_name`` and ``resource_name`` arguments are | |
63 used in the generation of route names by the ``action`` and | |
64 ``link`` methods. These in turn are used by the ``index``, | |
65 ``new``, ``create``, ``show``, ``edit``, ``update`` and | |
66 ``delete`` methods which may be invoked indirectly by listing | |
67 them in the ``actions`` argument. If the ``formatted`` argument | |
68 is set to ``True`` (the default), generated paths are given the | |
69 suffix '{.format}' which matches or generates an optional format | |
70 extension. | |
71 | |
72 Example:: | |
73 | |
74 >>> from routes.util import url_for | |
75 >>> map = Mapper(controller_scan=None) | |
76 >>> m = map.submapper(path_prefix='/entries', collection_name='entries', resource_name='entry', actions=['index', 'new']) | |
77 >>> url_for('entries') == '/entries' | |
78 True | |
79 >>> url_for('new_entry', format='xml') == '/entries/new.xml' | |
80 True | |
81 | |
82 """ | |
83 return SubMapper(self, **kargs) | |
84 | |
85 def collection(self, collection_name, resource_name, path_prefix=None, | |
86 member_prefix='/{id}', controller=None, | |
87 collection_actions=COLLECTION_ACTIONS, | |
88 member_actions=MEMBER_ACTIONS, member_options=None, | |
89 **kwargs): | |
90 """Create a submapper that represents a collection. | |
91 | |
92 This results in a :class:`routes.mapper.SubMapper` object, with a | |
93 ``member`` property of the same type that represents the collection's | |
94 member resources. | |
95 | |
96 Its interface is the same as the ``submapper`` together with | |
97 ``member_prefix``, ``member_actions`` and ``member_options`` | |
98 which are passed to the ``member`` submapper as ``path_prefix``, | |
99 ``actions`` and keyword arguments respectively. | |
100 | |
101 Example:: | |
102 | |
103 >>> from routes.util import url_for | |
104 >>> map = Mapper(controller_scan=None) | |
105 >>> c = map.collection('entries', 'entry') | |
106 >>> c.member.link('ping', method='POST') | |
107 >>> url_for('entries') == '/entries' | |
108 True | |
109 >>> url_for('edit_entry', id=1) == '/entries/1/edit' | |
110 True | |
111 >>> url_for('ping_entry', id=1) == '/entries/1/ping' | |
112 True | |
113 | |
114 """ | |
115 if controller is None: | |
116 controller = resource_name or collection_name | |
117 | |
118 if path_prefix is None: | |
119 if collection_name is None: | |
120 path_prefix_str = '' | |
121 else: | |
122 path_prefix_str = '/{collection_name}' | |
123 else: | |
124 if collection_name is None: | |
125 path_prefix_str = "{pre}" | |
126 else: | |
127 path_prefix_str = "{pre}/{collection_name}" | |
128 | |
129 # generate what will be the path prefix for the collection | |
130 path_prefix = path_prefix_str.format(pre=path_prefix, | |
131 collection_name=collection_name) | |
132 | |
133 collection = SubMapper(self, collection_name=collection_name, | |
134 resource_name=resource_name, | |
135 path_prefix=path_prefix, controller=controller, | |
136 actions=collection_actions, **kwargs) | |
137 | |
138 collection.member = SubMapper(collection, path_prefix=member_prefix, | |
139 actions=member_actions, | |
140 **(member_options or {})) | |
141 | |
142 return collection | |
143 | |
144 | |
145 class SubMapper(SubMapperParent): | |
146 """Partial mapper for use with_options""" | |
147 def __init__(self, obj, resource_name=None, collection_name=None, | |
148 actions=None, formatted=None, **kwargs): | |
149 self.kwargs = kwargs | |
150 self.obj = obj | |
151 self.collection_name = collection_name | |
152 self.member = None | |
153 self.resource_name = resource_name \ | |
154 or getattr(obj, 'resource_name', None) \ | |
155 or kwargs.get('controller', None) \ | |
156 or getattr(obj, 'controller', None) | |
157 if formatted is not None: | |
158 self.formatted = formatted | |
159 else: | |
160 self.formatted = getattr(obj, 'formatted', None) | |
161 if self.formatted is None: | |
162 self.formatted = True | |
163 self.add_actions(actions or [], **kwargs) | |
164 | |
165 def connect(self, routename, path=None, **kwargs): | |
166 newkargs = {} | |
167 _routename = routename | |
168 _path = path | |
169 for key, value in six.iteritems(self.kwargs): | |
170 if key == 'path_prefix': | |
171 if path is not None: | |
172 # if there's a name_prefix, add it to the route name | |
173 # and if there's a path_prefix | |
174 _path = ''.join((self.kwargs[key], path)) | |
175 else: | |
176 _path = ''.join((self.kwargs[key], routename)) | |
177 elif key == 'name_prefix': | |
178 if path is not None: | |
179 # if there's a name_prefix, add it to the route name | |
180 # and if there's a path_prefix | |
181 _routename = ''.join((self.kwargs[key], routename)) | |
182 else: | |
183 _routename = None | |
184 elif key in kwargs: | |
185 if isinstance(value, dict): | |
186 newkargs[key] = dict(value, **kwargs[key]) # merge dicts | |
187 else: | |
188 # Originally used this form: | |
189 # newkargs[key] = value + kwargs[key] | |
190 # New version avoids the inheritance concatenation issue | |
191 # with submappers. Only prefixes concatenate, everything | |
192 # else overrides in submappers. | |
193 newkargs[key] = kwargs[key] | |
194 else: | |
195 newkargs[key] = self.kwargs[key] | |
196 for key in kwargs: | |
197 if key not in self.kwargs: | |
198 newkargs[key] = kwargs[key] | |
199 | |
200 newargs = (_routename, _path) | |
201 return self.obj.connect(*newargs, **newkargs) | |
202 | |
203 def link(self, rel=None, name=None, action=None, method='GET', | |
204 formatted=None, **kwargs): | |
205 """Generates a named route for a subresource. | |
206 | |
207 Example:: | |
208 | |
209 >>> from routes.util import url_for | |
210 >>> map = Mapper(controller_scan=None) | |
211 >>> c = map.collection('entries', 'entry') | |
212 >>> c.link('recent', name='recent_entries') | |
213 >>> c.member.link('ping', method='POST', formatted=True) | |
214 >>> url_for('entries') == '/entries' | |
215 True | |
216 >>> url_for('recent_entries') == '/entries/recent' | |
217 True | |
218 >>> url_for('ping_entry', id=1) == '/entries/1/ping' | |
219 True | |
220 >>> url_for('ping_entry', id=1, format='xml') == '/entries/1/ping.xml' | |
221 True | |
222 | |
223 """ | |
224 if formatted or (formatted is None and self.formatted): | |
225 suffix = '{.format}' | |
226 else: | |
227 suffix = '' | |
228 | |
229 return self.connect(name or (rel + '_' + self.resource_name), | |
230 '/' + (rel or name) + suffix, | |
231 action=action or rel or name, | |
232 **_kwargs_with_conditions(kwargs, method)) | |
233 | |
234 def new(self, **kwargs): | |
235 """Generates the "new" link for a collection submapper.""" | |
236 return self.link(rel='new', **kwargs) | |
237 | |
238 def edit(self, **kwargs): | |
239 """Generates the "edit" link for a collection member submapper.""" | |
240 return self.link(rel='edit', **kwargs) | |
241 | |
242 def action(self, name=None, action=None, method='GET', formatted=None, | |
243 **kwargs): | |
244 """Generates a named route at the base path of a submapper. | |
245 | |
246 Example:: | |
247 | |
248 >>> from routes import url_for | |
249 >>> map = Mapper(controller_scan=None) | |
250 >>> c = map.submapper(path_prefix='/entries', controller='entry') | |
251 >>> c.action(action='index', name='entries', formatted=True) | |
252 >>> c.action(action='create', method='POST') | |
253 >>> url_for(controller='entry', action='index', method='GET') == '/entries' | |
254 True | |
255 >>> url_for(controller='entry', action='index', method='GET', format='xml') == '/entries.xml' | |
256 True | |
257 >>> url_for(controller='entry', action='create', method='POST') == '/entries' | |
258 True | |
259 | |
260 """ | |
261 if formatted or (formatted is None and self.formatted): | |
262 suffix = '{.format}' | |
263 else: | |
264 suffix = '' | |
265 return self.connect(name or (action + '_' + self.resource_name), | |
266 suffix, | |
267 action=action or name, | |
268 **_kwargs_with_conditions(kwargs, method)) | |
269 | |
270 def index(self, name=None, **kwargs): | |
271 """Generates the "index" action for a collection submapper.""" | |
272 return self.action(name=name or self.collection_name, | |
273 action='index', method='GET', **kwargs) | |
274 | |
275 def show(self, name=None, **kwargs): | |
276 """Generates the "show" action for a collection member submapper.""" | |
277 return self.action(name=name or self.resource_name, | |
278 action='show', method='GET', **kwargs) | |
279 | |
280 def create(self, **kwargs): | |
281 """Generates the "create" action for a collection submapper.""" | |
282 return self.action(action='create', method='POST', **kwargs) | |
283 | |
284 def update(self, **kwargs): | |
285 """Generates the "update" action for a collection member submapper.""" | |
286 return self.action(action='update', method='PUT', **kwargs) | |
287 | |
288 def delete(self, **kwargs): | |
289 """Generates the "delete" action for a collection member submapper.""" | |
290 return self.action(action='delete', method='DELETE', **kwargs) | |
291 | |
292 def add_actions(self, actions, **kwargs): | |
293 [getattr(self, action)(**kwargs) for action in actions] | |
294 | |
295 # Provided for those who prefer using the 'with' syntax in Python 2.5+ | |
296 def __enter__(self): | |
297 return self | |
298 | |
299 def __exit__(self, type, value, tb): | |
300 pass | |
301 | |
302 | |
303 # Create kwargs with a 'conditions' member generated for the given method | |
304 def _kwargs_with_conditions(kwargs, method): | |
305 if method and 'conditions' not in kwargs: | |
306 newkwargs = kwargs.copy() | |
307 newkwargs['conditions'] = {'method': method} | |
308 return newkwargs | |
309 else: | |
310 return kwargs | |
311 | |
312 | |
313 class Mapper(SubMapperParent): | |
314 """Mapper handles URL generation and URL recognition in a web | |
315 application. | |
316 | |
317 Mapper is built handling dictionary's. It is assumed that the web | |
318 application will handle the dictionary returned by URL recognition | |
319 to dispatch appropriately. | |
320 | |
321 URL generation is done by passing keyword parameters into the | |
322 generate function, a URL is then returned. | |
323 | |
324 """ | |
325 def __init__(self, controller_scan=controller_scan, directory=None, | |
326 always_scan=False, register=True, explicit=True): | |
327 """Create a new Mapper instance | |
328 | |
329 All keyword arguments are optional. | |
330 | |
331 ``controller_scan`` | |
332 Function reference that will be used to return a list of | |
333 valid controllers used during URL matching. If | |
334 ``directory`` keyword arg is present, it will be passed | |
335 into the function during its call. This option defaults to | |
336 a function that will scan a directory for controllers. | |
337 | |
338 Alternatively, a list of controllers or None can be passed | |
339 in which are assumed to be the definitive list of | |
340 controller names valid when matching 'controller'. | |
341 | |
342 ``directory`` | |
343 Passed into controller_scan for the directory to scan. It | |
344 should be an absolute path if using the default | |
345 ``controller_scan`` function. | |
346 | |
347 ``always_scan`` | |
348 Whether or not the ``controller_scan`` function should be | |
349 run during every URL match. This is typically a good idea | |
350 during development so the server won't need to be restarted | |
351 anytime a controller is added. | |
352 | |
353 ``register`` | |
354 Boolean used to determine if the Mapper should use | |
355 ``request_config`` to register itself as the mapper. Since | |
356 it's done on a thread-local basis, this is typically best | |
357 used during testing though it won't hurt in other cases. | |
358 | |
359 ``explicit`` | |
360 Boolean used to determine if routes should be connected | |
361 with implicit defaults of:: | |
362 | |
363 {'controller':'content','action':'index','id':None} | |
364 | |
365 When set to True, these defaults will not be added to route | |
366 connections and ``url_for`` will not use Route memory. | |
367 | |
368 Additional attributes that may be set after mapper | |
369 initialization (ie, map.ATTRIBUTE = 'something'): | |
370 | |
371 ``encoding`` | |
372 Used to indicate alternative encoding/decoding systems to | |
373 use with both incoming URL's, and during Route generation | |
374 when passed a Unicode string. Defaults to 'utf-8'. | |
375 | |
376 ``decode_errors`` | |
377 How to handle errors in the encoding, generally ignoring | |
378 any chars that don't convert should be sufficient. Defaults | |
379 to 'ignore'. | |
380 | |
381 ``minimization`` | |
382 Boolean used to indicate whether or not Routes should | |
383 minimize URL's and the generated URL's, or require every | |
384 part where it appears in the path. Defaults to True. | |
385 | |
386 ``hardcode_names`` | |
387 Whether or not Named Routes result in the default options | |
388 for the route being used *or* if they actually force url | |
389 generation to use the route. Defaults to False. | |
390 | |
391 """ | |
392 self.matchlist = [] | |
393 self.maxkeys = {} | |
394 self.minkeys = {} | |
395 self.urlcache = LRUCache(1600) | |
396 self._created_regs = False | |
397 self._created_gens = False | |
398 self._master_regexp = None | |
399 self.prefix = None | |
400 self.req_data = threading.local() | |
401 self.directory = directory | |
402 self.always_scan = always_scan | |
403 self.controller_scan = controller_scan | |
404 self._regprefix = None | |
405 self._routenames = {} | |
406 self.debug = False | |
407 self.append_slash = False | |
408 self.sub_domains = False | |
409 self.sub_domains_ignore = [] | |
410 self.domain_match = '[^\.\/]+?\.[^\.\/]+' | |
411 self.explicit = explicit | |
412 self.encoding = 'utf-8' | |
413 self.decode_errors = 'ignore' | |
414 self.hardcode_names = True | |
415 self.minimization = False | |
416 self.create_regs_lock = threading.Lock() | |
417 if register: | |
418 config = request_config() | |
419 config.mapper = self | |
420 | |
421 def __str__(self): | |
422 """Generates a tabular string representation.""" | |
423 def format_methods(r): | |
424 if r.conditions: | |
425 method = r.conditions.get('method', '') | |
426 return type(method) is str and method or ', '.join(method) | |
427 else: | |
428 return '' | |
429 | |
430 table = [('Route name', 'Methods', 'Path', 'Controller', 'action')] + \ | |
431 [(r.name or '', format_methods(r), r.routepath or '', | |
432 r.defaults.get('controller', ''), r.defaults.get('action', '')) | |
433 for r in self.matchlist] | |
434 | |
435 widths = [max(len(row[col]) for row in table) | |
436 for col in range(len(table[0]))] | |
437 | |
438 return '\n'.join( | |
439 ' '.join(row[col].ljust(widths[col]) | |
440 for col in range(len(widths))) | |
441 for row in table) | |
442 | |
443 def _envget(self): | |
444 try: | |
445 return self.req_data.environ | |
446 except AttributeError: | |
447 return None | |
448 | |
449 def _envset(self, env): | |
450 self.req_data.environ = env | |
451 | |
452 def _envdel(self): | |
453 del self.req_data.environ | |
454 environ = property(_envget, _envset, _envdel) | |
455 | |
456 def extend(self, routes, path_prefix=''): | |
457 """Extends the mapper routes with a list of Route objects | |
458 | |
459 If a path_prefix is provided, all the routes will have their | |
460 path prepended with the path_prefix. | |
461 | |
462 Example:: | |
463 | |
464 >>> map = Mapper(controller_scan=None) | |
465 >>> map.connect('home', '/', controller='home', action='splash') | |
466 >>> map.matchlist[0].name == 'home' | |
467 True | |
468 >>> routes = [Route('index', '/index.htm', controller='home', | |
469 ... action='index')] | |
470 >>> map.extend(routes) | |
471 >>> len(map.matchlist) == 2 | |
472 True | |
473 >>> map.extend(routes, path_prefix='/subapp') | |
474 >>> len(map.matchlist) == 3 | |
475 True | |
476 >>> map.matchlist[2].routepath == '/subapp/index.htm' | |
477 True | |
478 | |
479 .. note:: | |
480 | |
481 This function does not merely extend the mapper with the | |
482 given list of routes, it actually creates new routes with | |
483 identical calling arguments. | |
484 | |
485 """ | |
486 for route in routes: | |
487 if path_prefix and route.minimization: | |
488 routepath = '/'.join([path_prefix, route.routepath]) | |
489 elif path_prefix: | |
490 routepath = path_prefix + route.routepath | |
491 else: | |
492 routepath = route.routepath | |
493 self.connect(route.name, routepath, **route._kargs) | |
494 | |
495 def make_route(self, *args, **kargs): | |
496 """Make a new Route object | |
497 | |
498 A subclass can override this method to use a custom Route class. | |
499 """ | |
500 return Route(*args, **kargs) | |
501 | |
502 def connect(self, *args, **kargs): | |
503 """Create and connect a new Route to the Mapper. | |
504 | |
505 Usage: | |
506 | |
507 .. code-block:: python | |
508 | |
509 m = Mapper() | |
510 m.connect(':controller/:action/:id') | |
511 m.connect('date/:year/:month/:day', controller="blog", | |
512 action="view") | |
513 m.connect('archives/:page', controller="blog", action="by_page", | |
514 requirements = { 'page':'\d{1,2}' }) | |
515 m.connect('category_list', 'archives/category/:section', | |
516 controller='blog', action='category', | |
517 section='home', type='list') | |
518 m.connect('home', '', controller='blog', action='view', | |
519 section='home') | |
520 | |
521 """ | |
522 routename = None | |
523 if len(args) > 1: | |
524 routename = args[0] | |
525 else: | |
526 args = (None,) + args | |
527 if '_explicit' not in kargs: | |
528 kargs['_explicit'] = self.explicit | |
529 if '_minimize' not in kargs: | |
530 kargs['_minimize'] = self.minimization | |
531 route = self.make_route(*args, **kargs) | |
532 | |
533 # Apply encoding and errors if its not the defaults and the route | |
534 # didn't have one passed in. | |
535 if (self.encoding != 'utf-8' or self.decode_errors != 'ignore') and \ | |
536 '_encoding' not in kargs: | |
537 route.encoding = self.encoding | |
538 route.decode_errors = self.decode_errors | |
539 | |
540 if not route.static: | |
541 self.matchlist.append(route) | |
542 | |
543 if routename: | |
544 self._routenames[routename] = route | |
545 route.name = routename | |
546 if route.static: | |
547 return | |
548 exists = False | |
549 for key in self.maxkeys: | |
550 if key == route.maxkeys: | |
551 self.maxkeys[key].append(route) | |
552 exists = True | |
553 break | |
554 if not exists: | |
555 self.maxkeys[route.maxkeys] = [route] | |
556 self._created_gens = False | |
557 | |
558 def _create_gens(self): | |
559 """Create the generation hashes for route lookups""" | |
560 # Use keys temporailly to assemble the list to avoid excessive | |
561 # list iteration testing with "in" | |
562 controllerlist = {} | |
563 actionlist = {} | |
564 | |
565 # Assemble all the hardcoded/defaulted actions/controllers used | |
566 for route in self.matchlist: | |
567 if route.static: | |
568 continue | |
569 if 'controller' in route.defaults: | |
570 controllerlist[route.defaults['controller']] = True | |
571 if 'action' in route.defaults: | |
572 actionlist[route.defaults['action']] = True | |
573 | |
574 # Setup the lists of all controllers/actions we'll add each route | |
575 # to. We include the '*' in the case that a generate contains a | |
576 # controller/action that has no hardcodes | |
577 controllerlist = list(controllerlist.keys()) + ['*'] | |
578 actionlist = list(actionlist.keys()) + ['*'] | |
579 | |
580 # Go through our list again, assemble the controllers/actions we'll | |
581 # add each route to. If its hardcoded, we only add it to that dict key. | |
582 # Otherwise we add it to every hardcode since it can be changed. | |
583 gendict = {} # Our generated two-deep hash | |
584 for route in self.matchlist: | |
585 if route.static: | |
586 continue | |
587 clist = controllerlist | |
588 alist = actionlist | |
589 if 'controller' in route.hardcoded: | |
590 clist = [route.defaults['controller']] | |
591 if 'action' in route.hardcoded: | |
592 alist = [six.text_type(route.defaults['action'])] | |
593 for controller in clist: | |
594 for action in alist: | |
595 actiondict = gendict.setdefault(controller, {}) | |
596 actiondict.setdefault(action, ([], {}))[0].append(route) | |
597 self._gendict = gendict | |
598 self._created_gens = True | |
599 | |
600 def create_regs(self, *args, **kwargs): | |
601 """Atomically creates regular expressions for all connected | |
602 routes | |
603 """ | |
604 self.create_regs_lock.acquire() | |
605 try: | |
606 self._create_regs(*args, **kwargs) | |
607 finally: | |
608 self.create_regs_lock.release() | |
609 | |
610 def _create_regs(self, clist=None): | |
611 """Creates regular expressions for all connected routes""" | |
612 if clist is None: | |
613 if self.directory: | |
614 clist = self.controller_scan(self.directory) | |
615 elif callable(self.controller_scan): | |
616 clist = self.controller_scan() | |
617 elif not self.controller_scan: | |
618 clist = [] | |
619 else: | |
620 clist = self.controller_scan | |
621 | |
622 for key, val in six.iteritems(self.maxkeys): | |
623 for route in val: | |
624 route.makeregexp(clist) | |
625 | |
626 regexps = [] | |
627 routematches = [] | |
628 for route in self.matchlist: | |
629 if not route.static: | |
630 routematches.append(route) | |
631 regexps.append(route.makeregexp(clist, include_names=False)) | |
632 self._routematches = routematches | |
633 | |
634 # Create our regexp to strip the prefix | |
635 if self.prefix: | |
636 self._regprefix = re.compile(self.prefix + '(.*)') | |
637 | |
638 # Save the master regexp | |
639 regexp = '|'.join(['(?:%s)' % x for x in regexps]) | |
640 self._master_reg = regexp | |
641 try: | |
642 self._master_regexp = re.compile(regexp) | |
643 except OverflowError: | |
644 self._master_regexp = None | |
645 self._created_regs = True | |
646 | |
647 def _match(self, url, environ): | |
648 """Internal Route matcher | |
649 | |
650 Matches a URL against a route, and returns a tuple of the match | |
651 dict and the route object if a match is successfull, otherwise | |
652 it returns empty. | |
653 | |
654 For internal use only. | |
655 | |
656 """ | |
657 if not self._created_regs and self.controller_scan: | |
658 self.create_regs() | |
659 elif not self._created_regs: | |
660 raise RoutesException("You must generate the regular expressions" | |
661 " before matching.") | |
662 | |
663 if self.always_scan: | |
664 self.create_regs() | |
665 | |
666 matchlog = [] | |
667 if self.prefix: | |
668 if re.match(self._regprefix, url): | |
669 url = re.sub(self._regprefix, r'\1', url) | |
670 if not url: | |
671 url = '/' | |
672 else: | |
673 return (None, None, matchlog) | |
674 | |
675 environ = environ or self.environ | |
676 sub_domains = self.sub_domains | |
677 sub_domains_ignore = self.sub_domains_ignore | |
678 domain_match = self.domain_match | |
679 debug = self.debug | |
680 | |
681 if self._master_regexp is not None: | |
682 # Check to see if its a valid url against the main regexp | |
683 # Done for faster invalid URL elimination | |
684 valid_url = re.match(self._master_regexp, url) | |
685 else: | |
686 # Regex is None due to OverflowError caused by too many routes. | |
687 # This will allow larger projects to work but might increase time | |
688 # spent invalidating URLs in the loop below. | |
689 valid_url = True | |
690 if not valid_url: | |
691 return (None, None, matchlog) | |
692 | |
693 for route in self.matchlist: | |
694 if route.static: | |
695 if debug: | |
696 matchlog.append(dict(route=route, static=True)) | |
697 continue | |
698 match = route.match(url, environ, sub_domains, sub_domains_ignore, | |
699 domain_match) | |
700 if debug: | |
701 matchlog.append(dict(route=route, regexp=bool(match))) | |
702 if isinstance(match, dict) or match: | |
703 return (match, route, matchlog) | |
704 return (None, None, matchlog) | |
705 | |
706 def match(self, url=None, environ=None): | |
707 """Match a URL against against one of the routes contained. | |
708 | |
709 Will return None if no valid match is found. | |
710 | |
711 .. code-block:: python | |
712 | |
713 resultdict = m.match('/joe/sixpack') | |
714 | |
715 """ | |
716 if url is None and not environ: | |
717 raise RoutesException('URL or environ must be provided') | |
718 | |
719 if url is None: | |
720 url = environ['PATH_INFO'] | |
721 | |
722 result = self._match(url, environ) | |
723 if self.debug: | |
724 return result[0], result[1], result[2] | |
725 if isinstance(result[0], dict) or result[0]: | |
726 return result[0] | |
727 return None | |
728 | |
729 def routematch(self, url=None, environ=None): | |
730 """Match a URL against against one of the routes contained. | |
731 | |
732 Will return None if no valid match is found, otherwise a | |
733 result dict and a route object is returned. | |
734 | |
735 .. code-block:: python | |
736 | |
737 resultdict, route_obj = m.match('/joe/sixpack') | |
738 | |
739 """ | |
740 if url is None and not environ: | |
741 raise RoutesException('URL or environ must be provided') | |
742 | |
743 if url is None: | |
744 url = environ['PATH_INFO'] | |
745 result = self._match(url, environ) | |
746 if self.debug: | |
747 return result[0], result[1], result[2] | |
748 if isinstance(result[0], dict) or result[0]: | |
749 return result[0], result[1] | |
750 return None | |
751 | |
752 def generate(self, *args, **kargs): | |
753 """Generate a route from a set of keywords | |
754 | |
755 Returns the url text, or None if no URL could be generated. | |
756 | |
757 .. code-block:: python | |
758 | |
759 m.generate(controller='content',action='view',id=10) | |
760 | |
761 """ | |
762 # Generate ourself if we haven't already | |
763 if not self._created_gens: | |
764 self._create_gens() | |
765 | |
766 if self.append_slash: | |
767 kargs['_append_slash'] = True | |
768 | |
769 if not self.explicit: | |
770 if 'controller' not in kargs: | |
771 kargs['controller'] = 'content' | |
772 if 'action' not in kargs: | |
773 kargs['action'] = 'index' | |
774 | |
775 environ = kargs.pop('_environ', self.environ) or {} | |
776 if 'SCRIPT_NAME' in environ: | |
777 script_name = environ['SCRIPT_NAME'] | |
778 elif self.environ and 'SCRIPT_NAME' in self.environ: | |
779 script_name = self.environ['SCRIPT_NAME'] | |
780 else: | |
781 script_name = "" | |
782 controller = kargs.get('controller', None) | |
783 action = kargs.get('action', None) | |
784 | |
785 # If the URL didn't depend on the SCRIPT_NAME, we'll cache it | |
786 # keyed by just by kargs; otherwise we need to cache it with | |
787 # both SCRIPT_NAME and kargs: | |
788 cache_key = six.text_type(args).encode('utf8') + \ | |
789 six.text_type(kargs).encode('utf8') | |
790 | |
791 if self.urlcache is not None: | |
792 if six.PY3: | |
793 cache_key_script_name = b':'.join((script_name.encode('utf-8'), | |
794 cache_key)) | |
795 else: | |
796 cache_key_script_name = '%s:%s' % (script_name, cache_key) | |
797 | |
798 # Check the url cache to see if it exists, use it if it does | |
799 val = self.urlcache.get(cache_key_script_name, self) | |
800 if val != self: | |
801 return val | |
802 | |
803 controller = as_unicode(controller, self.encoding) | |
804 action = as_unicode(action, self.encoding) | |
805 | |
806 actionlist = self._gendict.get(controller) or self._gendict.get('*', {}) | |
807 if not actionlist and not args: | |
808 return None | |
809 (keylist, sortcache) = actionlist.get(action) or \ | |
810 actionlist.get('*', (None, {})) | |
811 if not keylist and not args: | |
812 return None | |
813 | |
814 keys = frozenset(kargs.keys()) | |
815 cacheset = False | |
816 cachekey = six.text_type(keys) | |
817 cachelist = sortcache.get(cachekey) | |
818 if args: | |
819 keylist = args | |
820 elif cachelist: | |
821 keylist = cachelist | |
822 else: | |
823 cacheset = True | |
824 newlist = [] | |
825 for route in keylist: | |
826 if len(route.minkeys - route.dotkeys - keys) == 0: | |
827 newlist.append(route) | |
828 keylist = newlist | |
829 | |
830 class KeySorter: | |
831 | |
832 def __init__(self, obj, *args): | |
833 self.obj = obj | |
834 | |
835 def __lt__(self, other): | |
836 return self._keysort(self.obj, other.obj) < 0 | |
837 | |
838 def _keysort(self, a, b): | |
839 """Sorts two sets of sets, to order them ideally for | |
840 matching.""" | |
841 a = a.maxkeys | |
842 b = b.maxkeys | |
843 | |
844 lendiffa = len(keys ^ a) | |
845 lendiffb = len(keys ^ b) | |
846 # If they both match, don't switch them | |
847 if lendiffa == 0 and lendiffb == 0: | |
848 return 0 | |
849 | |
850 # First, if a matches exactly, use it | |
851 if lendiffa == 0: | |
852 return -1 | |
853 | |
854 # Or b matches exactly, use it | |
855 if lendiffb == 0: | |
856 return 1 | |
857 | |
858 # Neither matches exactly, return the one with the most in | |
859 # common | |
860 if self._compare(lendiffa, lendiffb) != 0: | |
861 return self._compare(lendiffa, lendiffb) | |
862 | |
863 # Neither matches exactly, but if they both have just as | |
864 # much in common | |
865 if len(keys & b) == len(keys & a): | |
866 # Then we return the shortest of the two | |
867 return self._compare(len(a), len(b)) | |
868 | |
869 # Otherwise, we return the one that has the most in common | |
870 else: | |
871 return self._compare(len(keys & b), len(keys & a)) | |
872 | |
873 def _compare(self, obj1, obj2): | |
874 if obj1 < obj2: | |
875 return -1 | |
876 elif obj1 < obj2: | |
877 return 1 | |
878 else: | |
879 return 0 | |
880 | |
881 keylist.sort(key=KeySorter) | |
882 if cacheset: | |
883 sortcache[cachekey] = keylist | |
884 | |
885 # Iterate through the keylist of sorted routes (or a single route if | |
886 # it was passed in explicitly for hardcoded named routes) | |
887 for route in keylist: | |
888 fail = False | |
889 for key in route.hardcoded: | |
890 kval = kargs.get(key) | |
891 if not kval: | |
892 continue | |
893 kval = as_unicode(kval, self.encoding) | |
894 if kval != route.defaults[key] and \ | |
895 not callable(route.defaults[key]): | |
896 fail = True | |
897 break | |
898 if fail: | |
899 continue | |
900 path = route.generate(**kargs) | |
901 if path: | |
902 if self.prefix: | |
903 path = self.prefix + path | |
904 external_static = route.static and route.external | |
905 if not route.absolute and not external_static: | |
906 path = script_name + path | |
907 key = cache_key_script_name | |
908 else: | |
909 key = cache_key | |
910 if self.urlcache is not None: | |
911 self.urlcache.put(key, str(path)) | |
912 return str(path) | |
913 else: | |
914 continue | |
915 return None | |
916 | |
917 def resource(self, member_name, collection_name, **kwargs): | |
918 """Generate routes for a controller resource | |
919 | |
920 The member_name name should be the appropriate singular version | |
921 of the resource given your locale and used with members of the | |
922 collection. The collection_name name will be used to refer to | |
923 the resource collection methods and should be a plural version | |
924 of the member_name argument. By default, the member_name name | |
925 will also be assumed to map to a controller you create. | |
926 | |
927 The concept of a web resource maps somewhat directly to 'CRUD' | |
928 operations. The overlying things to keep in mind is that | |
929 mapping a resource is about handling creating, viewing, and | |
930 editing that resource. | |
931 | |
932 All keyword arguments are optional. | |
933 | |
934 ``controller`` | |
935 If specified in the keyword args, the controller will be | |
936 the actual controller used, but the rest of the naming | |
937 conventions used for the route names and URL paths are | |
938 unchanged. | |
939 | |
940 ``collection`` | |
941 Additional action mappings used to manipulate/view the | |
942 entire set of resources provided by the controller. | |
943 | |
944 Example:: | |
945 | |
946 map.resource('message', 'messages', collection={'rss':'GET'}) | |
947 # GET /message/rss (maps to the rss action) | |
948 # also adds named route "rss_message" | |
949 | |
950 ``member`` | |
951 Additional action mappings used to access an individual | |
952 'member' of this controllers resources. | |
953 | |
954 Example:: | |
955 | |
956 map.resource('message', 'messages', member={'mark':'POST'}) | |
957 # POST /message/1/mark (maps to the mark action) | |
958 # also adds named route "mark_message" | |
959 | |
960 ``new`` | |
961 Action mappings that involve dealing with a new member in | |
962 the controller resources. | |
963 | |
964 Example:: | |
965 | |
966 map.resource('message', 'messages', new={'preview':'POST'}) | |
967 # POST /message/new/preview (maps to the preview action) | |
968 # also adds a url named "preview_new_message" | |
969 | |
970 ``path_prefix`` | |
971 Prepends the URL path for the Route with the path_prefix | |
972 given. This is most useful for cases where you want to mix | |
973 resources or relations between resources. | |
974 | |
975 ``name_prefix`` | |
976 Perpends the route names that are generated with the | |
977 name_prefix given. Combined with the path_prefix option, | |
978 it's easy to generate route names and paths that represent | |
979 resources that are in relations. | |
980 | |
981 Example:: | |
982 | |
983 map.resource('message', 'messages', controller='categories', | |
984 path_prefix='/category/:category_id', | |
985 name_prefix="category_") | |
986 # GET /category/7/message/1 | |
987 # has named route "category_message" | |
988 | |
989 ``requirements`` | |
990 | |
991 A dictionary that restricts the matching of a | |
992 variable. Can be used when matching variables with path_prefix. | |
993 | |
994 Example:: | |
995 | |
996 map.resource('message', 'messages', | |
997 path_prefix='{project_id}/', | |
998 requirements={"project_id": R"\d+"}) | |
999 # POST /01234/message | |
1000 # success, project_id is set to "01234" | |
1001 # POST /foo/message | |
1002 # 404 not found, won't be matched by this route | |
1003 | |
1004 | |
1005 ``parent_resource`` | |
1006 A ``dict`` containing information about the parent | |
1007 resource, for creating a nested resource. It should contain | |
1008 the ``member_name`` and ``collection_name`` of the parent | |
1009 resource. This ``dict`` will | |
1010 be available via the associated ``Route`` object which can | |
1011 be accessed during a request via | |
1012 ``request.environ['routes.route']`` | |
1013 | |
1014 If ``parent_resource`` is supplied and ``path_prefix`` | |
1015 isn't, ``path_prefix`` will be generated from | |
1016 ``parent_resource`` as | |
1017 "<parent collection name>/:<parent member name>_id". | |
1018 | |
1019 If ``parent_resource`` is supplied and ``name_prefix`` | |
1020 isn't, ``name_prefix`` will be generated from | |
1021 ``parent_resource`` as "<parent member name>_". | |
1022 | |
1023 Example:: | |
1024 | |
1025 >>> from routes.util import url_for | |
1026 >>> m = Mapper() | |
1027 >>> m.resource('location', 'locations', | |
1028 ... parent_resource=dict(member_name='region', | |
1029 ... collection_name='regions')) | |
1030 >>> # path_prefix is "regions/:region_id" | |
1031 >>> # name prefix is "region_" | |
1032 >>> url_for('region_locations', region_id=13) | |
1033 '/regions/13/locations' | |
1034 >>> url_for('region_new_location', region_id=13) | |
1035 '/regions/13/locations/new' | |
1036 >>> url_for('region_location', region_id=13, id=60) | |
1037 '/regions/13/locations/60' | |
1038 >>> url_for('region_edit_location', region_id=13, id=60) | |
1039 '/regions/13/locations/60/edit' | |
1040 | |
1041 Overriding generated ``path_prefix``:: | |
1042 | |
1043 >>> m = Mapper() | |
1044 >>> m.resource('location', 'locations', | |
1045 ... parent_resource=dict(member_name='region', | |
1046 ... collection_name='regions'), | |
1047 ... path_prefix='areas/:area_id') | |
1048 >>> # name prefix is "region_" | |
1049 >>> url_for('region_locations', area_id=51) | |
1050 '/areas/51/locations' | |
1051 | |
1052 Overriding generated ``name_prefix``:: | |
1053 | |
1054 >>> m = Mapper() | |
1055 >>> m.resource('location', 'locations', | |
1056 ... parent_resource=dict(member_name='region', | |
1057 ... collection_name='regions'), | |
1058 ... name_prefix='') | |
1059 >>> # path_prefix is "regions/:region_id" | |
1060 >>> url_for('locations', region_id=51) | |
1061 '/regions/51/locations' | |
1062 | |
1063 """ | |
1064 collection = kwargs.pop('collection', {}) | |
1065 member = kwargs.pop('member', {}) | |
1066 new = kwargs.pop('new', {}) | |
1067 path_prefix = kwargs.pop('path_prefix', None) | |
1068 name_prefix = kwargs.pop('name_prefix', None) | |
1069 parent_resource = kwargs.pop('parent_resource', None) | |
1070 | |
1071 # Generate ``path_prefix`` if ``path_prefix`` wasn't specified and | |
1072 # ``parent_resource`` was. Likewise for ``name_prefix``. Make sure | |
1073 # that ``path_prefix`` and ``name_prefix`` *always* take precedence if | |
1074 # they are specified--in particular, we need to be careful when they | |
1075 # are explicitly set to "". | |
1076 if parent_resource is not None: | |
1077 if path_prefix is None: | |
1078 path_prefix = '%s/:%s_id' % (parent_resource['collection_name'], | |
1079 parent_resource['member_name']) | |
1080 if name_prefix is None: | |
1081 name_prefix = '%s_' % parent_resource['member_name'] | |
1082 else: | |
1083 if path_prefix is None: | |
1084 path_prefix = '' | |
1085 if name_prefix is None: | |
1086 name_prefix = '' | |
1087 | |
1088 # Ensure the edit and new actions are in and GET | |
1089 member['edit'] = 'GET' | |
1090 new.update({'new': 'GET'}) | |
1091 | |
1092 # Make new dict's based off the old, except the old values become keys, | |
1093 # and the old keys become items in a list as the value | |
1094 def swap(dct, newdct): | |
1095 """Swap the keys and values in the dict, and uppercase the values | |
1096 from the dict during the swap.""" | |
1097 for key, val in six.iteritems(dct): | |
1098 newdct.setdefault(val.upper(), []).append(key) | |
1099 return newdct | |
1100 collection_methods = swap(collection, {}) | |
1101 member_methods = swap(member, {}) | |
1102 new_methods = swap(new, {}) | |
1103 | |
1104 # Insert create, update, and destroy methods | |
1105 collection_methods.setdefault('POST', []).insert(0, 'create') | |
1106 member_methods.setdefault('PUT', []).insert(0, 'update') | |
1107 member_methods.setdefault('DELETE', []).insert(0, 'delete') | |
1108 | |
1109 # If there's a path prefix option, use it with the controller | |
1110 controller = strip_slashes(collection_name) | |
1111 path_prefix = strip_slashes(path_prefix) | |
1112 path_prefix = '/' + path_prefix | |
1113 if path_prefix and path_prefix != '/': | |
1114 path = path_prefix + '/' + controller | |
1115 else: | |
1116 path = '/' + controller | |
1117 collection_path = path | |
1118 new_path = path + "/new" | |
1119 member_path = path + "/:(id)" | |
1120 | |
1121 options = { | |
1122 'controller': kwargs.get('controller', controller), | |
1123 '_member_name': member_name, | |
1124 '_collection_name': collection_name, | |
1125 '_parent_resource': parent_resource, | |
1126 '_filter': kwargs.get('_filter') | |
1127 } | |
1128 if 'requirements' in kwargs: | |
1129 options['requirements'] = kwargs['requirements'] | |
1130 | |
1131 def requirements_for(meth): | |
1132 """Returns a new dict to be used for all route creation as the | |
1133 route options""" | |
1134 opts = options.copy() | |
1135 if method != 'any': | |
1136 opts['conditions'] = {'method': [meth.upper()]} | |
1137 return opts | |
1138 | |
1139 # Add the routes for handling collection methods | |
1140 for method, lst in six.iteritems(collection_methods): | |
1141 primary = (method != 'GET' and lst.pop(0)) or None | |
1142 route_options = requirements_for(method) | |
1143 for action in lst: | |
1144 route_options['action'] = action | |
1145 route_name = "%s%s_%s" % (name_prefix, action, collection_name) | |
1146 self.connect("formatted_" + route_name, "%s/%s.:(format)" % | |
1147 (collection_path, action), **route_options) | |
1148 self.connect(route_name, "%s/%s" % (collection_path, action), | |
1149 **route_options) | |
1150 if primary: | |
1151 route_options['action'] = primary | |
1152 self.connect("%s.:(format)" % collection_path, **route_options) | |
1153 self.connect(collection_path, **route_options) | |
1154 | |
1155 # Specifically add in the built-in 'index' collection method and its | |
1156 # formatted version | |
1157 self.connect("formatted_" + name_prefix + collection_name, | |
1158 collection_path + ".:(format)", action='index', | |
1159 conditions={'method': ['GET']}, **options) | |
1160 self.connect(name_prefix + collection_name, collection_path, | |
1161 action='index', conditions={'method': ['GET']}, **options) | |
1162 | |
1163 # Add the routes that deal with new resource methods | |
1164 for method, lst in six.iteritems(new_methods): | |
1165 route_options = requirements_for(method) | |
1166 for action in lst: | |
1167 name = "new_" + member_name | |
1168 route_options['action'] = action | |
1169 if action == 'new': | |
1170 path = new_path | |
1171 formatted_path = new_path + '.:(format)' | |
1172 else: | |
1173 path = "%s/%s" % (new_path, action) | |
1174 name = action + "_" + name | |
1175 formatted_path = "%s/%s.:(format)" % (new_path, action) | |
1176 self.connect("formatted_" + name_prefix + name, formatted_path, | |
1177 **route_options) | |
1178 self.connect(name_prefix + name, path, **route_options) | |
1179 | |
1180 requirements_regexp = '[^\/]+(?<!\\\)' | |
1181 | |
1182 # Add the routes that deal with member methods of a resource | |
1183 for method, lst in six.iteritems(member_methods): | |
1184 route_options = requirements_for(method) | |
1185 route_options['requirements'] = {'id': requirements_regexp} | |
1186 if method not in ['POST', 'GET', 'any']: | |
1187 primary = lst.pop(0) | |
1188 else: | |
1189 primary = None | |
1190 for action in lst: | |
1191 route_options['action'] = action | |
1192 self.connect("formatted_%s%s_%s" % (name_prefix, action, | |
1193 member_name), | |
1194 "%s/%s.:(format)" % (member_path, action), | |
1195 **route_options) | |
1196 self.connect("%s%s_%s" % (name_prefix, action, member_name), | |
1197 "%s/%s" % (member_path, action), **route_options) | |
1198 if primary: | |
1199 route_options['action'] = primary | |
1200 self.connect("%s.:(format)" % member_path, **route_options) | |
1201 self.connect(member_path, **route_options) | |
1202 | |
1203 # Specifically add the member 'show' method | |
1204 route_options = requirements_for('GET') | |
1205 route_options['action'] = 'show' | |
1206 route_options['requirements'] = {'id': requirements_regexp} | |
1207 self.connect("formatted_" + name_prefix + member_name, | |
1208 member_path + ".:(format)", **route_options) | |
1209 self.connect(name_prefix + member_name, member_path, **route_options) | |
1210 | |
1211 def redirect(self, match_path, destination_path, *args, **kwargs): | |
1212 """Add a redirect route to the mapper | |
1213 | |
1214 Redirect routes bypass the wrapped WSGI application and instead | |
1215 result in a redirect being issued by the RoutesMiddleware. As | |
1216 such, this method is only meaningful when using | |
1217 RoutesMiddleware. | |
1218 | |
1219 By default, a 302 Found status code is used, this can be | |
1220 changed by providing a ``_redirect_code`` keyword argument | |
1221 which will then be used instead. Note that the entire status | |
1222 code string needs to be present. | |
1223 | |
1224 When using keyword arguments, all arguments that apply to | |
1225 matching will be used for the match, while generation specific | |
1226 options will be used during generation. Thus all options | |
1227 normally available to connected Routes may be used with | |
1228 redirect routes as well. | |
1229 | |
1230 Example:: | |
1231 | |
1232 map = Mapper() | |
1233 map.redirect('/legacyapp/archives/{url:.*}', '/archives/{url}') | |
1234 map.redirect('/home/index', '/', | |
1235 _redirect_code='301 Moved Permanently') | |
1236 | |
1237 """ | |
1238 both_args = ['_encoding', '_explicit', '_minimize'] | |
1239 gen_args = ['_filter'] | |
1240 | |
1241 status_code = kwargs.pop('_redirect_code', '302 Found') | |
1242 gen_dict, match_dict = {}, {} | |
1243 | |
1244 # Create the dict of args for the generation route | |
1245 for key in both_args + gen_args: | |
1246 if key in kwargs: | |
1247 gen_dict[key] = kwargs[key] | |
1248 gen_dict['_static'] = True | |
1249 | |
1250 # Create the dict of args for the matching route | |
1251 for key in kwargs: | |
1252 if key not in gen_args: | |
1253 match_dict[key] = kwargs[key] | |
1254 | |
1255 self.connect(match_path, **match_dict) | |
1256 match_route = self.matchlist[-1] | |
1257 | |
1258 self.connect('_redirect_%s' % id(match_route), destination_path, | |
1259 **gen_dict) | |
1260 match_route.redirect = True | |
1261 match_route.redirect_status = status_code |