sites/irclogs.qmsk.net/urltree.py
branchsites
changeset 39 82df0bb66ca7
child 40 71ab68f31a1c
equal deleted inserted replaced
38:9737b6ca2295 39:82df0bb66ca7
       
     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) :
       
    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             if type :
       
    63                 # XXX: resolve using eval() for now, should be a module or something
       
    64                 type = eval(type)
       
    65 
       
    66             else :
       
    67                 type = str
       
    68 
       
    69             # defaults?
       
    70             default = defaults.get(key)
       
    71 
       
    72             if not default :
       
    73                 default = match.group('default')
       
    74 
       
    75                 if default :
       
    76                     # apply type to default
       
    77                     default = type(default)
       
    78 
       
    79             # build
       
    80             return SimpleValueLabel(key, type, default)
       
    81         
       
    82         # static?
       
    83         match = StaticLabel.EXPR.match(mask)
       
    84 
       
    85         if match :
       
    86             return StaticLabel(match.group('name'))
       
    87 
       
    88         # invalid
       
    89         raise URLError("Invalid label: %r" % (mask, ))
       
    90     
       
    91     def match (self, value=None) :
       
    92         """
       
    93             Match this label against the given value, returning either True to match without a value, a LabelValue
       
    94             object, or boolean false to not match.
       
    95 
       
    96             If value is None, this means that only a default value should be returned.
       
    97         """
       
    98 
       
    99         abstract
       
   100 
       
   101 class EmptyLabel (Label) :
       
   102     """
       
   103         An empty label, i.e. just a slash in the URL
       
   104     """
       
   105     
       
   106     def __eq__ (self, other) :
       
   107         """
       
   108             Just compares type
       
   109         """
       
   110 
       
   111         return isinstance(other, EmptyLabel)
       
   112     
       
   113     def match (self, value=None) :
       
   114         """
       
   115             Match empty string -> True
       
   116         """
       
   117         
       
   118         # no default
       
   119         if value is None :
       
   120             return False
       
   121         
       
   122         # only empty segments
       
   123         if value == '' :
       
   124             return True
       
   125 
       
   126     def __str__ (self) :
       
   127         return ''
       
   128 
       
   129 class StaticLabel (Label) :
       
   130     """
       
   131         A simple literal Label, used for fixed terms in the URL
       
   132     """
       
   133 
       
   134     EXPR = re.compile(r'^(?P<name>[a-zA-Z_.-]+)$')
       
   135 
       
   136     def __init__ (self, name) :
       
   137         """
       
   138             The given name is the literal name of this label
       
   139         """
       
   140 
       
   141         self.name = name
       
   142 
       
   143     def __eq__ (self, other) :
       
   144         """
       
   145             Compares names
       
   146         """
       
   147 
       
   148         return isinstance(other, StaticLabel) and self.name == other.name
       
   149     
       
   150     def match (self, value=None) :
       
   151         """
       
   152             Match exactly -> True
       
   153         """
       
   154 
       
   155         # no defaults
       
   156         if value is None :
       
   157             return False
       
   158         
       
   159         # match name
       
   160         if value == self.name :
       
   161             return True
       
   162 
       
   163     def __str__ (self) :
       
   164         return self.name
       
   165 
       
   166 class ValueLabel (Label) :
       
   167     """
       
   168         A label with a key and a value
       
   169 
       
   170         XXX: do we even need this?
       
   171     """
       
   172 
       
   173     def __init__ (self, key, default) :
       
   174         """
       
   175             Set the key and default value. Default value may be None if there is no default value defined
       
   176         """
       
   177 
       
   178         self.key = key
       
   179         self.default = default
       
   180 
       
   181     def __eq__ (self, other) :
       
   182         """
       
   183             Compares keys
       
   184         """
       
   185 
       
   186         return isinstance(other, ValueLabel) and self.key == other.key
       
   187 
       
   188 class SimpleValueLabel (ValueLabel) :
       
   189     """
       
   190         A label that has a name and a simple string value
       
   191     """
       
   192 
       
   193     EXPR = re.compile(r'^\{(?P<key>[a-zA-Z_][a-zA-Z0-9_]*)(:(?P<type>[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P<default>[^}]+))?\}$')
       
   194 
       
   195     def __init__ (self, key, type=str, default=None) :
       
   196         """
       
   197             The given key is the name of this label's value
       
   198         """
       
   199 
       
   200         # type
       
   201         self.type = type
       
   202 
       
   203         # store
       
   204         self.key = key
       
   205         self.default = default
       
   206         
       
   207     def match (self, value=None) :
       
   208         """
       
   209             Match -> LabelValue
       
   210         """
       
   211         
       
   212         # default?
       
   213         if value is None and self.default :
       
   214             return LabelValue(self, self.default)
       
   215         
       
   216         # only non-empty values!
       
   217         elif value :
       
   218             # convert with type
       
   219             try :
       
   220                 value = self.type(value)
       
   221 
       
   222             except Exception, e :
       
   223                 raise URLError("Bad value %r for type %s: %s" % (value, self.type.__name__, e))
       
   224 
       
   225             return LabelValue(self, value)
       
   226 
       
   227     def __str__ (self) :
       
   228         return '{%s%s%s}' % (
       
   229             self.key, 
       
   230             ':%s' % (self.type.__name__ ) if self.type != str else '',
       
   231             '=%s' % (self.default, ) if self.default else '',
       
   232         )
       
   233             
       
   234 class URL (object) :
       
   235     """
       
   236         Represents a specific URL
       
   237     """
       
   238 
       
   239     def __init__ (self, url_mask, handler, **defaults) :
       
   240         """
       
   241             Create an URL with the given url mask, handler, and default values
       
   242         """
       
   243 
       
   244         # store
       
   245         self.url_mask = url_mask
       
   246         self.handler = handler
       
   247         self.defaults = defaults
       
   248 
       
   249         # build our labels
       
   250         self.label_path = [Label.parse(mask, defaults) for mask in url_mask.split('/')]
       
   251         
       
   252     def get_label_path (self) :
       
   253         """
       
   254             Returns a list containing the labels in this url
       
   255         """
       
   256         
       
   257         # copy self.label_path
       
   258         return list(self.label_path)
       
   259 
       
   260     def execute (self, request, label_values) :
       
   261         """
       
   262             Invoke the handler, using the given label values
       
   263         """
       
   264         
       
   265         # start with the defaults
       
   266         kwargs = self.defaults()
       
   267 
       
   268         # then add all the values
       
   269         for label_value in label_values :
       
   270             kwargs[label_value.label.key] = label_value.value
       
   271             
       
   272         # execute the handler
       
   273         return self.handler(request, **kwargs)
       
   274 
       
   275     def __str__ (self) :
       
   276         return '/'.join(str(label) for label in self.label_path)
       
   277     
       
   278     def __repr__ (self) :
       
   279         return "URL(%r, %r)" % (str(self), self.handler)
       
   280 
       
   281 class URLNode (object) :
       
   282     """
       
   283         Represents a node in the URLTree
       
   284     """
       
   285 
       
   286     def __init__ (self, parent, label) :
       
   287         """
       
   288             Initialize with the given parent and label, empty children dict
       
   289         """
       
   290         
       
   291         # the parent URLNode
       
   292         self.parent = parent
       
   293 
       
   294         # this node's Label
       
   295         self.label = label
       
   296 
       
   297         # list of child URLNodes
       
   298         self.children = []
       
   299 
       
   300         # this node's URL, set by add_url for an empty label_path
       
   301         self.url = None
       
   302 
       
   303     def _build_child (self, label) :
       
   304         """
       
   305             Build, insert and return a new child Node
       
   306         """
       
   307         
       
   308         # build new child
       
   309         child = URLNode(self, label)
       
   310         
       
   311         # add to children
       
   312         self.children.append(child)
       
   313 
       
   314         # return
       
   315         return child
       
   316 
       
   317     def add_url (self, url, label_path) :
       
   318         """
       
   319             Add a URL object to this node under the given path. Uses recursion to process the path.
       
   320 
       
   321             The label_path argument is a (partial) label path as returned by URL.get_label_path.
       
   322 
       
   323             If label_path is empty (len zero, or begins with EmptyLabel), then the given url is assigned to this node, if no
       
   324             url was assigned before.
       
   325         """
       
   326         
       
   327         # matches this node?
       
   328         if not label_path or isinstance(label_path[0], EmptyLabel) :
       
   329             if self.url :
       
   330                 raise URLError(url, "node already defined")
       
   331 
       
   332             else :
       
   333                 # set
       
   334                 self.url = url
       
   335 
       
   336         else :
       
   337             # pop child label from label_path
       
   338             child_label = label_path.pop(0)
       
   339 
       
   340             # look for the child to recurse into
       
   341             child = None
       
   342 
       
   343             # look for an existing child with that label
       
   344             for child in self.children :
       
   345                 if child.label == child_label :
       
   346                     # found, use this
       
   347                     break
       
   348 
       
   349             else :
       
   350                 # build a new child
       
   351                 child = self._build_child(child_label)
       
   352 
       
   353             # recurse to handle the rest of the label_path
       
   354             child.add_url(url, label_path)
       
   355     
       
   356     def match (self, label_path) :
       
   357         """
       
   358             Locate the URL object corresponding to the given label_path value under this node.
       
   359 
       
   360             Returns a (url, label_values) tuple
       
   361         """
       
   362 
       
   363         # determine value to use
       
   364         value = None
       
   365 
       
   366         # empty label_path?
       
   367         if not label_path or label_path[0] == '' :
       
   368             # the search ends at this node
       
   369             if self.url :
       
   370                 # this URL is the best match
       
   371                 return (self.url, [])
       
   372             
       
   373             elif not self.children :
       
   374                 # incomplete URL
       
   375                 raise URLError("no URL handler defined for this Node")
       
   376             
       
   377             else :
       
   378                 # use default value, i.e. Label.match(None)
       
   379                 label = None
       
   380 
       
   381         else :
       
   382             # pop the next label from the label path
       
   383             label = label_path.pop(0)
       
   384 
       
   385         # return one match...
       
   386         match = value = None
       
   387 
       
   388         # recurse through our children, DFS
       
   389         for child in self.children :
       
   390             # match value
       
   391             value = child.label.match(label)
       
   392 
       
   393             # skip those that don't match at all
       
   394             if not value :
       
   395                 continue;
       
   396             
       
   397             # already found a match? :/
       
   398             if match :
       
   399                 raise URLError("Ambiguous URL")
       
   400 
       
   401             # ok, but continue looking to make sure there's no ambiguous URLs
       
   402             match = child
       
   403         
       
   404         # found something?
       
   405         if not match :
       
   406             raise URLError("No child found for label")
       
   407 
       
   408         # ok, recurse into the match
       
   409         url, label_value = match.match(label_path)
       
   410 
       
   411         # add our value?
       
   412         if isinstance(value, LabelValue) :
       
   413             label_value.append(value)
       
   414 
       
   415         # return the match
       
   416         return url, label_value
       
   417     
       
   418     def dump (self, indent=0) :
       
   419         """
       
   420             Returns a multi-line string representation of this Node
       
   421         """
       
   422 
       
   423         return '\n'.join([
       
   424             "%-45s%s" % (
       
   425                 ' '*indent + str(self.label) + ('/' if self.children else ''), 
       
   426                 (' -> %r' % self.url) if self.url else ''
       
   427             )
       
   428         ] + [
       
   429             child.dump(indent + 4) for child in self.children
       
   430         ])
       
   431 
       
   432     def __str__ (self) :
       
   433         return "%s/[%s]" % (self.label, ','.join(str(child) for child in self.children))
       
   434 
       
   435 class URLTree (map.Mapper) :
       
   436     """
       
   437         Map requests to handlers, using a defined tree of URLs
       
   438     """
       
   439 
       
   440     def __init__ (self, url_list) :
       
   441         """
       
   442             Initialize the tree using the given list of URLs
       
   443         """
       
   444 
       
   445         # root node
       
   446         self.root = URLNode(None, EmptyLabel())
       
   447         
       
   448         # just add each URL
       
   449         for url in url_list :
       
   450             self.add_url(url)
       
   451 
       
   452     def add_url (self, url) :
       
   453         """
       
   454             Adds the given URL to the tree. The URL must begin with a root slash.
       
   455         """
       
   456         # get url's label path
       
   457         path = url.get_label_path()
       
   458 
       
   459         # should begin with root
       
   460         root_label = path.pop(0)
       
   461         assert root_label == self.root.label, "URL must begin with root"
       
   462 
       
   463         # add to root
       
   464         self.root.add_url(url, path)
       
   465         
       
   466     def match (self, url) :
       
   467         """
       
   468             Find the URL object best corresponding to the given url, matching any ValueLabels.
       
   469 
       
   470             Returns an (URL, [LabelValue]) tuple.
       
   471         """
       
   472 
       
   473         # normalize the URL
       
   474         url = os.path.normpath(url)
       
   475 
       
   476         # split it into labels
       
   477         path = url.split('/')
       
   478 
       
   479         # ensure that it starts with a /
       
   480         root_label = path.pop(0)
       
   481         assert self.root.label.match(root_label), "URL must begin with root"
       
   482 
       
   483         # just match starting at root
       
   484         return self.root.match(path)
       
   485 
       
   486     def handle_request (self, request) :
       
   487         """
       
   488             Looks up the request's URL, and invokes its handler
       
   489         """
       
   490         
       
   491         # get the request's URL path
       
   492         url, label_values = self.match(request.get_page_name())
       
   493 
       
   494         # let the URL handle it
       
   495         url.execute(request, label_values)
       
   496 
       
   497