1""" 2 3uritemplate.variable 4==================== 5 6This module contains the URIVariable class which powers the URITemplate class. 7 8What treasures await you: 9 10- URIVariable class 11 12You see a hammer in front of you. 13What do you do? 14> 15 16""" 17 18import collections 19import sys 20 21if (2, 6) <= sys.version_info < (2, 8): 22 import urllib 23elif (3, 3) <= sys.version_info < (4, 0): 24 import urllib.parse as urllib 25 26 27class URIVariable(object): 28 29 """This object validates everything inside the URITemplate object. 30 31 It validates template expansions and will truncate length as decided by 32 the template. 33 34 Please note that just like the :class:`URITemplate <URITemplate>`, this 35 object's ``__str__`` and ``__repr__`` methods do not return the same 36 information. Calling ``str(var)`` will return the original variable. 37 38 This object does the majority of the heavy lifting. The ``URITemplate`` 39 object finds the variables in the URI and then creates ``URIVariable`` 40 objects. Expansions of the URI are handled by each ``URIVariable`` 41 object. ``URIVariable.expand()`` returns a dictionary of the original 42 variable and the expanded value. Check that method's documentation for 43 more information. 44 45 """ 46 47 operators = ('+', '#', '.', '/', ';', '?', '&', '|', '!', '@') 48 reserved = ":/?#[]@!$&'()*+,;=" 49 50 def __init__(self, var): 51 #: The original string that comes through with the variable 52 self.original = var 53 #: The operator for the variable 54 self.operator = '' 55 #: List of safe characters when quoting the string 56 self.safe = '' 57 #: List of variables in this variable 58 self.variables = [] 59 #: List of variable names 60 self.variable_names = [] 61 #: List of defaults passed in 62 self.defaults = {} 63 # Parse the variable itself. 64 self.parse() 65 self.post_parse() 66 67 def __repr__(self): 68 return 'URIVariable(%s)' % self 69 70 def __str__(self): 71 return self.original 72 73 def parse(self): 74 """Parse the variable. 75 76 This finds the: 77 - operator, 78 - set of safe characters, 79 - variables, and 80 - defaults. 81 82 """ 83 var_list = self.original 84 if self.original[0] in URIVariable.operators: 85 self.operator = self.original[0] 86 var_list = self.original[1:] 87 88 if self.operator in URIVariable.operators[:2]: 89 self.safe = URIVariable.reserved 90 91 var_list = var_list.split(',') 92 93 for var in var_list: 94 default_val = None 95 name = var 96 if '=' in var: 97 name, default_val = tuple(var.split('=', 1)) 98 99 explode = False 100 if name.endswith('*'): 101 explode = True 102 name = name[:-1] 103 104 prefix = None 105 if ':' in name: 106 name, prefix = tuple(name.split(':', 1)) 107 prefix = int(prefix) 108 109 if default_val: 110 self.defaults[name] = default_val 111 112 self.variables.append( 113 (name, {'explode': explode, 'prefix': prefix}) 114 ) 115 116 self.variable_names = [varname for (varname, _) in self.variables] 117 118 def post_parse(self): 119 """Set ``start``, ``join_str`` and ``safe`` attributes. 120 121 After parsing the variable, we need to set up these attributes and it 122 only makes sense to do it in a more easily testable way. 123 """ 124 self.safe = '' 125 self.start = self.join_str = self.operator 126 if self.operator == '+': 127 self.start = '' 128 if self.operator in ('+', '#', ''): 129 self.join_str = ',' 130 if self.operator == '#': 131 self.start = '#' 132 if self.operator == '?': 133 self.start = '?' 134 self.join_str = '&' 135 136 if self.operator in ('+', '#'): 137 self.safe = URIVariable.reserved 138 139 def _query_expansion(self, name, value, explode, prefix): 140 """Expansion method for the '?' and '&' operators.""" 141 if value is None: 142 return None 143 144 tuples, items = is_list_of_tuples(value) 145 146 safe = self.safe 147 if list_test(value) and not tuples: 148 if not value: 149 return None 150 if explode: 151 return self.join_str.join( 152 '%s=%s' % (name, quote(v, safe)) for v in value 153 ) 154 else: 155 value = ','.join(quote(v, safe) for v in value) 156 return '%s=%s' % (name, value) 157 158 if dict_test(value) or tuples: 159 if not value: 160 return None 161 items = items or sorted(value.items()) 162 if explode: 163 return self.join_str.join( 164 '%s=%s' % ( 165 quote(k, safe), quote(v, safe) 166 ) for k, v in items 167 ) 168 else: 169 value = ','.join( 170 '%s,%s' % ( 171 quote(k, safe), quote(v, safe) 172 ) for k, v in items 173 ) 174 return '%s=%s' % (name, value) 175 176 if value: 177 value = value[:prefix] if prefix else value 178 return '%s=%s' % (name, quote(value, safe)) 179 return name + '=' 180 181 def _label_path_expansion(self, name, value, explode, prefix): 182 """Label and path expansion method. 183 184 Expands for operators: '/', '.' 185 186 """ 187 join_str = self.join_str 188 safe = self.safe 189 190 if value is None or (len(value) == 0 and value != ''): 191 return None 192 193 tuples, items = is_list_of_tuples(value) 194 195 if list_test(value) and not tuples: 196 if not explode: 197 join_str = ',' 198 199 expanded = join_str.join( 200 quote(v, safe) for v in value if value is not None 201 ) 202 return expanded if expanded else None 203 204 if dict_test(value) or tuples: 205 items = items or sorted(value.items()) 206 format_str = '%s=%s' 207 if not explode: 208 format_str = '%s,%s' 209 join_str = ',' 210 211 expanded = join_str.join( 212 format_str % ( 213 quote(k, safe), quote(v, safe) 214 ) for k, v in items if v is not None 215 ) 216 return expanded if expanded else None 217 218 value = value[:prefix] if prefix else value 219 return quote(value, safe) 220 221 def _semi_path_expansion(self, name, value, explode, prefix): 222 """Expansion method for ';' operator.""" 223 join_str = self.join_str 224 safe = self.safe 225 226 if value is None: 227 return None 228 229 if self.operator == '?': 230 join_str = '&' 231 232 tuples, items = is_list_of_tuples(value) 233 234 if list_test(value) and not tuples: 235 if explode: 236 expanded = join_str.join( 237 '%s=%s' % ( 238 name, quote(v, safe) 239 ) for v in value if v is not None 240 ) 241 return expanded if expanded else None 242 else: 243 value = ','.join(quote(v, safe) for v in value) 244 return '%s=%s' % (name, value) 245 246 if dict_test(value) or tuples: 247 items = items or sorted(value.items()) 248 249 if explode: 250 return join_str.join( 251 '%s=%s' % ( 252 quote(k, safe), quote(v, safe) 253 ) for k, v in items if v is not None 254 ) 255 else: 256 expanded = ','.join( 257 '%s,%s' % ( 258 quote(k, safe), quote(v, safe) 259 ) for k, v in items if v is not None 260 ) 261 return '%s=%s' % (name, expanded) 262 263 value = value[:prefix] if prefix else value 264 if value: 265 return '%s=%s' % (name, quote(value, safe)) 266 267 return name 268 269 def _string_expansion(self, name, value, explode, prefix): 270 if value is None: 271 return None 272 273 tuples, items = is_list_of_tuples(value) 274 275 if list_test(value) and not tuples: 276 return ','.join(quote(v, self.safe) for v in value) 277 278 if dict_test(value) or tuples: 279 items = items or sorted(value.items()) 280 format_str = '%s=%s' if explode else '%s,%s' 281 282 return ','.join( 283 format_str % ( 284 quote(k, self.safe), quote(v, self.safe) 285 ) for k, v in items 286 ) 287 288 value = value[:prefix] if prefix else value 289 return quote(value, self.safe) 290 291 def expand(self, var_dict=None): 292 """Expand the variable in question. 293 294 Using ``var_dict`` and the previously parsed defaults, expand this 295 variable and subvariables. 296 297 :param dict var_dict: dictionary of key-value pairs to be used during 298 expansion 299 :returns: dict(variable=value) 300 301 Examples:: 302 303 # (1) 304 v = URIVariable('/var') 305 expansion = v.expand({'var': 'value'}) 306 print(expansion) 307 # => {'/var': '/value'} 308 309 # (2) 310 v = URIVariable('?var,hello,x,y') 311 expansion = v.expand({'var': 'value', 'hello': 'Hello World!', 312 'x': '1024', 'y': '768'}) 313 print(expansion) 314 # => {'?var,hello,x,y': 315 # '?var=value&hello=Hello%20World%21&x=1024&y=768'} 316 317 """ 318 return_values = [] 319 320 for name, opts in self.variables: 321 value = var_dict.get(name, None) 322 if not value and value != '' and name in self.defaults: 323 value = self.defaults[name] 324 325 if value is None: 326 continue 327 328 expanded = None 329 if self.operator in ('/', '.'): 330 expansion = self._label_path_expansion 331 elif self.operator in ('?', '&'): 332 expansion = self._query_expansion 333 elif self.operator == ';': 334 expansion = self._semi_path_expansion 335 else: 336 expansion = self._string_expansion 337 338 expanded = expansion(name, value, opts['explode'], opts['prefix']) 339 340 if expanded is not None: 341 return_values.append(expanded) 342 343 value = '' 344 if return_values: 345 value = self.start + self.join_str.join(return_values) 346 return {self.original: value} 347 348 349def is_list_of_tuples(value): 350 if (not value or 351 not isinstance(value, (list, tuple)) or 352 not all(isinstance(t, tuple) and len(t) == 2 for t in value)): 353 return False, None 354 355 return True, value 356 357 358def list_test(value): 359 return isinstance(value, (list, tuple)) 360 361 362def dict_test(value): 363 return isinstance(value, (dict, collections.MutableMapping)) 364 365 366try: 367 texttype = unicode 368except NameError: # Python 3 369 texttype = str 370 371stringlikes = (texttype, bytes) 372 373 374def _encode(value, encoding='utf-8'): 375 if (isinstance(value, texttype) and 376 getattr(value, 'encode', None) is not None): 377 return value.encode(encoding) 378 return value 379 380 381def quote(value, safe): 382 if not isinstance(value, stringlikes): 383 value = str(value) 384 return urllib.quote(_encode(value), safe) 385