urltree.py
changeset 47 99c45fc13edc
parent 46 54c5f5f340de
child 48 480adab03749
equal deleted inserted replaced
46:54c5f5f340de 47:99c45fc13edc
    37     """
    37     """
    38         Base class for URL labels (i.e. the segments of the URL between /s)
    38         Base class for URL labels (i.e. the segments of the URL between /s)
    39     """
    39     """
    40 
    40 
    41     @staticmethod
    41     @staticmethod
    42     def parse (mask, defaults, types) :
    42     def parse (mask, defaults, config) :
    43         """
    43         """
    44             Parse the given label-segment, and return a *Label instance
    44             Parse the given label-segment, and return a *Label instance. Config is the URLConfig to use
    45         """
    45         """
    46 
    46 
    47         # empty?
    47         # empty?
    48         if not mask :
    48         if not mask :
    49             return EmptyLabel()
    49             return EmptyLabel()
    56             key = match.group('key')
    56             key = match.group('key')
    57 
    57 
    58             # type
    58             # type
    59             type = match.group("type")
    59             type = match.group("type")
    60             
    60             
    61             # lookup type, None for default
    61             # lookup type, None -> default
    62             type = types[type]
    62             type = config.get_type(type)
    63 
    63 
    64             # defaults?
    64             # defaults?
    65             default = defaults.get(key)
    65             default = defaults.get(key)
    66 
    66 
    67             if not default :
    67             if not default :
    68                 default = match.group('default')
    68                 default = match.group('default')
    69 
    69 
    70                 if default :
    70                 if default :
    71                     # apply type to default
    71                     # apply type to default
    72                     default = type(default)
    72                     default = type.parse(default)
    73 
    73 
    74             # build
    74             # build
    75             return SimpleValueLabel(key, type, default)
    75             return SimpleValueLabel(key, type, default)
    76         
    76         
    77         # static?
    77         # static?
   213         A label that has a name and a simple string value
   213         A label that has a name and a simple string value
   214     """
   214     """
   215 
   215 
   216     EXPR = re.compile(r'^\{(?P<key>[a-zA-Z_][a-zA-Z0-9_]*)(:(?P<type>[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P<default>[^}]+))?\}$')
   216     EXPR = re.compile(r'^\{(?P<key>[a-zA-Z_][a-zA-Z0-9_]*)(:(?P<type>[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P<default>[^}]+))?\}$')
   217 
   217 
   218     def __init__ (self, key, type=str, default=None) :
   218     def __init__ (self, key, type, default) :
   219         """
   219         """
   220             The given key is the name of this label's value
   220             The given key is the name of this label's value
   221         """
   221         """
   222 
   222 
   223         # type
   223         # type
   236         if value is None and self.default :
   236         if value is None and self.default :
   237             return LabelValue(self, self.default)
   237             return LabelValue(self, self.default)
   238         
   238         
   239         # only non-empty values!
   239         # only non-empty values!
   240         elif value :
   240         elif value :
       
   241             # test
       
   242             if not self.type.test(value) :
       
   243                 return False
       
   244 
   241             # convert with type
   245             # convert with type
   242             try :
   246             value = self.type.parse(value)
   243                 value = self.type(value)
       
   244 
       
   245             except Exception, e :
       
   246                 raise URLError("Bad value %r for type %s: %s: %s" % (value, self.type.__name__, type(e).__name__, e))
       
   247 
   247 
   248             return LabelValue(self, value)
   248             return LabelValue(self, value)
   249 
   249 
   250     def __str__ (self) :
   250     def __str__ (self) :
   251         return '{%s%s%s}' % (
   251         return '{%s%s%s}' % (
   252             self.key, 
   252             self.key, 
   253             ':%s' % (self.type.__name__ ) if self.type != str else '',
   253             ':%s' % (self.type, ),  # XXX: omit if default
   254             '=%s' % (self.default, ) if self.default else '',
   254             '=%s' % (self.default, ) if self.default else '',
   255         )
   255         )
   256 
   256 
       
   257 class URLType (object) :
       
   258     """
       
   259         Handles the type-ness of values in the URL
       
   260     """
       
   261 
       
   262     def _init_name (self, name) :
       
   263         """
       
   264             Initialize our .name attribute, called by URLConfig
       
   265         """
       
   266 
       
   267         self.name = name
       
   268 
       
   269     def test (self, value) :
       
   270         """
       
   271             Tests if the given value is valid for this type.
       
   272 
       
   273             Defaults to calling parse(), and returning False on errors, True otherwise
       
   274         """
       
   275         
       
   276         try :
       
   277             self.parse(value)
       
   278 
       
   279         except :
       
   280             return False
       
   281 
       
   282         else :
       
   283             return True
       
   284     
       
   285     def parse (self, value) :
       
   286         """
       
   287             Parse the given value, which was tested earlier with test(), and return the value object
       
   288         """
       
   289 
       
   290         abstract
       
   291     
       
   292    
       
   293     def build (self, obj) :
       
   294         """
       
   295             Reverse of parse(), return an url-value built from the given object
       
   296         """
       
   297 
       
   298         abstract
       
   299 
       
   300     def __str__ (self) :
       
   301         """
       
   302             Return a short string giving the name of this type, defaults to self.name
       
   303         """
       
   304 
       
   305         return self.name
       
   306  
       
   307 class URLStringType (URLType) :
       
   308     """
       
   309         The default URLType, just plain strings.
       
   310 
       
   311         Note that this does not accept empty strings as valid
       
   312     """
       
   313 
       
   314     def __init__ (self) :
       
   315         super(URLStringType, self).__init__('str')
       
   316 
       
   317     def parse (self, value) :
       
   318         """
       
   319             Identitiy
       
   320         """
       
   321 
       
   322         return value
       
   323 
       
   324     def build (self, obj) :
       
   325         if not obj :
       
   326             raise ValueError("String must not be empty")
       
   327 
       
   328         return str(obj)
       
   329 
       
   330 class URLIntegerType (URLType) :
       
   331     """
       
   332         A URLType for simple integers
       
   333     """
       
   334 
       
   335     def __init__ (self, negative=True, zero=True, max=None) :
       
   336         """
       
   337             Pass in negative=False to disallow negative numbers, zero=False to disallow zero, or non-zero max
       
   338             to specifiy maximum value
       
   339         """
       
   340 
       
   341         super(URLIntegerType, self).__init__('int')
       
   342 
       
   343         self.negative = negative
       
   344         self.zero = zero
       
   345         self.max = max
       
   346     
       
   347     def _validate (self, value) :
       
   348         """
       
   349             Test to make sure value fits our criteria
       
   350         """
       
   351 
       
   352         # negative?
       
   353         if self.negative and value < 0 :
       
   354             raise ValueError("value is negative")
       
   355         
       
   356         # zero?
       
   357         if self.zero and value == 0 :
       
   358             raise ValueError("value is zero")
       
   359         
       
   360         # max?
       
   361         if self.max is not None and value > max :
       
   362             raise ValueError("value is too large: %d" % value)
       
   363         
       
   364         return value
       
   365 
       
   366     def parse (self, value) :
       
   367         """
       
   368             Convert str -> int
       
   369         """
       
   370 
       
   371         return self._validate(int(value))
       
   372     
       
   373     def build (self, obj) :
       
   374         """
       
   375             Convert int -> str
       
   376         """
       
   377 
       
   378         return unicode(self._validate(obj))
       
   379     
   257 class URLConfig (object) :
   380 class URLConfig (object) :
   258     """
   381     """
   259         Global configuration relevant to all URLs
   382         Global configuration relevant to all URLs. This can be used to construct a set of URLs and then create an
       
   383         URLTree out of them. Simply call the url_config() instace with the normal URL arguments (except, of course,
       
   384         config), and finally just pass the url_config to URLTree (it's iterable).
   260     """
   385     """
   261 
   386 
   262     # built-in type codes
   387     # built-in type codes
   263     BUILTIN_TYPES = {
   388     BUILTIN_TYPES = {
   264         # default
       
   265         None    : str,
       
   266 
       
   267         # string
   389         # string
   268         'str'   : str,
   390         'str'   : URLStringType(),
   269 
   391 
   270         # integer
   392         # integer
   271         'int'   : int,
   393         'int'   : URLIntegerType(),
   272     }
   394     }
   273 
   395 
   274     def __init__ (self, type_dict=None) :
   396     # init names
       
   397     for name, type in BUILTIN_TYPES.iteritems() :
       
   398         type._init_name(name)
       
   399 
       
   400     def __init__ (self, type_dict=None, default_type='str') :
   275         """
   401         """
   276             Create an URLConfig for use with URL
   402             Create an URLConfig for use with URL
   277 
   403 
   278             If type_dict is given, it should be a mapping of type names -> callables, and they will be available for
   404             If type_dict is given, it should be a dict of { type_names: URLType }, and they will be available for
   279             type specifications in addition to the defaults.
   405             type specifications in addition to the defaults.
   280         """
   406         """
   281 
   407 
   282         # build our type_dict
   408         # build our type_dict
   283         self.type_dict = self.BUILTIN_TYPES.copy()
   409         self.type_dict = self.BUILTIN_TYPES.copy()
   284         
   410         
   285         # apply the given type_dict
   411         # apply the given type_dict
   286         if type_dict :
   412         if type_dict :
       
   413             # initialize names
       
   414             for name, type in type_dict.iteritems() :
       
   415                 type._init_name(name)
       
   416             
       
   417             # merge
   287             self.type_dict.update(type_dict)
   418             self.type_dict.update(type_dict)
   288 
   419 
       
   420         # init
       
   421         self.default_type = default_type
       
   422         self.urls = []
       
   423         
       
   424     def get_type (self, type_name=None) :
       
   425         """
       
   426             Lookup an URLType by type_name, None for default
       
   427         """
       
   428         
       
   429         # default type?
       
   430         if not type_name :
       
   431             type_name = self.default_type
       
   432         
       
   433         # lookup + return
       
   434         return self.type_dict[type_name]
       
   435 
       
   436     def __call__ (self, *args, **kwargs) :
       
   437         """
       
   438             Return new URL object with this config and the given args, adding it to our list of urls
       
   439         """
       
   440         
       
   441         # build
       
   442         url = URL(self, *args, **kwargs)
       
   443         
       
   444         # store
       
   445         self.urls.append(url)
       
   446 
       
   447         # return
       
   448         return url
       
   449     
       
   450     def __iter__ (self) :
       
   451         """
       
   452             Returns all defined URLs
       
   453         """
       
   454 
       
   455         return iter(self.urls)
       
   456 
   289 class URL (object) :
   457 class URL (object) :
   290     """
   458     """
   291         Represents a specific URL
   459         Represents a specific URL
   292     """
   460     """
   293 
   461 
   294 
   462 
   295     def __init__ (self, config, url_mask, handler, type_dict=None, **defaults) :
   463     def __init__ (self, config, url_mask, handler, **defaults) :
   296         """
   464         """
   297             Create an URL using the given URLConfig, with the given url mask, handler, and default values.
   465             Create an URL using the given URLConfig, with the given url mask, handler, and default values.
   298         """
   466         """
   299 
   467 
   300         # store
   468         # store
   313         
   481         
   314         else :
   482         else :
   315             query_mask = None
   483             query_mask = None
   316 
   484 
   317         # build our label path
   485         # build our label path
   318         self.label_path = [Label.parse(mask, defaults, config.type_dict) for mask in url_mask.split('/')]
   486         self.label_path = [Label.parse(mask, defaults, config) for mask in url_mask.split('/')]
   319 
   487 
   320         # build our query args list
   488         # build our query args list
   321         if query_mask :
   489         if query_mask :
   322             # split into items
   490             # split into items
   323             for query_item in query_mask.split('&') :
   491             for query_item in query_mask.split('&') :
   336                 
   504                 
   337                 # parse key
   505                 # parse key
   338                 key = query_item
   506                 key = query_item
   339 
   507 
   340                 # type
   508                 # type
   341                 type = self.config.type_dict[type]
   509                 type = self.config.get_type(type)
   342 
   510 
   343                 # add to query_args as (type, default) tuple
   511                 # add to query_args as (type, default) tuple
   344                 self.query_args[key] = (type, type(default) if default else default)
   512                 self.query_args[key] = (type, type.parse(default) if default else default)
   345          
   513          
   346     def get_label_path (self) :
   514     def get_label_path (self) :
   347         """
   515         """
   348             Returns a list containing the labels in this url
   516             Returns a list containing the labels in this url
   349         """
   517         """
   371             # normalize empty value to None
   539             # normalize empty value to None
   372             if not value :
   540             if not value :
   373                 value = None
   541                 value = None
   374 
   542 
   375             else :
   543             else :
   376                 # process value
   544                 # parse value
   377                 value = type(value)
   545                 value = type.parse(value)
   378 
   546 
   379             # set default?
   547             # set default?
   380             if not value :
   548             if not value :
   381                 if default :
   549                 if default :
   382                     value = default
   550                     value = default
   637     def match (self, url) :
   805     def match (self, url) :
   638         """
   806         """
   639             Find the URL object best corresponding to the given url, matching any ValueLabels.
   807             Find the URL object best corresponding to the given url, matching any ValueLabels.
   640 
   808 
   641             Returns an (URL, [LabelValue]) tuple.
   809             Returns an (URL, [LabelValue]) tuple.
       
   810 
       
   811             XXX: handle unicode on URLs
   642         """
   812         """
   643 
   813 
   644         # split it into labels
   814         # split it into labels
   645         path = url.split('/')
   815         path = url.split('/')
   646         
   816