Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rfc3986/parseresult.py: 54%

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

200 statements  

1# Copyright (c) 2015 Ian Stapleton Cordasco 

2# Licensed under the Apache License, Version 2.0 (the "License"); 

3# you may not use this file except in compliance with the License. 

4# You may obtain a copy of the License at 

5# 

6# http://www.apache.org/licenses/LICENSE-2.0 

7# 

8# Unless required by applicable law or agreed to in writing, software 

9# distributed under the License is distributed on an "AS IS" BASIS, 

10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 

11# implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14"""Module containing the urlparse compatibility logic.""" 

15 

16import typing as t 

17from collections import namedtuple 

18 

19from . import compat 

20from . import exceptions 

21from . import misc 

22from . import normalizers 

23from . import uri 

24from ._typing_compat import Self as _Self 

25 

26__all__ = ("ParseResult", "ParseResultBytes") 

27 

28PARSED_COMPONENTS = ( 

29 "scheme", 

30 "userinfo", 

31 "host", 

32 "port", 

33 "path", 

34 "query", 

35 "fragment", 

36) 

37 

38 

39class ParseResultMixin(t.Generic[t.AnyStr]): 

40 if t.TYPE_CHECKING: 

41 userinfo: t.Optional[t.AnyStr] 

42 host: t.Optional[t.AnyStr] 

43 port: t.Optional[int] 

44 query: t.Optional[t.AnyStr] 

45 encoding: str 

46 

47 @property 

48 def authority(self) -> t.Optional[t.AnyStr]: ... 

49 

50 def _generate_authority( 

51 self, 

52 attributes: t.Dict[str, t.Optional[t.AnyStr]], 

53 ) -> t.Optional[str]: 

54 # I swear I did not align the comparisons below. That's just how they 

55 # happened to align based on pep8 and attribute lengths. 

56 userinfo, host, port = ( 

57 attributes[p] for p in ("userinfo", "host", "port") 

58 ) 

59 if self.userinfo != userinfo or self.host != host or self.port != port: 

60 if port: 

61 port = f"{port}" 

62 return normalizers.normalize_authority( 

63 ( 

64 compat.to_str(userinfo, self.encoding), 

65 compat.to_str(host, self.encoding), 

66 port, 

67 ) 

68 ) 

69 if isinstance(self.authority, bytes): 

70 return self.authority.decode("utf-8") 

71 return self.authority 

72 

73 def geturl(self) -> t.AnyStr: 

74 """Shim to match the standard library method.""" 

75 return self.unsplit() 

76 

77 @property 

78 def hostname(self) -> t.Optional[t.AnyStr]: 

79 """Shim to match the standard library.""" 

80 return self.host 

81 

82 @property 

83 def netloc(self) -> t.Optional[t.AnyStr]: 

84 """Shim to match the standard library.""" 

85 return self.authority 

86 

87 @property 

88 def params(self) -> t.Optional[t.AnyStr]: 

89 """Shim to match the standard library.""" 

90 return self.query 

91 

92 

93class ParseResult( 

94 namedtuple("ParseResult", PARSED_COMPONENTS), ParseResultMixin[str] 

95): 

96 """Implementation of urlparse compatibility class. 

97 

98 This uses the URIReference logic to handle compatibility with the 

99 urlparse.ParseResult class. 

100 """ 

101 

102 scheme: t.Optional[str] 

103 userinfo: t.Optional[str] 

104 host: t.Optional[str] 

105 port: t.Optional[int] 

106 path: t.Optional[str] 

107 query: t.Optional[str] 

108 fragment: t.Optional[str] 

109 encoding: str 

110 reference: "uri.URIReference" 

111 

112 def __new__( 

113 cls, 

114 scheme: t.Optional[str], 

115 userinfo: t.Optional[str], 

116 host: t.Optional[str], 

117 port: t.Optional[int], 

118 path: t.Optional[str], 

119 query: t.Optional[str], 

120 fragment: t.Optional[str], 

121 uri_ref: "uri.URIReference", 

122 encoding: str = "utf-8", 

123 ) -> _Self: 

124 """Create a new ParseResult.""" 

125 parse_result = super().__new__( 

126 cls, 

127 scheme or None, 

128 userinfo or None, 

129 host, 

130 port or None, 

131 path or None, 

132 query, 

133 fragment, 

134 ) 

135 parse_result.encoding = encoding 

136 parse_result.reference = uri_ref 

137 return parse_result 

138 

139 @classmethod 

140 def from_parts( 

141 cls, 

142 scheme: t.Optional[str] = None, 

143 userinfo: t.Optional[str] = None, 

144 host: t.Optional[str] = None, 

145 port: t.Optional[t.Union[int, str]] = None, 

146 path: t.Optional[str] = None, 

147 query: t.Optional[str] = None, 

148 fragment: t.Optional[str] = None, 

149 encoding: str = "utf-8", 

150 ) -> _Self: 

151 """Create a ParseResult instance from its parts.""" 

152 authority = "" 

153 if userinfo is not None: 

154 authority += userinfo + "@" 

155 if host is not None: 

156 authority += host 

157 if port is not None: 

158 authority += f":{port}" 

159 uri_ref = uri.URIReference( 

160 scheme=scheme, 

161 authority=authority, 

162 path=path, 

163 query=query, 

164 fragment=fragment, 

165 encoding=encoding, 

166 ).normalize() 

167 userinfo, host, port = authority_from(uri_ref, strict=True) 

168 return cls( 

169 scheme=uri_ref.scheme, 

170 userinfo=userinfo, 

171 host=host, 

172 port=port, 

173 path=uri_ref.path, 

174 query=uri_ref.query, 

175 fragment=uri_ref.fragment, 

176 uri_ref=uri_ref, 

177 encoding=encoding, 

178 ) 

179 

180 @classmethod 

181 def from_string( 

182 cls, 

183 uri_string: t.Union[str, bytes], 

184 encoding: str = "utf-8", 

185 strict: bool = True, 

186 lazy_normalize: bool = True, 

187 ) -> _Self: 

188 """Parse a URI from the given unicode URI string. 

189 

190 :param str uri_string: Unicode URI to be parsed into a reference. 

191 :param str encoding: The encoding of the string provided 

192 :param bool strict: Parse strictly according to :rfc:`3986` if True. 

193 If False, parse similarly to the standard library's urlparse 

194 function. 

195 :returns: :class:`ParseResult` or subclass thereof 

196 """ 

197 reference = uri.URIReference.from_string(uri_string, encoding) 

198 if not lazy_normalize: 

199 reference = reference.normalize() 

200 userinfo, host, port = authority_from(reference, strict) 

201 

202 return cls( 

203 scheme=reference.scheme, 

204 userinfo=userinfo, 

205 host=host, 

206 port=port, 

207 path=reference.path, 

208 query=reference.query, 

209 fragment=reference.fragment, 

210 uri_ref=reference, 

211 encoding=encoding, 

212 ) 

213 

214 @property 

215 def authority(self) -> t.Optional[str]: 

216 """Return the normalized authority.""" 

217 return self.reference.authority 

218 

219 def copy_with( 

220 self, 

221 scheme: t.Optional[str] = misc.UseExisting, 

222 userinfo: t.Optional[str] = misc.UseExisting, 

223 host: t.Optional[str] = misc.UseExisting, 

224 port: t.Optional[t.Union[int, str]] = misc.UseExisting, 

225 path: t.Optional[str] = misc.UseExisting, 

226 query: t.Optional[str] = misc.UseExisting, 

227 fragment: t.Optional[str] = misc.UseExisting, 

228 ) -> "ParseResult": 

229 """Create a copy of this instance replacing with specified parts.""" 

230 attributes = zip( 

231 PARSED_COMPONENTS, 

232 (scheme, userinfo, host, port, path, query, fragment), 

233 ) 

234 attrs_dict: t.Dict[str, t.Optional[str]] = {} 

235 for name, value in attributes: 

236 if value is misc.UseExisting: 

237 value = getattr(self, name) 

238 attrs_dict[name] = value 

239 authority = self._generate_authority(attrs_dict) 

240 ref = self.reference.copy_with( 

241 scheme=attrs_dict["scheme"], 

242 authority=authority, 

243 path=attrs_dict["path"], 

244 query=attrs_dict["query"], 

245 fragment=attrs_dict["fragment"], 

246 ) 

247 return ParseResult(uri_ref=ref, encoding=self.encoding, **attrs_dict) 

248 

249 def encode(self, encoding: t.Optional[str] = None) -> "ParseResultBytes": 

250 """Convert to an instance of ParseResultBytes.""" 

251 encoding = encoding or self.encoding 

252 attrs = dict( 

253 zip( 

254 PARSED_COMPONENTS, 

255 ( 

256 attr.encode(encoding) if hasattr(attr, "encode") else attr 

257 for attr in self 

258 ), 

259 ) 

260 ) 

261 return ParseResultBytes( 

262 uri_ref=self.reference, encoding=encoding, **attrs 

263 ) 

264 

265 def unsplit(self, use_idna: bool = False) -> str: 

266 """Create a URI string from the components. 

267 

268 :returns: The parsed URI reconstituted as a string. 

269 :rtype: str 

270 """ 

271 parse_result = self 

272 if use_idna and self.host: 

273 hostbytes = self.host.encode("idna") 

274 host = hostbytes.decode(self.encoding) 

275 parse_result = self.copy_with(host=host) 

276 return parse_result.reference.unsplit() 

277 

278 

279class ParseResultBytes( 

280 namedtuple("ParseResultBytes", PARSED_COMPONENTS), ParseResultMixin[bytes] 

281): 

282 """Compatibility shim for the urlparse.ParseResultBytes object.""" 

283 

284 scheme: t.Optional[bytes] 

285 userinfo: t.Optional[bytes] 

286 host: t.Optional[bytes] 

287 port: t.Optional[int] 

288 path: t.Optional[bytes] 

289 query: t.Optional[bytes] 

290 fragment: t.Optional[bytes] 

291 encoding: str 

292 reference: "uri.URIReference" 

293 lazy_normalize: bool 

294 

295 def __new__( 

296 cls, 

297 scheme: t.Optional[bytes], 

298 userinfo: t.Optional[bytes], 

299 host: t.Optional[bytes], 

300 port: t.Optional[int], 

301 path: t.Optional[bytes], 

302 query: t.Optional[bytes], 

303 fragment: t.Optional[bytes], 

304 uri_ref: "uri.URIReference", 

305 encoding: str = "utf-8", 

306 lazy_normalize: bool = True, 

307 ) -> _Self: 

308 """Create a new ParseResultBytes instance.""" 

309 parse_result = super().__new__( 

310 cls, 

311 scheme or None, 

312 userinfo or None, 

313 host, 

314 port or None, 

315 path or None, 

316 query or None, 

317 fragment or None, 

318 ) 

319 parse_result.encoding = encoding 

320 parse_result.reference = uri_ref 

321 parse_result.lazy_normalize = lazy_normalize 

322 return parse_result 

323 

324 @classmethod 

325 def from_parts( 

326 cls, 

327 scheme: t.Optional[str] = None, 

328 userinfo: t.Optional[str] = None, 

329 host: t.Optional[str] = None, 

330 port: t.Optional[t.Union[int, str]] = None, 

331 path: t.Optional[str] = None, 

332 query: t.Optional[str] = None, 

333 fragment: t.Optional[str] = None, 

334 encoding: str = "utf-8", 

335 lazy_normalize: bool = True, 

336 ) -> _Self: 

337 """Create a ParseResult instance from its parts.""" 

338 authority = "" 

339 if userinfo is not None: 

340 authority += userinfo + "@" 

341 if host is not None: 

342 authority += host 

343 if port is not None: 

344 authority += f":{int(port)}" 

345 uri_ref = uri.URIReference( 

346 scheme=scheme, 

347 authority=authority, 

348 path=path, 

349 query=query, 

350 fragment=fragment, 

351 encoding=encoding, 

352 ) 

353 if not lazy_normalize: 

354 uri_ref = uri_ref.normalize() 

355 to_bytes = compat.to_bytes 

356 userinfo, host, port = authority_from(uri_ref, strict=True) 

357 return cls( 

358 scheme=to_bytes(scheme, encoding), 

359 userinfo=to_bytes(userinfo, encoding), 

360 host=to_bytes(host, encoding), 

361 port=port, 

362 path=to_bytes(path, encoding), 

363 query=to_bytes(query, encoding), 

364 fragment=to_bytes(fragment, encoding), 

365 uri_ref=uri_ref, 

366 encoding=encoding, 

367 lazy_normalize=lazy_normalize, 

368 ) 

369 

370 @classmethod 

371 def from_string( 

372 cls, 

373 uri_string: t.Union[str, bytes], 

374 encoding: str = "utf-8", 

375 strict: bool = True, 

376 lazy_normalize: bool = True, 

377 ) -> _Self: 

378 """Parse a URI from the given unicode URI string. 

379 

380 :param str uri_string: Unicode URI to be parsed into a reference. 

381 :param str encoding: The encoding of the string provided 

382 :param bool strict: Parse strictly according to :rfc:`3986` if True. 

383 If False, parse similarly to the standard library's urlparse 

384 function. 

385 :returns: :class:`ParseResultBytes` or subclass thereof 

386 """ 

387 reference = uri.URIReference.from_string(uri_string, encoding) 

388 if not lazy_normalize: 

389 reference = reference.normalize() 

390 userinfo, host, port = authority_from(reference, strict) 

391 

392 to_bytes = compat.to_bytes 

393 return cls( 

394 scheme=to_bytes(reference.scheme, encoding), 

395 userinfo=to_bytes(userinfo, encoding), 

396 host=to_bytes(host, encoding), 

397 port=port, 

398 path=to_bytes(reference.path, encoding), 

399 query=to_bytes(reference.query, encoding), 

400 fragment=to_bytes(reference.fragment, encoding), 

401 uri_ref=reference, 

402 encoding=encoding, 

403 lazy_normalize=lazy_normalize, 

404 ) 

405 

406 @property 

407 def authority(self) -> bytes: 

408 """Return the normalized authority.""" 

409 return self.reference.authority.encode(self.encoding) 

410 

411 def copy_with( 

412 self, 

413 scheme: t.Optional[t.Union[str, bytes]] = misc.UseExisting, 

414 userinfo: t.Optional[t.Union[str, bytes]] = misc.UseExisting, 

415 host: t.Optional[t.Union[str, bytes]] = misc.UseExisting, 

416 port: t.Optional[t.Union[int, str, bytes]] = misc.UseExisting, 

417 path: t.Optional[t.Union[str, bytes]] = misc.UseExisting, 

418 query: t.Optional[t.Union[str, bytes]] = misc.UseExisting, 

419 fragment: t.Optional[t.Union[str, bytes]] = misc.UseExisting, 

420 lazy_normalize: bool = True, 

421 ) -> "ParseResultBytes": 

422 """Create a copy of this instance replacing with specified parts.""" 

423 attributes = zip( 

424 PARSED_COMPONENTS, 

425 (scheme, userinfo, host, port, path, query, fragment), 

426 ) 

427 attrs_dict = {} 

428 for name, value in attributes: 

429 if value is misc.UseExisting: 

430 value = getattr(self, name) 

431 if not isinstance(value, bytes) and hasattr(value, "encode"): 

432 value = value.encode(self.encoding) 

433 attrs_dict[name] = value 

434 

435 if t.TYPE_CHECKING: 

436 attrs_dict = t.cast(t.Dict[str, t.Optional[bytes]], attrs_dict) 

437 

438 authority = self._generate_authority(attrs_dict) 

439 to_str = compat.to_str 

440 ref = self.reference.copy_with( 

441 scheme=to_str(attrs_dict["scheme"], self.encoding), 

442 authority=to_str(authority, self.encoding), 

443 path=to_str(attrs_dict["path"], self.encoding), 

444 query=to_str(attrs_dict["query"], self.encoding), 

445 fragment=to_str(attrs_dict["fragment"], self.encoding), 

446 ) 

447 if not lazy_normalize: 

448 ref = ref.normalize() 

449 return ParseResultBytes( 

450 uri_ref=ref, 

451 encoding=self.encoding, 

452 lazy_normalize=lazy_normalize, 

453 **attrs_dict, 

454 ) 

455 

456 def unsplit(self, use_idna: bool = False) -> bytes: 

457 """Create a URI bytes object from the components. 

458 

459 :returns: The parsed URI reconstituted as a string. 

460 :rtype: bytes 

461 """ 

462 parse_result = self 

463 if use_idna and self.host: 

464 # self.host is bytes, to encode to idna, we need to decode it 

465 # first 

466 host = self.host.decode(self.encoding) 

467 hostbytes = host.encode("idna") 

468 parse_result = self.copy_with(host=hostbytes) 

469 if self.lazy_normalize: 

470 parse_result = parse_result.copy_with(lazy_normalize=False) 

471 uri = parse_result.reference.unsplit() 

472 return uri.encode(self.encoding) 

473 

474 

475def split_authority( 

476 authority: str, 

477) -> t.Tuple[t.Optional[str], t.Optional[str], t.Optional[str]]: 

478 # Initialize our expected return values 

479 userinfo = host = port = None 

480 # Initialize an extra var we may need to use 

481 extra_host = None 

482 # Set-up rest in case there is no userinfo portion 

483 rest = authority 

484 

485 if "@" in authority: 

486 userinfo, rest = authority.rsplit("@", 1) 

487 

488 # Handle IPv6 host addresses 

489 if rest.startswith("["): 

490 host, rest = rest.split("]", 1) 

491 host += "]" 

492 

493 if ":" in rest: 

494 extra_host, port = rest.split(":", 1) 

495 elif not host and rest: 

496 host = rest 

497 

498 if extra_host and not host: 

499 host = extra_host 

500 

501 return userinfo, host, port 

502 

503 

504def authority_from( 

505 reference: "uri.URIReference", 

506 strict: bool, 

507) -> t.Tuple[t.Optional[str], t.Optional[str], t.Optional[int]]: 

508 try: 

509 subauthority = reference.authority_info() 

510 except exceptions.InvalidAuthority: 

511 if strict: 

512 raise 

513 userinfo, host, port = split_authority(reference.authority) 

514 else: 

515 # Thanks to Richard Barrell for this idea: 

516 # https://twitter.com/0x2ba22e11/status/617338811975139328 

517 userinfo = subauthority.get("userinfo") 

518 host = subauthority.get("host") 

519 port = subauthority.get("port") 

520 

521 if port: 

522 if port.isascii() and port.isdigit(): 

523 port = int(port) 

524 else: 

525 raise exceptions.InvalidPort(port) 

526 return userinfo, host, port