Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/uritemplate/variable.py: 73%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

231 statements  

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.abc 

19import enum 

20import string 

21import typing as t 

22import urllib.parse 

23 

24ScalarVariableValue = t.Union[int, float, complex, str, None] 

25VariableValue = t.Union[ 

26 t.Sequence[ScalarVariableValue], 

27 t.List[ScalarVariableValue], 

28 t.Mapping[str, ScalarVariableValue], 

29 t.Tuple[str, ScalarVariableValue], 

30 ScalarVariableValue, 

31] 

32VariableValueDict = t.Dict[str, VariableValue] 

33 

34 

35_UNRESERVED_CHARACTERS: t.Final[str] = ( 

36 f"{string.ascii_letters}{string.digits}~-_." 

37) 

38_GEN_DELIMS: t.Final[str] = ":/?#[]@" 

39_SUB_DELIMS: t.Final[str] = "!$&'()*+,;=" 

40_RESERVED_CHARACTERS: t.Final[str] = f"{_GEN_DELIMS}{_SUB_DELIMS}" 

41 

42 

43class Operator(enum.Enum): 

44 # Section 2.2. Expressions 

45 # expression = "{" [ operator ] variable-list "}" 

46 # operator = op-level2 / op-level3 / op-reserve 

47 # op-level2 = "+" / "#" 

48 # op-level3 = "." / "/" / ";" / "?" / "&" 

49 # op-reserve = "=" / "," / "!" / "@" / "|" 

50 default = "" # 3.2.2. Simple String Expansiona: {var} 

51 # Operator Level 2 (op-level2) 

52 reserved = "+" # 3.2.3. Reserved Expansion: {+var} 

53 fragment = "#" # 3.2.4. Fragment Expansion: {#var} 

54 # Operator Level 3 (op-level3) 

55 # 3.2.5. Label Expansion with Dot-Prefix: {.var} 

56 label_with_dot_prefix = "." 

57 path_segment = "/" # 3.2.6. Path Segment Expansion: {/var} 

58 path_style_parameter = ( 

59 ";" # 3.2.7. Path-Style Parameter Expansion: {;var} 

60 ) 

61 form_style_query = "?" # 3.2.8. Form-Style Query Expansion: {?var} 

62 # 3.2.9. Form-Style Query Continuation: {&var} 

63 form_style_query_continuation = "&" 

64 # Reserved Operators (op-reserve) 

65 reserved_eq = "=" 

66 reserved_comma = "," 

67 reserved_bang = "!" 

68 reserved_at = "@" 

69 reserved_pipe = "|" 

70 

71 def reserved_characters(self) -> str: 

72 # TODO: Re-enable after un-commenting 3.9 

73 # match self: 

74 # case Operator.reserved: 

75 # return _RESERVED_CHARACTERS + "%" 

76 # # case Operator.default | Operator.reserved | Operator.fragment: 

77 # case Operator.fragment: 

78 # return _RESERVED_CHARACTERS 

79 # case _: 

80 # return "" 

81 if self == Operator.reserved: 

82 return _RESERVED_CHARACTERS + "%" 

83 if self == Operator.fragment: 

84 return _RESERVED_CHARACTERS 

85 return "" 

86 

87 def expansion_separator(self) -> str: 

88 """Identify the separator used during expansion. 

89 

90 Per `Section 3.2.1. Variable Expansion`_: 

91 

92 ====== =========== ========= 

93 Type Separator 

94 ====== =========== ========= 

95 ``","`` (default) 

96 ``+`` ``","`` 

97 ``#`` ``","`` 

98 ``.`` ``"."`` 

99 ``/`` ``"/"`` 

100 ``;`` ``";"`` 

101 ``?`` ``"&"`` 

102 ``&`` ``"&"`` 

103 ====== =========== ========= 

104 

105 .. _`Section 3.2.1. Variable Expansion`: 

106 https://www.rfc-editor.org/rfc/rfc6570#section-3.2.1 

107 """ 

108 if self == Operator.label_with_dot_prefix: 

109 return "." 

110 if self == Operator.path_segment: 

111 return "/" 

112 if self == Operator.path_style_parameter: 

113 return ";" 

114 if ( 

115 self == Operator.form_style_query 

116 or self == Operator.form_style_query_continuation 

117 ): 

118 return "&" 

119 # if self == Operator.reserved or self == Operator.fragment: 

120 # return "," 

121 return "," 

122 # match self: 

123 # case Operator.label_with_dot_prefix: 

124 # return "." 

125 # case Operator.path_segment: 

126 # return "/" 

127 # case Operator.path_style_parameter: 

128 # return ";" 

129 # case ( 

130 # Operator.form_style_query | 

131 # Operator.form_style_query_continuation 

132 # ): 

133 # return "&" 

134 # case Operator.reserved | Operator.fragment: 

135 # return "," 

136 # case _: 

137 # return "," 

138 

139 def variable_prefix(self) -> str: 

140 if self == Operator.reserved: 

141 return "" 

142 return t.cast(str, self.value) 

143 # match self: 

144 # case Operator.reserved: 

145 # return "" 

146 # case _: 

147 # return t.cast(str, self.value) 

148 

149 def _always_quote(self, value: str) -> str: 

150 return quote(value, "") 

151 

152 def _only_quote_unquoted_characters(self, value: str) -> str: 

153 if urllib.parse.unquote(value) == value: 

154 return quote(value, _RESERVED_CHARACTERS) 

155 return value 

156 

157 def quote(self, value: t.Any) -> str: 

158 if not isinstance(value, (str, bytes)): 

159 value = str(value) 

160 if isinstance(value, bytes): 

161 value = value.decode() 

162 

163 if self == Operator.reserved or self == Operator.fragment: 

164 return self._only_quote_unquoted_characters(value) 

165 return self._always_quote(value) 

166 

167 @staticmethod 

168 def from_string(s: str) -> "Operator": 

169 return _operators.get(s, Operator.default) 

170 

171 

172_operators: t.Final[t.Dict[str, Operator]] = { 

173 "+": Operator.reserved, 

174 "#": Operator.fragment, 

175 ".": Operator.label_with_dot_prefix, 

176 "/": Operator.path_segment, 

177 ";": Operator.path_style_parameter, 

178 "?": Operator.form_style_query, 

179 "&": Operator.form_style_query_continuation, 

180 "!": Operator.reserved_bang, 

181 "|": Operator.reserved_pipe, 

182 "@": Operator.reserved_at, 

183 "=": Operator.reserved_eq, 

184 ",": Operator.reserved_comma, 

185} 

186 

187 

188class URIVariable: 

189 """This object validates everything inside the URITemplate object. 

190 

191 It validates template expansions and will truncate length as decided by 

192 the template. 

193 

194 Please note that just like the :class:`URITemplate <URITemplate>`, this 

195 object's ``__str__`` and ``__repr__`` methods do not return the same 

196 information. Calling ``str(var)`` will return the original variable. 

197 

198 This object does the majority of the heavy lifting. The ``URITemplate`` 

199 object finds the variables in the URI and then creates ``URIVariable`` 

200 objects. Expansions of the URI are handled by each ``URIVariable`` 

201 object. ``URIVariable.expand()`` returns a dictionary of the original 

202 variable and the expanded value. Check that method's documentation for 

203 more information. 

204 

205 """ 

206 

207 def __init__(self, var: str): 

208 #: The original string that comes through with the variable 

209 self.original: str = var 

210 #: The operator for the variable 

211 self.operator: Operator = Operator.default 

212 #: List of variables in this variable 

213 self.variables: t.List[t.Tuple[str, t.MutableMapping[str, t.Any]]] = ( 

214 [] 

215 ) 

216 #: List of variable names 

217 self.variable_names: t.List[str] = [] 

218 #: List of defaults passed in 

219 self.defaults: t.MutableMapping[str, ScalarVariableValue] = {} 

220 # Parse the variable itself. 

221 self.parse() 

222 

223 def __repr__(self) -> str: 

224 return "URIVariable(%s)" % self 

225 

226 def __str__(self) -> str: 

227 return self.original 

228 

229 def parse(self) -> None: 

230 """Parse the variable. 

231 

232 This finds the: 

233 - operator, 

234 - set of safe characters, 

235 - variables, and 

236 - defaults. 

237 

238 """ 

239 var_list_str = self.original 

240 if (operator_str := self.original[0]) in _operators: 

241 self.operator = Operator.from_string(operator_str) 

242 var_list_str = self.original[1:] 

243 

244 var_list = var_list_str.split(",") 

245 

246 for var in var_list: 

247 default_val = None 

248 name = var 

249 # NOTE(sigmavirus24): This is from an earlier draft but is not in 

250 # the specification 

251 if "=" in var: 

252 name, default_val = tuple(var.split("=", 1)) 

253 

254 explode = name.endswith("*") 

255 name = name.rstrip("*") 

256 

257 prefix: t.Optional[int] = None 

258 if ":" in name: 

259 name, prefix_str = tuple(name.split(":", 1)) 

260 prefix = int(prefix_str, 10) 

261 

262 if default_val: 

263 self.defaults[name] = default_val 

264 

265 self.variables.append( 

266 (name, {"explode": explode, "prefix": prefix}) 

267 ) 

268 

269 self.variable_names = [varname for (varname, _) in self.variables] 

270 

271 def _query_expansion( 

272 self, 

273 name: str, 

274 value: VariableValue, 

275 explode: bool, 

276 prefix: t.Optional[int], 

277 ) -> t.Optional[str]: 

278 """Expansion method for the '?' and '&' operators.""" 

279 if value is None: 

280 return None 

281 

282 tuples, items = is_list_of_tuples(value) 

283 

284 safe = self.operator.reserved_characters() 

285 _quote = self.operator.quote 

286 if list_test(value) and not tuples: 

287 if not value: 

288 return None 

289 value = t.cast(t.Sequence[ScalarVariableValue], value) 

290 if explode: 

291 return self.operator.expansion_separator().join( 

292 f"{name}={_quote(v)}" for v in value 

293 ) 

294 else: 

295 value = ",".join(_quote(v) for v in value) 

296 return f"{name}={value}" 

297 

298 if dict_test(value) or tuples: 

299 if not value: 

300 return None 

301 value = t.cast(t.Mapping[str, ScalarVariableValue], value) 

302 items = items or sorted(value.items()) 

303 if explode: 

304 return self.operator.expansion_separator().join( 

305 f"{quote(k, safe)}={_quote(v)}" for k, v in items 

306 ) 

307 else: 

308 value = ",".join( 

309 f"{quote(k, safe)},{_quote(v)}" for k, v in items 

310 ) 

311 return f"{name}={value}" 

312 

313 if value: 

314 value = t.cast(t.Text, value) 

315 value = value[:prefix] if prefix else value 

316 return f"{name}={_quote(value)}" 

317 return name + "=" 

318 

319 def _label_path_expansion( 

320 self, 

321 name: str, 

322 value: VariableValue, 

323 explode: bool, 

324 prefix: t.Optional[int], 

325 ) -> t.Optional[str]: 

326 """Label and path expansion method. 

327 

328 Expands for operators: '/', '.' 

329 

330 """ 

331 join_str = self.operator.expansion_separator() 

332 safe = self.operator.reserved_characters() 

333 

334 if value is None or ( 

335 not isinstance(value, (str, int, float, complex)) 

336 and len(value) == 0 

337 ): 

338 return None 

339 

340 tuples, items = is_list_of_tuples(value) 

341 

342 if list_test(value) and not tuples: 

343 if not explode: 

344 join_str = "," 

345 

346 value = t.cast(t.Sequence[ScalarVariableValue], value) 

347 fragments = [ 

348 self.operator.quote(v) for v in value if v is not None 

349 ] 

350 return join_str.join(fragments) if fragments else None 

351 

352 if dict_test(value) or tuples: 

353 value = t.cast(t.Mapping[str, ScalarVariableValue], value) 

354 items = items or sorted(value.items()) 

355 format_str = "%s=%s" 

356 if not explode: 

357 format_str = "%s,%s" 

358 join_str = "," 

359 

360 expanded = join_str.join( 

361 format_str % (quote(k, safe), self.operator.quote(v)) 

362 for k, v in items 

363 if v is not None 

364 ) 

365 return expanded if expanded else None 

366 

367 value = t.cast(t.Text, value) 

368 value = value[:prefix] if prefix else value 

369 return self.operator.quote(value) 

370 

371 def _semi_path_expansion( 

372 self, 

373 name: str, 

374 value: VariableValue, 

375 explode: bool, 

376 prefix: t.Optional[int], 

377 ) -> t.Optional[str]: 

378 """Expansion method for ';' operator.""" 

379 join_str = self.operator.expansion_separator() 

380 safe = self.operator.reserved_characters() 

381 

382 if value is None: 

383 return None 

384 

385 tuples, items = is_list_of_tuples(value) 

386 

387 if list_test(value) and not tuples: 

388 value = t.cast(t.Sequence[ScalarVariableValue], value) 

389 if explode: 

390 expanded = join_str.join( 

391 f"{name}={quote(v, safe)}" for v in value if v is not None 

392 ) 

393 return expanded if expanded else None 

394 else: 

395 value = ",".join(quote(v, safe) for v in value) 

396 return f"{name}={value}" 

397 

398 if dict_test(value) or tuples: 

399 value = t.cast(t.Mapping[str, ScalarVariableValue], value) 

400 items = items or sorted(value.items()) 

401 

402 if explode: 

403 return join_str.join( 

404 f"{quote(k, safe)}={self.operator.quote(v)}" 

405 for k, v in items 

406 if v is not None 

407 ) 

408 else: 

409 expanded = ",".join( 

410 f"{quote(k, safe)},{self.operator.quote(v)}" 

411 for k, v in items 

412 if v is not None 

413 ) 

414 return f"{name}={expanded}" 

415 

416 value = t.cast(t.Text, value) 

417 value = value[:prefix] if prefix else value 

418 if value: 

419 return f"{name}={self.operator.quote(value)}" 

420 

421 return name 

422 

423 def _string_expansion( 

424 self, 

425 name: str, 

426 value: VariableValue, 

427 explode: bool, 

428 prefix: t.Optional[int], 

429 ) -> t.Optional[str]: 

430 if value is None: 

431 return None 

432 

433 tuples, items = is_list_of_tuples(value) 

434 

435 if list_test(value) and not tuples: 

436 value = t.cast(t.Sequence[ScalarVariableValue], value) 

437 return ",".join(self.operator.quote(v) for v in value) 

438 

439 if dict_test(value) or tuples: 

440 value = t.cast(t.Mapping[str, ScalarVariableValue], value) 

441 items = items or sorted(value.items()) 

442 format_str = "%s=%s" if explode else "%s,%s" 

443 

444 return ",".join( 

445 format_str % (self.operator.quote(k), self.operator.quote(v)) 

446 for k, v in items 

447 ) 

448 

449 value = t.cast(t.Text, value) 

450 value = value[:prefix] if prefix else value 

451 return self.operator.quote(value) 

452 

453 def expand( 

454 self, var_dict: t.Optional[VariableValueDict] = None 

455 ) -> t.Mapping[str, str]: 

456 """Expand the variable in question. 

457 

458 Using ``var_dict`` and the previously parsed defaults, expand this 

459 variable and subvariables. 

460 

461 :param dict var_dict: dictionary of key-value pairs to be used during 

462 expansion 

463 :returns: dict(variable=value) 

464 

465 Examples:: 

466 

467 # (1) 

468 v = URIVariable('/var') 

469 expansion = v.expand({'var': 'value'}) 

470 print(expansion) 

471 # => {'/var': '/value'} 

472 

473 # (2) 

474 v = URIVariable('?var,hello,x,y') 

475 expansion = v.expand({'var': 'value', 'hello': 'Hello World!', 

476 'x': '1024', 'y': '768'}) 

477 print(expansion) 

478 # => {'?var,hello,x,y': 

479 # '?var=value&hello=Hello%20World%21&x=1024&y=768'} 

480 

481 """ 

482 return_values = [] 

483 if var_dict is None: 

484 return {self.original: self.original} 

485 

486 for name, opts in self.variables: 

487 value = var_dict.get(name, None) 

488 if not value and value != "" and name in self.defaults: 

489 value = self.defaults[name] 

490 

491 if value is None: 

492 continue 

493 

494 expanded = None 

495 if ( 

496 self.operator == Operator.path_segment 

497 or self.operator == Operator.label_with_dot_prefix 

498 ): 

499 expansion = self._label_path_expansion 

500 elif ( 

501 self.operator == Operator.form_style_query 

502 or self.operator == Operator.form_style_query_continuation 

503 ): 

504 expansion = self._query_expansion 

505 elif self.operator == Operator.path_style_parameter: 

506 expansion = self._semi_path_expansion 

507 else: 

508 expansion = self._string_expansion 

509 # match self.operator: 

510 # case Operator.path_segment | Operator.label_with_dot_prefix: 

511 # expansion = self._label_path_expansion 

512 # case (Operator.form_style_query | 

513 # Operator.form_style_query_continuation): 

514 # expansion = self._query_expansion 

515 # case Operator.path_style_parameter: 

516 # expansion = self._semi_path_expansion 

517 # case _: 

518 # expansion = self._string_expansion 

519 

520 expanded = expansion(name, value, opts["explode"], opts["prefix"]) 

521 

522 if expanded is not None: 

523 return_values.append(expanded) 

524 

525 value = "" 

526 if return_values: 

527 value = ( 

528 self.operator.variable_prefix() 

529 + self.operator.expansion_separator().join(return_values) 

530 ) 

531 return {self.original: value} 

532 

533 

534def is_list_of_tuples( 

535 value: t.Any, 

536) -> t.Tuple[bool, t.Optional[t.Sequence[t.Tuple[str, ScalarVariableValue]]]]: 

537 if ( 

538 not value 

539 or not isinstance(value, (list, tuple)) 

540 or not all(isinstance(t, tuple) and len(t) == 2 for t in value) 

541 ): 

542 return False, None 

543 

544 return True, value 

545 

546 

547def list_test(value: t.Any) -> bool: 

548 return isinstance(value, (list, tuple)) 

549 

550 

551def dict_test(value: t.Any) -> bool: 

552 return isinstance(value, (dict, collections.abc.MutableMapping)) 

553 

554 

555def _encode(value: t.AnyStr, encoding: str = "utf-8") -> bytes: 

556 if isinstance(value, str): 

557 return value.encode(encoding) 

558 return value 

559 

560 

561def quote(value: t.Any, safe: str) -> str: 

562 if not isinstance(value, (str, bytes)): 

563 value = str(value) 

564 return urllib.parse.quote(_encode(value), safe)