lib/urltree.py
changeset 46 185504387370
parent 45 e94ab812c0c8
child 47 3d59c9eeffaa
equal deleted inserted replaced
45:e94ab812c0c8 46:185504387370
     1 """
       
     2     Tree-based URL mapping
       
     3 """
       
     4 
       
     5 import re
       
     6 import os.path
       
     7 
       
     8 # for Mapper
       
     9 from lib import map
       
    10 
       
    11 class URLError (Exception) :
       
    12     """
       
    13         Error with an URL definition
       
    14     """
       
    15 
       
    16     pass
       
    17 
       
    18 class LabelValue (object) :
       
    19     """
       
    20         Represents the value of a ValueLabel... love these names
       
    21     """
       
    22 
       
    23     def __init__ (self, label, value) :
       
    24         """
       
    25             Just store
       
    26         """
       
    27 
       
    28         self.label = label
       
    29         self.value = value
       
    30     
       
    31     def __str__ (self) :
       
    32         return "%s=%r" % (self.label.key, self.value)
       
    33 
       
    34     def __repr__ (self) :
       
    35         return "<%s>" % self
       
    36 
       
    37 class Label (object) :
       
    38     """
       
    39         Base class for URL labels (i.e. the segments of the URL between /s)
       
    40     """
       
    41 
       
    42     @staticmethod
       
    43     def parse (mask, defaults, types) :
       
    44         """
       
    45             Parse the given label-segment, and return a *Label instance
       
    46         """
       
    47 
       
    48         # empty?
       
    49         if not mask :
       
    50             return EmptyLabel()
       
    51 
       
    52         # simple value?
       
    53         match = SimpleValueLabel.EXPR.match(mask)
       
    54 
       
    55         if match :
       
    56             # key
       
    57             key = match.group('key')
       
    58 
       
    59             # type
       
    60             type = match.group("type")
       
    61             
       
    62             # lookup type, None for default
       
    63             type = types[type]
       
    64 
       
    65             # defaults?
       
    66             default = defaults.get(key)
       
    67 
       
    68             if not default :
       
    69                 default = match.group('default')
       
    70 
       
    71                 if default :
       
    72                     # apply type to default
       
    73                     default = type(default)
       
    74 
       
    75             # build
       
    76             return SimpleValueLabel(key, type, default)
       
    77         
       
    78         # static?
       
    79         match = StaticLabel.EXPR.match(mask)
       
    80 
       
    81         if match :
       
    82             return StaticLabel(match.group('name'))
       
    83 
       
    84         # invalid
       
    85         raise URLError("Invalid label: %r" % (mask, ))
       
    86     
       
    87     def match (self, value=None) :
       
    88         """
       
    89             Match this label against the given value, returning either True to match without a value, a LabelValue
       
    90             object, or boolean false to not match.
       
    91 
       
    92             If value is None, this means that only a default value should be returned.
       
    93         """
       
    94 
       
    95         abstract
       
    96     
       
    97     def build (self, value_dict) :
       
    98         """
       
    99             Return a string representing this label, using the values in the given value_dict if needed
       
   100         """
       
   101 
       
   102         abstract
       
   103 
       
   104 class EmptyLabel (Label) :
       
   105     """
       
   106         An empty label, i.e. just a slash in the URL
       
   107     """
       
   108     
       
   109     def __eq__ (self, other) :
       
   110         """
       
   111             Just compares type
       
   112         """
       
   113 
       
   114         return isinstance(other, EmptyLabel)
       
   115     
       
   116     def match (self, value=None) :
       
   117         """
       
   118             Match empty string -> True
       
   119         """
       
   120         
       
   121         # no default
       
   122         if value is None :
       
   123             return False
       
   124         
       
   125         # only empty segments
       
   126         if value == '' :
       
   127             return True
       
   128     
       
   129     def build (self, values) :
       
   130         return str(self)
       
   131 
       
   132     def __str__ (self) :
       
   133         return ''
       
   134 
       
   135 class StaticLabel (Label) :
       
   136     """
       
   137         A simple literal Label, used for fixed terms in the URL
       
   138     """
       
   139 
       
   140     EXPR = re.compile(r'^(?P<name>[a-zA-Z_.-]+)$')
       
   141 
       
   142     def __init__ (self, name) :
       
   143         """
       
   144             The given name is the literal name of this label
       
   145         """
       
   146 
       
   147         self.name = name
       
   148 
       
   149     def __eq__ (self, other) :
       
   150         """
       
   151             Compares names
       
   152         """
       
   153 
       
   154         return isinstance(other, StaticLabel) and self.name == other.name
       
   155     
       
   156     def match (self, value=None) :
       
   157         """
       
   158             Match exactly -> True
       
   159         """
       
   160 
       
   161         # no defaults
       
   162         if value is None :
       
   163             return False
       
   164         
       
   165         # match name
       
   166         if value == self.name :
       
   167             return True
       
   168 
       
   169     def build (self, values) :
       
   170         return str(self)
       
   171 
       
   172     def __str__ (self) :
       
   173         return self.name
       
   174 
       
   175 class ValueLabel (Label) :
       
   176     """
       
   177         A label with a key and a value
       
   178 
       
   179         XXX: do we even need this?
       
   180     """
       
   181 
       
   182     def __init__ (self, key, default) :
       
   183         """
       
   184             Set the key and default value. Default value may be None if there is no default value defined
       
   185         """
       
   186 
       
   187         self.key = key
       
   188         self.default = default
       
   189 
       
   190     def __eq__ (self, other) :
       
   191         """
       
   192             Compares keys
       
   193         """
       
   194 
       
   195         return isinstance(other, ValueLabel) and self.key == other.key
       
   196     
       
   197     def build (self, values) :
       
   198         """
       
   199             Return either the assigned value from values, our default value, or raise an error
       
   200         """
       
   201 
       
   202         value = values.get(self.key)
       
   203         
       
   204         if not value and self.default :
       
   205             value = self.default
       
   206 
       
   207         elif not value :
       
   208             raise URLError("No value given for label %r" % (self.key, ))
       
   209 
       
   210         return value
       
   211 
       
   212 class SimpleValueLabel (ValueLabel) :
       
   213     """
       
   214         A label that has a name and a simple string value
       
   215     """
       
   216 
       
   217     EXPR = re.compile(r'^\{(?P<key>[a-zA-Z_][a-zA-Z0-9_]*)(:(?P<type>[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P<default>[^}]+))?\}$')
       
   218 
       
   219     def __init__ (self, key, type=str, default=None) :
       
   220         """
       
   221             The given key is the name of this label's value
       
   222         """
       
   223 
       
   224         # type
       
   225         self.type = type
       
   226 
       
   227         # store
       
   228         self.key = key
       
   229         self.default = default
       
   230         
       
   231     def match (self, value=None) :
       
   232         """
       
   233             Match -> LabelValue
       
   234         """
       
   235         
       
   236         # default?
       
   237         if value is None and self.default :
       
   238             return LabelValue(self, self.default)
       
   239         
       
   240         # only non-empty values!
       
   241         elif value :
       
   242             # convert with type
       
   243             try :
       
   244                 value = self.type(value)
       
   245 
       
   246             except Exception, e :
       
   247                 raise URLError("Bad value %r for type %s: %s: %s" % (value, self.type.__name__, type(e).__name__, e))
       
   248 
       
   249             return LabelValue(self, value)
       
   250 
       
   251     def __str__ (self) :
       
   252         return '{%s%s%s}' % (
       
   253             self.key, 
       
   254             ':%s' % (self.type.__name__ ) if self.type != str else '',
       
   255             '=%s' % (self.default, ) if self.default else '',
       
   256         )
       
   257 
       
   258 class URLConfig (object) :
       
   259     """
       
   260         Global configuration relevant to all URLs
       
   261     """
       
   262 
       
   263     # built-in type codes
       
   264     BUILTIN_TYPES = {
       
   265         # default
       
   266         None    : str,
       
   267 
       
   268         # string
       
   269         'str'   : str,
       
   270 
       
   271         # integer
       
   272         'int'   : int,
       
   273     }
       
   274 
       
   275     def __init__ (self, type_dict=None) :
       
   276         """
       
   277             Create an URLConfig for use with URL
       
   278 
       
   279             If type_dict is given, it should be a mapping of type names -> callables, and they will be available for
       
   280             type specifications in addition to the defaults.
       
   281         """
       
   282 
       
   283         # build our type_dict
       
   284         self.type_dict = self.BUILTIN_TYPES.copy()
       
   285         
       
   286         # apply the given type_dict
       
   287         if type_dict :
       
   288             self.type_dict.update(type_dict)
       
   289 
       
   290 class URL (object) :
       
   291     """
       
   292         Represents a specific URL
       
   293     """
       
   294 
       
   295 
       
   296     def __init__ (self, config, url_mask, handler, type_dict=None, **defaults) :
       
   297         """
       
   298             Create an URL using the given URLConfig, with the given url mask, handler, and default values.
       
   299         """
       
   300 
       
   301         # store
       
   302         self.config = config
       
   303         self.url_mask = url_mask
       
   304         self.handler = handler
       
   305         self.defaults = defaults
       
   306 
       
   307         # query string
       
   308         self.query_args = dict()
       
   309         
       
   310         # parse any query string
       
   311         # XXX: conflicts with regexp syntax
       
   312         if '/?' in url_mask :
       
   313             url_mask, query_mask = url_mask.split('/?')
       
   314         
       
   315         else :
       
   316             query_mask = None
       
   317 
       
   318         # build our label path
       
   319         self.label_path = [Label.parse(mask, defaults, config.type_dict) for mask in url_mask.split('/')]
       
   320 
       
   321         # build our query args list
       
   322         if query_mask :
       
   323             # split into items
       
   324             for query_item in query_mask.split('&') :
       
   325                 # parse default
       
   326                 if '=' in query_item :
       
   327                     query_item, default = query_item.split('=')
       
   328 
       
   329                 else :
       
   330                     default = None
       
   331                 
       
   332                 # parse type
       
   333                 if ':' in query_item :
       
   334                     query_item, type = query_item.split(':')
       
   335                 else :
       
   336                     type = None
       
   337                 
       
   338                 # parse key
       
   339                 key = query_item
       
   340 
       
   341                 # type
       
   342                 type = self.config.type_dict[type]
       
   343 
       
   344                 # add to query_args as (type, default) tuple
       
   345                 self.query_args[key] = (type, type(default) if default else default)
       
   346          
       
   347     def get_label_path (self) :
       
   348         """
       
   349             Returns a list containing the labels in this url
       
   350         """
       
   351         
       
   352         # copy self.label_path
       
   353         return list(self.label_path)
       
   354 
       
   355     def execute (self, request, label_values) :
       
   356         """
       
   357             Invoke the handler, using the given label values
       
   358         """
       
   359         
       
   360         # start with the defaults
       
   361         kwargs = self.defaults.copy()
       
   362 
       
   363         # then add all the values
       
   364         for label_value in label_values :
       
   365             kwargs[label_value.label.key] = label_value.value
       
   366        
       
   367         # then parse all query args
       
   368         for key, value in request.get_args() :
       
   369             # lookup spec
       
   370             type, default = self.query_args[key]
       
   371 
       
   372             # normalize empty value to None
       
   373             if not value :
       
   374                 value = None
       
   375 
       
   376             else :
       
   377                 # process value
       
   378                 value = type(value)
       
   379 
       
   380             # set default?
       
   381             if not value :
       
   382                 if default :
       
   383                     value = default
       
   384 
       
   385                 if default == '' :
       
   386                     # do not pass key at all
       
   387                     continue
       
   388 
       
   389                 # otherwise, fail
       
   390                 raise URLError("No value given for required argument: %r" % (key, ))
       
   391             
       
   392             # set key
       
   393             kwargs[key] = value
       
   394         
       
   395         # then check all query args
       
   396         for key, (type, default) in self.query_args.iteritems() :
       
   397             # skip those already present
       
   398             if key in kwargs :
       
   399                 continue
       
   400 
       
   401             # apply default?
       
   402             if default is None :
       
   403                 raise URLError("Missing required argument: %r" % (key, ))
       
   404             
       
   405             elif default == '' :
       
   406                 # skip empty default
       
   407                 continue
       
   408 
       
   409             else :
       
   410                 # set default
       
   411                 kwargs[key] = default
       
   412 
       
   413         # execute the handler
       
   414         return self.handler(request, **kwargs)
       
   415     
       
   416     def build (self, request, **values) :
       
   417         """
       
   418             Build an absolute URL pointing to this target, with the given values
       
   419         """
       
   420 
       
   421         # build URL from request page prefix and our labels
       
   422         return request.page_prefix + '/'.join(label.build(values) for label in self.label_path)
       
   423 
       
   424     def __str__ (self) :
       
   425         return '/'.join(str(label) for label in self.label_path)
       
   426     
       
   427     def __repr__ (self) :
       
   428         return "URL(%r, %r)" % (str(self), self.handler)
       
   429 
       
   430 class URLNode (object) :
       
   431     """
       
   432         Represents a node in the URLTree
       
   433     """
       
   434 
       
   435     def __init__ (self, parent, label) :
       
   436         """
       
   437             Initialize with the given parent and label, empty children dict
       
   438         """
       
   439         
       
   440         # the parent URLNode
       
   441         self.parent = parent
       
   442 
       
   443         # this node's Label
       
   444         self.label = label
       
   445 
       
   446         # list of child URLNodes
       
   447         self.children = []
       
   448 
       
   449         # this node's URL, set by add_url for an empty label_path
       
   450         self.url = None
       
   451 
       
   452     def _build_child (self, label) :
       
   453         """
       
   454             Build, insert and return a new child Node
       
   455         """
       
   456         
       
   457         # build new child
       
   458         child = URLNode(self, label)
       
   459         
       
   460         # add to children
       
   461         self.children.append(child)
       
   462 
       
   463         # return
       
   464         return child
       
   465 
       
   466     def add_url (self, url, label_path) :
       
   467         """
       
   468             Add a URL object to this node under the given path. Uses recursion to process the path.
       
   469 
       
   470             The label_path argument is a (partial) label path as returned by URL.get_label_path.
       
   471 
       
   472             If label_path is empty (len zero, or begins with EmptyLabel), then the given url is assigned to this node, if no
       
   473             url was assigned before.
       
   474         """
       
   475         
       
   476         # matches this node?
       
   477         if not label_path or isinstance(label_path[0], EmptyLabel) :
       
   478             if self.url :
       
   479                 raise URLError(url, "node already defined")
       
   480 
       
   481             else :
       
   482                 # set
       
   483                 self.url = url
       
   484 
       
   485         else :
       
   486             # pop child label from label_path
       
   487             child_label = label_path.pop(0)
       
   488 
       
   489             # look for the child to recurse into
       
   490             child = None
       
   491 
       
   492             # look for an existing child with that label
       
   493             for child in self.children :
       
   494                 if child.label == child_label :
       
   495                     # found, use this
       
   496                     break
       
   497 
       
   498             else :
       
   499                 # build a new child
       
   500                 child = self._build_child(child_label)
       
   501 
       
   502             # recurse to handle the rest of the label_path
       
   503             child.add_url(url, label_path)
       
   504     
       
   505     def match (self, label_path) :
       
   506         """
       
   507             Locate the URL object corresponding to the given label_path value under this node.
       
   508 
       
   509             Returns a (url, label_values) tuple
       
   510         """
       
   511 
       
   512         # determine value to use
       
   513         value = None
       
   514 
       
   515         # empty label_path?
       
   516         if not label_path or label_path[0] == '' :
       
   517             # the search ends at this node
       
   518             if self.url :
       
   519                 # this URL is the best match
       
   520                 return (self.url, [])
       
   521             
       
   522             elif not self.children :
       
   523                 # incomplete URL
       
   524                 raise URLError("no URL handler defined for this Node")
       
   525             
       
   526             else :
       
   527                 # use default value, i.e. Label.match(None)
       
   528                 label = None
       
   529 
       
   530         else :
       
   531             # pop the next label from the label path
       
   532             label = label_path.pop(0)
       
   533 
       
   534         # return one match...
       
   535         match = value = None
       
   536 
       
   537         # recurse through our children, DFS
       
   538         for child in self.children :
       
   539             # match value
       
   540             value = child.label.match(label)
       
   541 
       
   542             # skip those that don't match at all
       
   543             if not value :
       
   544                 continue;
       
   545             
       
   546             # already found a match? :/
       
   547             if match :
       
   548                 raise URLError("Ambiguous URL")
       
   549 
       
   550             # ok, but continue looking to make sure there's no ambiguous URLs
       
   551             match = child
       
   552         
       
   553         # found something?
       
   554         if not match :
       
   555             raise URLError("No child found for label: %s + %s + %s" % (self.get_url(), label, '/'.join(str(l) for l in label_path)))
       
   556 
       
   557         # ok, recurse into the match
       
   558         url, label_value = match.match(label_path)
       
   559 
       
   560         # add our value?
       
   561         if isinstance(value, LabelValue) :
       
   562             label_value.append(value)
       
   563 
       
   564         # return the match
       
   565         return url, label_value
       
   566 
       
   567     def get_url (self) :
       
   568         """
       
   569             Returns the URL for this node, by iterating over our parents
       
   570         """
       
   571         
       
   572         # URL segments in reverse order
       
   573         segments = ['']
       
   574         
       
   575         # start with ourself
       
   576         node = self
       
   577         
       
   578         # iterate up to root
       
   579         while node :
       
   580             segments.append(str(node.label))
       
   581 
       
   582             node = node.parent
       
   583 
       
   584         # reverse
       
   585         segments.reverse()
       
   586 
       
   587         # return
       
   588         return '/'.join(segments)
       
   589 
       
   590     def dump (self, indent=0) :
       
   591         """
       
   592             Returns a multi-line string representation of this Node
       
   593         """
       
   594 
       
   595         return '\n'.join([
       
   596             "%-45s%s" % (
       
   597                 ' '*indent + str(self.label) + ('/' if self.children else ''), 
       
   598                 (' -> %r' % self.url) if self.url else ''
       
   599             )
       
   600         ] + [
       
   601             child.dump(indent + 4) for child in self.children
       
   602         ])
       
   603 
       
   604     def __str__ (self) :
       
   605         return "%s/[%s]" % (self.label, ','.join(str(child) for child in self.children))
       
   606 
       
   607 class URLTree (map.Mapper) :
       
   608     """
       
   609         Map requests to handlers, using a defined tree of URLs
       
   610     """
       
   611 
       
   612     def __init__ (self, url_list) :
       
   613         """
       
   614             Initialize the tree using the given list of URLs
       
   615         """
       
   616 
       
   617         # root node
       
   618         self.root = URLNode(None, EmptyLabel())
       
   619         
       
   620         # just add each URL
       
   621         for url in url_list :
       
   622             self.add_url(url)
       
   623 
       
   624     def add_url (self, url) :
       
   625         """
       
   626             Adds the given URL to the tree. The URL must begin with a root slash.
       
   627         """
       
   628         # get url's label path
       
   629         path = url.get_label_path()
       
   630 
       
   631         # should begin with root
       
   632         root_label = path.pop(0)
       
   633         assert root_label == self.root.label, "URL must begin with root"
       
   634 
       
   635         # add to root
       
   636         self.root.add_url(url, path)
       
   637         
       
   638     def match (self, url) :
       
   639         """
       
   640             Find the URL object best corresponding to the given url, matching any ValueLabels.
       
   641 
       
   642             Returns an (URL, [LabelValue]) tuple.
       
   643         """
       
   644 
       
   645         # split it into labels
       
   646         path = url.split('/')
       
   647         
       
   648         # empty URL is empty
       
   649         if url :
       
   650             # ensure that it doesn't start with a /
       
   651             assert not self.root.label.match(path[0]), "URL must not begin with root"
       
   652 
       
   653         # just match starting at root
       
   654         return self.root.match(path)
       
   655 
       
   656     def handle_request (self, request) :
       
   657         """
       
   658             Looks up the request's URL, and invokes its handler
       
   659         """
       
   660         
       
   661         # get the requested URL
       
   662         request_url = request.get_page_name()
       
   663 
       
   664         # find the URL+values to use
       
   665         url, label_values = self.match(request_url)
       
   666 
       
   667         # let the URL handle it
       
   668         return url.execute(request, label_values)
       
   669 
       
   670