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