Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/routes/routes/base.py
0001"""Route and Mapper core classes"""
0002
0003import re
0004import sys
0005import urllib
0006from util import _url_quote as url_quote
0007from util import controller_scan, RouteException
0008from routes import request_config
0009
0010if sys.version < '2.4':
0011    from sets import ImmutableSet as frozenset
0012
0013import threadinglocal
0014
0015def strip_slashes(name):
0016    """Remove slashes from the beginning and end of a part/URL."""
0017    if name.startswith('/'):
0018        name = name[1:]
0019    if name.endswith('/'):
0020        name = name[:-1]
0021    return name
0022
0023class Route(object):
0024    """The Route object holds a route recognition and generation routine.
0025    
0026    See Route.__init__ docs for usage.
0027    """
0028
0029    def __init__(self, routepath, **kargs):
0030        """Initialize a route, with a given routepath for matching/generation
0031        
0032        The set of keyword args will be used as defaults.
0033        
0034        Usage::
0035        
0036            >>> from routes.base import Route
0037            >>> newroute = Route(':controller/:action/:id')
0038            >>> newroute.defaults
0039            {'action': 'index', 'id': None}
0040            >>> newroute = Route('date/:year/:month/:day', controller="blog", 
0041            ...     action="view")
0042            >>> newroute = Route('archives/:page', controller="blog", 
0043            ...     action="by_page", requirements = { 'page':'\d{1,2}' })
0044            >>> newroute.reqs
0045            {'page': '\\\d{1,2}'}
0046        
0047        .. Note:: 
0048            Route is generally not called directly, a Mapper instance connect 
0049            method should be used to add routes.
0050        """
0051
0052        self.routepath = routepath
0053        self.sub_domains = False
0054        self.prior = None
0055        self.encoding = kargs.pop('_encoding', 'utf-8')
0056        self.decode_errors = 'replace'
0057
0058        # Don't bother forming stuff we don't need if its a static route
0059        self.static = kargs.get('_static', False)
0060        self.filter = kargs.pop('_filter', None)
0061        self.absolute = kargs.pop('_absolute', False)
0062
0063        # Pull out the member/collection name if present, this applies only to
0064        # map.resource
0065        self.member_name = kargs.pop('_member_name', None)
0066        self.collection_name = kargs.pop('_collection_name', None)
0067        self.parent_resource = kargs.pop('_parent_resource', None)
0068
0069        # Pull out route conditions
0070        self.conditions = kargs.pop('conditions', None)
0071
0072        # Determine if explicit behavior should be used
0073        self.explicit = kargs.pop('_explicit', False)
0074
0075        # reserved keys that don't count
0076        reserved_keys = ['requirements']
0077
0078        # special chars to indicate a natural split in the URL
0079        self.done_chars = ('/', ',', ';', '.', '#')
0080
0081        # Strip preceding '/' if present
0082        if routepath.startswith('/'):
0083            routepath = routepath[1:]
0084
0085        # Build our routelist, and the keys used in the route
0086        self.routelist = routelist = self._pathkeys(routepath)
0087        routekeys = frozenset([key['name'] for key in routelist                                  if isinstance(key, dict)])
0089
0090        # Build a req list with all the regexp requirements for our args
0091        self.reqs = kargs.get('requirements', {})
0092        self.req_regs = {}
0093        for key, val in self.reqs.iteritems():
0094            self.req_regs[key] = re.compile('^' + val + '$')
0095        # Update our defaults and set new default keys if needed. defaults
0096        # needs to be saved
0097        (self.defaults, defaultkeys) = self._defaults(routekeys,
0098                                                      reserved_keys, kargs)
0099        # Save the maximum keys we could utilize
0100        self.maxkeys = defaultkeys | routekeys
0101
0102        # Populate our minimum keys, and save a copy of our backward keys for
0103        # quicker generation later
0104        (self.minkeys, self.routebackwards) = self._minkeys(routelist[:])
0105
0106        # Populate our hardcoded keys, these are ones that are set and don't 
0107        # exist in the route
0108        self.hardcoded = frozenset([key for key in self.maxkeys               if key not in routekeys and self.defaults[key] is not None])
0110
0111    def _pathkeys(self, routepath):
0112        """Utility function to walk the route, and pull out the valid 
0113        dynamic/wildcard keys."""
0114        collecting = False
0115        current = ''
0116        done_on = ''
0117        var_type = ''
0118        just_started = False
0119        routelist = []
0120        for char in routepath:
0121            if char in [':', '*'] and not collecting:
0122                just_started = True
0123                collecting = True
0124                var_type = char
0125                if len(current) > 0:
0126                    routelist.append(current)
0127                    current = ''
0128            elif collecting and just_started:
0129                just_started = False
0130                if char == '(':
0131                    done_on = ')'
0132                else:
0133                    current = char
0134                    done_on = self.done_chars + ('-',)
0135            elif collecting and char not in done_on:
0136                current += char
0137            elif collecting:
0138                collecting = False
0139                routelist.append(dict(type=var_type, name=current))
0140                if char in self.done_chars:
0141                    routelist.append(char)
0142                done_on = var_type = current = ''
0143            else:
0144                current += char
0145        if collecting:
0146            routelist.append(dict(type=var_type, name=current))
0147        elif current:
0148            routelist.append(current)
0149        return routelist
0150
0151    def _minkeys(self, routelist):
0152        """Utility function to walk the route backwards
0153        
0154        Will also determine the minimum keys we can handle to generate a 
0155        working route.
0156        
0157        routelist is a list of the '/' split route path
0158        defaults is a dict of all the defaults provided for the route
0159        """
0160        minkeys = []
0161        backcheck = routelist[:]
0162        gaps = False
0163        backcheck.reverse()
0164        for part in backcheck:
0165            if not isinstance(part, dict) and part not in self.done_chars:
0166                gaps = True
0167                continue
0168            elif not isinstance(part, dict):
0169                continue
0170            key = part['name']
0171            if self.defaults.has_key(key) and not gaps:
0172                continue
0173            minkeys.append(key)
0174            gaps = True
0175        return  (frozenset(minkeys), backcheck)
0176
0177    def _defaults(self, routekeys, reserved_keys, kargs):
0178        """Creates default set with values stringified
0179        
0180        Put together our list of defaults, stringify non-None values
0181        and add in our action/id default if they use it and didn't specify it
0182        
0183        defaultkeys is a list of the currently assumed default keys
0184        routekeys is a list of the keys found in the route path
0185        reserved_keys is a list of keys that are not
0186        
0187        """
0188        defaults = {}
0189        # Add in a controller/action default if they don't exist
0190        if 'controller' not in routekeys and 'controller' not in kargs              and not self.explicit:
0192            kargs['controller'] = 'content'
0193        if 'action' not in routekeys and 'action' not in kargs              and not self.explicit:
0195            kargs['action'] = 'index'
0196        defaultkeys = frozenset([key for key in kargs.keys()                                    if key not in reserved_keys])
0198        for key in defaultkeys:
0199            if kargs[key] is not None:
0200                defaults[key] = unicode(kargs[key])
0201            else:
0202                defaults[key] = None
0203        if 'action' in routekeys and not defaults.has_key('action')              and not self.explicit:
0205            defaults['action'] = 'index'
0206        if 'id' in routekeys and not defaults.has_key('id')              and not self.explicit:
0208            defaults['id'] = None
0209        newdefaultkeys = frozenset([key for key in defaults.keys()                                       if key not in reserved_keys])
0211        return (defaults, newdefaultkeys)
0212
0213    def makeregexp(self, clist):
0214        """Create a regular expression for matching purposes
0215        
0216        Note: This MUST be called before match can function properly.
0217        
0218        clist should be a list of valid controller strings that can be 
0219        matched, for this reason makeregexp should be called by the web 
0220        framework after it knows all available controllers that can be 
0221        utilized.
0222        """
0223        (reg, noreqs, allblank) = self.buildnextreg(self.routelist, clist)
0224
0225        if not reg:
0226            reg = '/'
0227        reg = reg + '(/)?' + '$'
0228        if not reg.startswith('/'):
0229            reg = '/' + reg
0230        reg = '^' + reg
0231
0232        self.regexp = reg
0233        self.regmatch = re.compile(reg)
0234
0235    def buildnextreg(self, path, clist):
0236        """Recursively build our regexp given a path, and a controller list.
0237        
0238        Returns the regular expression string, and two booleans that can be
0239        ignored as they're only used internally by buildnextreg.
0240        """
0241        if path:
0242            part = path[0]
0243        else:
0244            part = ''
0245        reg = ''
0246
0247        # noreqs will remember whether the remainder has either a string 
0248        # match, or a non-defaulted regexp match on a key, allblank remembers
0249        # if the rest could possible be completely empty
0250        (rest, noreqs, allblank) = ('', True, True)
0251        if len(path[1:]) > 0:
0252            self.prior = part
0253            (rest, noreqs, allblank) = self.buildnextreg(path[1:], clist)
0254
0255        if isinstance(part, dict) and part['type'] == ':':
0256            var = part['name']
0257            partreg = ''
0258
0259            # First we plug in the proper part matcher
0260            if self.reqs.has_key(var):
0261                partreg = '(?P<' + var + '>' + self.reqs[var] + ')'
0262            elif var == 'controller':
0263                partreg = '(?P<' + var + '>' + '|'.join(map(re.escape, clist))
0264                partreg += ')'
0265            elif self.prior in ['/', '#']:
0266                partreg = '(?P<' + var + '>[^' + self.prior + ']+?)'
0267            else:
0268                if not rest:
0269                    partreg = '(?P<' + var + '>[^%s]+?)' % '/'
0270                else:
0271                    end = ''.join(self.done_chars)
0272                    rem = rest
0273                    if rem[0] == '\\' and len(rem) > 1:
0274                        rem = rem[1]
0275                    elif rem.startswith('(\\') and len(rem) > 2:
0276                        rem = rem[2]
0277                    else:
0278                        rem = end
0279                    rem = frozenset(rem) | frozenset(['/'])
0280                    partreg = '(?P<' + var + '>[^%s]+?)' % ''.join(rem)
0281
0282            if self.reqs.has_key(var):
0283                noreqs = False
0284            if not self.defaults.has_key(var):
0285                allblank = False
0286                noreqs = False
0287
0288            # Now we determine if its optional, or required. This changes 
0289            # depending on what is in the rest of the match. If noreqs is 
0290            # true, then its possible the entire thing is optional as there's
0291            # no reqs or string matches.
0292            if noreqs:
0293                # The rest is optional, but now we have an optional with a 
0294                # regexp. Wrap to ensure that if we match anything, we match
0295                # our regexp first. It's still possible we could be completely
0296                # blank as we have a default
0297                if self.reqs.has_key(var) and self.defaults.has_key(var):
0298                    reg = '(' + partreg + rest + ')?'
0299
0300                # Or we have a regexp match with no default, so now being 
0301                # completely blank form here on out isn't possible
0302                elif self.reqs.has_key(var):
0303                    allblank = False
0304                    reg = partreg + rest
0305
0306                # If the character before this is a special char, it has to be
0307                # followed by this
0308                elif self.defaults.has_key(var) and                        self.prior in (',', ';', '.'):
0310                    reg = partreg + rest
0311
0312                # Or we have a default with no regexp, don't touch the allblank
0313                elif self.defaults.has_key(var):
0314                    reg = partreg + '?' + rest
0315
0316                # Or we have a key with no default, and no reqs. Not possible
0317                # to be all blank from here
0318                else:
0319                    allblank = False
0320                    reg = partreg + rest
0321            # In this case, we have something dangling that might need to be
0322            # matched
0323            else:
0324                # If they can all be blank, and we have a default here, we know
0325                # its safe to make everything from here optional. Since 
0326                # something else in the chain does have req's though, we have
0327                # to make the partreg here required to continue matching
0328                if allblank and self.defaults.has_key(var):
0329                    reg = '(' + partreg + rest + ')?'
0330
0331                # Same as before, but they can't all be blank, so we have to 
0332                # require it all to ensure our matches line up right
0333                else:
0334                    reg = partreg + rest
0335        elif isinstance(part, dict) and part['type'] == '*':
0336            var = part['name']
0337            if noreqs:
0338                if self.defaults.has_key(var):
0339                    reg = '(?P<' + var + '>.*)' + rest
0340                else:
0341                    reg = '(?P<' + var + '>.*)' + rest
0342                    allblank = False
0343                    noreqs = False
0344            else:
0345                if allblank and self.defaults.has_key(var):
0346                    reg = '(?P<' + var + '>.*)' + rest
0347                elif self.defaults.has_key(var):
0348                    reg = '(?P<' + var + '>.*)' + rest
0349                else:
0350                    allblank = False
0351                    noreqs = False
0352                    reg = '(?P<' + var + '>.*)' + rest
0353        elif part and part[-1] in self.done_chars:
0354            if allblank:
0355                reg = re.escape(part[:-1]) + '(' + re.escape(part[-1]) + rest
0356                reg += ')?'
0357            else:
0358                allblank = False
0359                reg = re.escape(part) + rest
0360
0361        # We have a normal string here, this is a req, and it prevents us from 
0362        # being all blank
0363        else:
0364            noreqs = False
0365            allblank = False
0366            reg = re.escape(part) + rest
0367
0368        return (reg, noreqs, allblank)
0369
0370    def match(self, url, environ=None, sub_domains=False,
0371              sub_domains_ignore=None, domain_match=''):
0372        """Match a url to our regexp. 
0373        
0374        While the regexp might match, this operation isn't
0375        guaranteed as there's other factors that can cause a match to fail 
0376        even though the regexp succeeds (Default that was relied on wasn't 
0377        given, requirement regexp doesn't pass, etc.).
0378        
0379        Therefore the calling function shouldn't assume this will return a
0380        valid dict, the other possible return is False if a match doesn't work
0381        out.
0382        """
0383        # Static routes don't match, they generate only
0384        if self.static:
0385            return False
0386
0387        if url.endswith('/') and len(url) > 1:
0388            url = url[:-1]
0389        match = self.regmatch.match(url)
0390
0391        if not match:
0392            return False
0393
0394        if not environ:
0395            environ = {}
0396
0397        sub_domain = None
0398
0399        if environ.get('HTTP_HOST') and sub_domains:
0400            host = environ['HTTP_HOST'].split(':')[0]
0401            sub_match = re.compile('^(.+?)\.%s$' % domain_match)
0402            subdomain = re.sub(sub_match, r'\1', host)
0403            if subdomain not in sub_domains_ignore and host != subdomain:
0404                sub_domain = subdomain
0405
0406        if self.conditions:
0407            if self.conditions.has_key('method') and                   environ.get('REQUEST_METHOD') not in self.conditions['method']:
0409                return False
0410
0411            # Check sub-domains?
0412            use_sd = self.conditions.get('sub_domain')
0413            if use_sd and not sub_domain:
0414                return False
0415            if isinstance(use_sd, list) and sub_domain not in use_sd:
0416                return False
0417
0418        matchdict = match.groupdict()
0419        result = {}
0420        extras = frozenset(self.defaults.keys()) - frozenset(matchdict.keys())
0421        for key, val in matchdict.iteritems