root/galaxy-central/eggs/Routes-1.12.3-py2.6.egg/routes/route.py @ 3

リビジョン 3, 28.7 KB (コミッタ: kohda, 14 年 前)

Install Unix tools  http://hannonlab.cshl.edu/galaxy_unix_tools/galaxy.html

行番号 
1import re
2import sys
3import urllib
4
5if sys.version < '2.4':
6    from sets import ImmutableSet as frozenset
7
8from routes.util import _url_quote as url_quote, _str_encode
9
10
11class Route(object):
12    """The Route object holds a route recognition and generation
13    routine.
14   
15    See Route.__init__ docs for usage.
16   
17    """
18    # reserved keys that don't count
19    reserved_keys = ['requirements']
20   
21    # special chars to indicate a natural split in the URL
22    done_chars = ('/', ',', ';', '.', '#')
23   
24    def __init__(self, name, routepath, **kargs):
25        """Initialize a route, with a given routepath for
26        matching/generation
27       
28        The set of keyword args will be used as defaults.
29       
30        Usage::
31       
32            >>> from routes.base import Route
33            >>> newroute = Route(None, ':controller/:action/:id')
34            >>> sorted(newroute.defaults.items())
35            [('action', 'index'), ('id', None)]
36            >>> newroute = Route(None, 'date/:year/:month/:day', 
37            ...     controller="blog", action="view")
38            >>> newroute = Route(None, 'archives/:page', controller="blog",
39            ...     action="by_page", requirements = { 'page':'\d{1,2}' })
40            >>> newroute.reqs
41            {'page': '\\\d{1,2}'}
42       
43        .. Note::
44            Route is generally not called directly, a Mapper instance
45            connect method should be used to add routes.
46       
47        """
48        self.routepath = routepath
49        self.sub_domains = False
50        self.prior = None
51        self.redirect = False
52        self.name = name
53        self._kargs = kargs
54        self.minimization = kargs.pop('_minimize', False)
55        self.encoding = kargs.pop('_encoding', 'utf-8')
56        self.reqs = kargs.get('requirements', {})
57        self.decode_errors = 'replace'
58       
59        # Don't bother forming stuff we don't need if its a static route
60        self.static = kargs.pop('_static', False)
61        self.filter = kargs.pop('_filter', None)
62        self.absolute = kargs.pop('_absolute', False)
63       
64        # Pull out the member/collection name if present, this applies only to
65        # map.resource
66        self.member_name = kargs.pop('_member_name', None)
67        self.collection_name = kargs.pop('_collection_name', None)
68        self.parent_resource = kargs.pop('_parent_resource', None)
69       
70        # Pull out route conditions
71        self.conditions = kargs.pop('conditions', None)
72       
73        # Determine if explicit behavior should be used
74        self.explicit = kargs.pop('_explicit', False)
75               
76        # Since static need to be generated exactly, treat them as
77        # non-minimized
78        if self.static:
79            self.external = '://' in self.routepath
80            self.minimization = False
81       
82        # Strip preceding '/' if present, and not minimizing
83        if routepath.startswith('/') and self.minimization:
84            self.routepath = routepath[1:]
85        self._setup_route()
86       
87    def _setup_route(self):
88        # Build our routelist, and the keys used in the route
89        self.routelist = routelist = self._pathkeys(self.routepath)
90        routekeys = frozenset([key['name'] for key in routelist
91                               if isinstance(key, dict)])
92        self.dotkeys = frozenset([key['name'] for key in routelist
93                                  if isinstance(key, dict) and
94                                     key['type'] == '.'])
95
96        if not self.minimization:
97            self.make_full_route()
98       
99        # Build a req list with all the regexp requirements for our args
100        self.req_regs = {}
101        for key, val in self.reqs.iteritems():
102            self.req_regs[key] = re.compile('^' + val + '$')
103        # Update our defaults and set new default keys if needed. defaults
104        # needs to be saved
105        (self.defaults, defaultkeys) = self._defaults(routekeys,
106                                                      self.reserved_keys,
107                                                      self._kargs.copy())
108        # Save the maximum keys we could utilize
109        self.maxkeys = defaultkeys | routekeys
110       
111        # Populate our minimum keys, and save a copy of our backward keys for
112        # quicker generation later
113        (self.minkeys, self.routebackwards) = self._minkeys(routelist[:])
114       
115        # Populate our hardcoded keys, these are ones that are set and don't
116        # exist in the route
117        self.hardcoded = frozenset([key for key in self.maxkeys \
118            if key not in routekeys and self.defaults[key] is not None])
119       
120        # Cache our default keys
121        self._default_keys = frozenset(self.defaults.keys())
122   
123    def make_full_route(self):
124        """Make a full routelist string for use with non-minimized
125        generation"""
126        regpath = ''
127        for part in self.routelist:
128            if isinstance(part, dict):
129                regpath += '%(' + part['name'] + ')s'
130            else:
131                regpath += part
132        self.regpath = regpath
133   
134    def make_unicode(self, s):
135        """Transform the given argument into a unicode string."""
136        if isinstance(s, unicode):
137            return s
138        elif isinstance(s, str):
139            return s.decode(self.encoding)
140        elif callable(s):
141            return s
142        else:
143            return unicode(s)
144   
145    def _pathkeys(self, routepath):
146        """Utility function to walk the route, and pull out the valid
147        dynamic/wildcard keys."""
148        collecting = False
149        current = ''
150        done_on = ''
151        var_type = ''
152        just_started = False
153        routelist = []
154        for char in routepath:
155            if char in [':', '*', '{'] and not collecting and not self.static \
156               or char in ['{'] and not collecting:
157                just_started = True
158                collecting = True
159                var_type = char
160                if char == '{':
161                    done_on = '}'
162                    just_started = False
163                if len(current) > 0:
164                    routelist.append(current)
165                    current = ''
166            elif collecting and just_started:
167                just_started = False
168                if char == '(':
169                    done_on = ')'
170                else:
171                    current = char
172                    done_on = self.done_chars + ('-',)
173            elif collecting and char not in done_on:
174                current += char
175            elif collecting:
176                collecting = False
177                if var_type == '{':
178                    if current[0] == '.':
179                        var_type = '.'
180                        current = current[1:]
181                    else:
182                        var_type = ':'
183                    opts = current.split(':')
184                    if len(opts) > 1:
185                        current = opts[0]
186                        self.reqs[current] = opts[1]
187                routelist.append(dict(type=var_type, name=current))
188                if char in self.done_chars:
189                    routelist.append(char)
190                done_on = var_type = current = ''
191            else:
192                current += char
193        if collecting:
194            routelist.append(dict(type=var_type, name=current))
195        elif current:
196            routelist.append(current)
197        return routelist
198
199    def _minkeys(self, routelist):
200        """Utility function to walk the route backwards
201       
202        Will also determine the minimum keys we can handle to generate
203        a working route.
204       
205        routelist is a list of the '/' split route path
206        defaults is a dict of all the defaults provided for the route
207       
208        """
209        minkeys = []
210        backcheck = routelist[:]
211       
212        # If we don't honor minimization, we need all the keys in the
213        # route path
214        if not self.minimization:
215            for part in backcheck:
216                if isinstance(part, dict):
217                    minkeys.append(part['name'])
218            return (frozenset(minkeys), backcheck)
219       
220        gaps = False
221        backcheck.reverse()
222        for part in backcheck:
223            if not isinstance(part, dict) and part not in self.done_chars:
224                gaps = True
225                continue
226            elif not isinstance(part, dict):
227                continue
228            key = part['name']
229            if self.defaults.has_key(key) and not gaps:
230                continue
231            minkeys.append(key)
232            gaps = True
233        return  (frozenset(minkeys), backcheck)
234   
235    def _defaults(self, routekeys, reserved_keys, kargs):
236        """Creates default set with values stringified
237       
238        Put together our list of defaults, stringify non-None values
239        and add in our action/id default if they use it and didn't
240        specify it.
241       
242        defaultkeys is a list of the currently assumed default keys
243        routekeys is a list of the keys found in the route path
244        reserved_keys is a list of keys that are not
245       
246        """
247        defaults = {}
248        # Add in a controller/action default if they don't exist
249        if 'controller' not in routekeys and 'controller' not in kargs \
250           and not self.explicit:
251            kargs['controller'] = 'content'
252        if 'action' not in routekeys and 'action' not in kargs \
253           and not self.explicit:
254            kargs['action'] = 'index'
255        defaultkeys = frozenset([key for key in kargs.keys() \
256                                 if key not in reserved_keys])
257        for key in defaultkeys:
258            if kargs[key] is not None:
259                defaults[key] = self.make_unicode(kargs[key])
260            else:
261                defaults[key] = None
262        if 'action' in routekeys and not defaults.has_key('action') \
263           and not self.explicit:
264            defaults['action'] = 'index'
265        if 'id' in routekeys and not defaults.has_key('id') \
266           and not self.explicit:
267            defaults['id'] = None
268        newdefaultkeys = frozenset([key for key in defaults.keys() \
269                                    if key not in reserved_keys])
270       
271        return (defaults, newdefaultkeys)
272       
273    def makeregexp(self, clist, include_names=True):
274        """Create a regular expression for matching purposes
275       
276        Note: This MUST be called before match can function properly.
277       
278        clist should be a list of valid controller strings that can be
279        matched, for this reason makeregexp should be called by the web
280        framework after it knows all available controllers that can be
281        utilized.
282       
283        include_names indicates whether this should be a match regexp
284        assigned to itself using regexp grouping names, or if names
285        should be excluded for use in a single larger regexp to
286        determine if any routes match
287       
288        """
289        if self.minimization:
290            reg = self.buildnextreg(self.routelist, clist, include_names)[0]
291            if not reg:
292                reg = '/'
293            reg = reg + '/?' + '$'
294       
295            if not reg.startswith('/'):
296                reg = '/' + reg
297        else:
298            reg = self.buildfullreg(clist, include_names)
299       
300        reg = '^' + reg
301       
302        if not include_names:
303            return reg
304       
305        self.regexp = reg
306        self.regmatch = re.compile(reg)
307   
308    def buildfullreg(self, clist, include_names=True):
309        """Build the regexp by iterating through the routelist and
310        replacing dicts with the appropriate regexp match"""
311        regparts = []
312        for part in self.routelist:
313            if isinstance(part, dict):
314                var = part['name']
315                if var == 'controller':
316                    partmatch = '|'.join(map(re.escape, clist))
317                elif part['type'] == ':':
318                    partmatch = self.reqs.get(var) or '[^/]+?'
319                elif part['type'] == '.':
320                    partmatch = self.reqs.get(var) or '[^/.]+?'
321                else:
322                    partmatch = self.reqs.get(var) or '.+?'
323                if include_names:
324                    regpart = '(?P<%s>%s)' % (var, partmatch)
325                else:
326                    regpart = '(?:%s)' % partmatch
327                if part['type'] == '.':
328                    regparts.append('(?:\.%s)??' % regpart)
329                else:
330                    regparts.append(regpart)
331            else:
332                regparts.append(re.escape(part))
333        regexp = ''.join(regparts) + '$'
334        return regexp
335   
336    def buildnextreg(self, path, clist, include_names=True):
337        """Recursively build our regexp given a path, and a controller
338        list.
339       
340        Returns the regular expression string, and two booleans that
341        can be ignored as they're only used internally by buildnextreg.
342       
343        """
344        if path:
345            part = path[0]
346        else:
347            part = ''
348        reg = ''
349       
350        # noreqs will remember whether the remainder has either a string
351        # match, or a non-defaulted regexp match on a key, allblank remembers
352        # if the rest could possible be completely empty
353        (rest, noreqs, allblank) = ('', True, True)
354        if len(path[1:]) > 0:
355            self.prior = part
356            (rest, noreqs, allblank) = self.buildnextreg(path[1:], clist, include_names)
357       
358        if isinstance(part, dict) and part['type'] in (':', '.'):
359            var = part['name']
360            typ = part['type']
361            partreg = ''
362           
363            # First we plug in the proper part matcher
364            if self.reqs.has_key(var):
365                if include_names:
366                    partreg = '(?P<%s>%s)' % (var, self.reqs[var])
367                else:
368                    partreg = '(?:%s)' % self.reqs[var]
369                if typ == '.':
370                    partreg = '(?:\.%s)??' % partreg
371            elif var == 'controller':
372                if include_names:
373                    partreg = '(?P<%s>%s)' % (var, '|'.join(map(re.escape, clist)))
374                else:
375                    partreg = '(?:%s)' % '|'.join(map(re.escape, clist))
376            elif self.prior in ['/', '#']:
377                if include_names:
378                    partreg = '(?P<' + var + '>[^' + self.prior + ']+?)'
379                else:
380                    partreg = '(?:[^' + self.prior + ']+?)'
381            else:
382                if not rest:
383                    if typ == '.':
384                        exclude_chars = '/.'
385                    else:
386                        exclude_chars = '/'
387                    if include_names:
388                        partreg = '(?P<%s>[^%s]+?)' % (var, exclude_chars)
389                    else:
390                        partreg = '(?:[^%s]+?)' % exclude_chars
391                    if typ == '.':
392                        partreg = '(?:\.%s)??' % partreg
393                else:
394                    end = ''.join(self.done_chars)
395                    rem = rest
396                    if rem[0] == '\\' and len(rem) > 1:
397                        rem = rem[1]
398                    elif rem.startswith('(\\') and len(rem) > 2:
399                        rem = rem[2]
400                    else:
401                        rem = end
402                    rem = frozenset(rem) | frozenset(['/'])
403                    if include_names:
404                        partreg = '(?P<%s>[^%s]+?)' % (var, ''.join(rem))
405                    else:
406                        partreg = '(?:[^%s]+?)' % ''.join(rem)
407           
408            if self.reqs.has_key(var):
409                noreqs = False
410            if not self.defaults.has_key(var):
411                allblank = False
412                noreqs = False
413           
414            # Now we determine if its optional, or required. This changes
415            # depending on what is in the rest of the match. If noreqs is
416            # true, then its possible the entire thing is optional as there's
417            # no reqs or string matches.
418            if noreqs:
419                # The rest is optional, but now we have an optional with a
420                # regexp. Wrap to ensure that if we match anything, we match
421                # our regexp first. It's still possible we could be completely
422                # blank as we have a default
423                if self.reqs.has_key(var) and self.defaults.has_key(var):
424                    reg = '(' + partreg + rest + ')?'
425               
426                # Or we have a regexp match with no default, so now being
427                # completely blank form here on out isn't possible
428                elif self.reqs.has_key(var):
429                    allblank = False
430                    reg = partreg + rest
431               
432                # If the character before this is a special char, it has to be
433                # followed by this
434                elif self.defaults.has_key(var) and \
435                     self.prior in (',', ';', '.'):
436                    reg = partreg + rest
437               
438                # Or we have a default with no regexp, don't touch the allblank
439                elif self.defaults.has_key(var):
440                    reg = partreg + '?' + rest
441               
442                # Or we have a key with no default, and no reqs. Not possible
443                # to be all blank from here
444                else:
445                    allblank = False
446                    reg = partreg + rest
447            # In this case, we have something dangling that might need to be
448            # matched
449            else:
450                # If they can all be blank, and we have a default here, we know
451                # its safe to make everything from here optional. Since
452                # something else in the chain does have req's though, we have
453                # to make the partreg here required to continue matching
454                if allblank and self.defaults.has_key(var):
455                    reg = '(' + partreg + rest + ')?'
456                   
457                # Same as before, but they can't all be blank, so we have to
458                # require it all to ensure our matches line up right
459                else:
460                    reg = partreg + rest
461        elif isinstance(part, dict) and part['type'] == '*':
462            var = part['name']
463            if noreqs:
464                if include_names:
465                    reg = '(?P<%s>.*)' % var + rest
466                else:
467                    reg = '(?:.*)' + rest
468                if not self.defaults.has_key(var):
469                    allblank = False
470                    noreqs = False
471            else:
472                if allblank and self.defaults.has_key(var):
473                    if include_names:
474                        reg = '(?P<%s>.*)' % var + rest
475                    else:
476                        reg = '(?:.*)' + rest
477                elif self.defaults.has_key(var):
478                    if include_names:
479                        reg = '(?P<%s>.*)' % var + rest
480                    else:
481                        reg = '(?:.*)' + rest
482                else:
483                    if include_names:
484                        reg = '(?P<%s>.*)' % var + rest
485                    else:
486                        reg = '(?:.*)' + rest
487                    allblank = False
488                    noreqs = False
489        elif part and part[-1] in self.done_chars:
490            if allblank:
491                reg = re.escape(part[:-1]) + '(' + re.escape(part[-1]) + rest
492                reg += ')?'
493            else:
494                allblank = False
495                reg = re.escape(part) + rest
496       
497        # We have a normal string here, this is a req, and it prevents us from
498        # being all blank
499        else:
500            noreqs = False
501            allblank = False
502            reg = re.escape(part) + rest
503       
504        return (reg, noreqs, allblank)
505   
506    def match(self, url, environ=None, sub_domains=False,
507              sub_domains_ignore=None, domain_match=''):
508        """Match a url to our regexp.
509       
510        While the regexp might match, this operation isn't
511        guaranteed as there's other factors that can cause a match to
512        fail even though the regexp succeeds (Default that was relied
513        on wasn't given, requirement regexp doesn't pass, etc.).
514       
515        Therefore the calling function shouldn't assume this will
516        return a valid dict, the other possible return is False if a
517        match doesn't work out.
518       
519        """
520        # Static routes don't match, they generate only
521        if self.static:
522            return False
523       
524        match = self.regmatch.match(url)
525       
526        if not match:
527            return False
528           
529        sub_domain = None
530       
531        if sub_domains and environ and 'HTTP_HOST' in environ:
532            host = environ['HTTP_HOST'].split(':')[0]
533            sub_match = re.compile('^(.+?)\.%s$' % domain_match)
534            subdomain = re.sub(sub_match, r'\1', host)
535            if subdomain not in sub_domains_ignore and host != subdomain:
536                sub_domain = subdomain
537       
538        if self.conditions:
539            if 'method' in self.conditions and environ and \
540                environ['REQUEST_METHOD'] not in self.conditions['method']:
541                return False
542           
543            # Check sub-domains?
544            use_sd = self.conditions.get('sub_domain')
545            if use_sd and not sub_domain:
546                return False
547            elif not use_sd and 'sub_domain' in self.conditions and sub_domain:
548                return False
549            if isinstance(use_sd, list) and sub_domain not in use_sd:
550                return False
551       
552        matchdict = match.groupdict()
553        result = {}
554        extras = self._default_keys - frozenset(matchdict.keys())
555        for key, val in matchdict.iteritems():
556            if key != 'path_info' and self.encoding:
557                # change back into python unicode objects from the URL
558                # representation
559                try:
560                    val = val and val.decode(self.encoding, self.decode_errors)
561                except UnicodeDecodeError:
562                    return False
563           
564            if not val and key in self.defaults and self.defaults[key]:
565                result[key] = self.defaults[key]
566            else:
567                result[key] = val
568        for key in extras:
569            result[key] = self.defaults[key]
570       
571        # Add the sub-domain if there is one
572        if sub_domains:
573            result['sub_domain'] = sub_domain
574       
575        # If there's a function, call it with environ and expire if it
576        # returns False
577        if self.conditions and 'function' in self.conditions and \
578            not self.conditions['function'](environ, result):
579            return False
580       
581        return result
582   
583    def generate_non_minimized(self, kargs):
584        """Generate a non-minimal version of the URL"""
585        # Iterate through the keys that are defaults, and NOT in the route
586        # path. If its not in kargs, or doesn't match, or is None, this
587        # route won't work
588        for k in self.maxkeys - self.minkeys:
589            if k not in kargs:
590                return False
591            elif self.make_unicode(kargs[k]) != \
592                self.make_unicode(self.defaults[k]):
593                return False
594               
595        # Ensure that all the args in the route path are present and not None
596        for arg in self.minkeys:
597            if arg not in kargs or kargs[arg] is None:
598                if arg in self.dotkeys:
599                    kargs[arg] = ''
600                else:
601                    return False
602
603        # Encode all the argument that the regpath can use
604        for k in kargs:
605            if k in self.maxkeys:
606                if k in self.dotkeys:
607                    if kargs[k]:
608                        kargs[k] = url_quote('.' + kargs[k], self.encoding)
609                else:
610                    kargs[k] = url_quote(kargs[k], self.encoding)
611
612        return self.regpath % kargs
613   
614    def generate_minimized(self, kargs):
615        """Generate a minimized version of the URL"""
616        routelist = self.routebackwards
617        urllist = []
618        gaps = False
619        for part in routelist:
620            if isinstance(part, dict) and part['type'] in (':', '.'):
621                arg = part['name']
622               
623                # For efficiency, check these just once
624                has_arg = kargs.has_key(arg)
625                has_default = self.defaults.has_key(arg)
626               
627                # Determine if we can leave this part off
628                # First check if the default exists and wasn't provided in the
629                # call (also no gaps)
630                if has_default and not has_arg and not gaps:
631                    continue
632                   
633                # Now check to see if there's a default and it matches the
634                # incoming call arg
635                if (has_default and has_arg) and self.make_unicode(kargs[arg]) == \
636                    self.make_unicode(self.defaults[arg]) and not gaps:
637                    continue
638               
639                # We need to pull the value to append, if the arg is None and
640                # we have a default, use that
641                if has_arg and kargs[arg] is None and has_default and not gaps:
642                    continue
643               
644                # Otherwise if we do have an arg, use that
645                elif has_arg:
646                    val = kargs[arg]
647               
648                elif has_default and self.defaults[arg] is not None:
649                    val = self.defaults[arg]
650                # Optional format parameter?
651                elif part['type'] == '.':
652                    continue
653                # No arg at all? This won't work
654                else:
655                    return False
656                   
657                urllist.append(url_quote(val, self.encoding))
658                if part['type'] == '.':
659                    urllist.append('.')
660
661                if has_arg:
662                    del kargs[arg]
663                gaps = True
664            elif isinstance(part, dict) and part['type'] == '*':
665                arg = part['name']
666                kar = kargs.get(arg)
667                if kar is not None:
668                    urllist.append(url_quote(kar, self.encoding))
669                    gaps = True
670            elif part and part[-1] in self.done_chars:
671                if not gaps and part in self.done_chars:
672                    continue
673                elif not gaps:
674                    urllist.append(part[:-1])
675                    gaps = True
676                else:
677                    gaps = True
678                    urllist.append(part)
679            else:
680                gaps = True
681                urllist.append(part)
682        urllist.reverse()
683        url = ''.join(urllist)
684        return url
685   
686    def generate(self, _ignore_req_list=False, _append_slash=False, **kargs):
687        """Generate a URL from ourself given a set of keyword arguments
688       
689        Toss an exception if this
690        set of keywords would cause a gap in the url.
691       
692        """
693        # Verify that our args pass any regexp requirements
694        if not _ignore_req_list:
695            for key in self.reqs.keys():
696                val = kargs.get(key)
697                if val and not self.req_regs[key].match(self.make_unicode(val)):
698                    return False
699       
700        # Verify that if we have a method arg, its in the method accept list.
701        # Also, method will be changed to _method for route generation
702        meth = kargs.get('method')
703        if meth:
704            if self.conditions and 'method' in self.conditions \
705                and meth.upper() not in self.conditions['method']:
706                return False
707            kargs.pop('method')
708       
709        if self.minimization:
710            url = self.generate_minimized(kargs)
711        else:
712            url = self.generate_non_minimized(kargs)
713       
714        if url is False:
715            return url
716       
717        if not url.startswith('/') and not self.static:
718            url = '/' + url
719        extras = frozenset(kargs.keys()) - self.maxkeys
720        if extras:
721            if _append_slash and not url.endswith('/'):
722                url += '/'
723            fragments = []
724            # don't assume the 'extras' set preserves order: iterate
725            # through the ordered kargs instead
726            for key in kargs:
727                if key not in extras:
728                    continue
729                if key == 'action' or key == 'controller':
730                    continue
731                val = kargs[key]
732                if isinstance(val, (tuple, list)):
733                    for value in val:
734                        fragments.append((key, _str_encode(value, self.encoding)))
735                else:
736                    fragments.append((key, _str_encode(val, self.encoding)))
737            if fragments:
738                url += '?'
739                url += urllib.urlencode(fragments)
740        elif _append_slash and not url.endswith('/'):
741            url += '/'
742        return url
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。