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
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
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."""
16import typing as t
17from collections import namedtuple
19from . import compat
20from . import exceptions
21from . import misc
22from . import normalizers
23from . import uri
24from ._typing_compat import Self as _Self
26__all__ = ("ParseResult", "ParseResultBytes")
28PARSED_COMPONENTS = (
29 "scheme",
30 "userinfo",
31 "host",
32 "port",
33 "path",
34 "query",
35 "fragment",
36)
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
47 @property
48 def authority(self) -> t.Optional[t.AnyStr]: ...
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
73 def geturl(self) -> t.AnyStr:
74 """Shim to match the standard library method."""
75 return self.unsplit()
77 @property
78 def hostname(self) -> t.Optional[t.AnyStr]:
79 """Shim to match the standard library."""
80 return self.host
82 @property
83 def netloc(self) -> t.Optional[t.AnyStr]:
84 """Shim to match the standard library."""
85 return self.authority
87 @property
88 def params(self) -> t.Optional[t.AnyStr]:
89 """Shim to match the standard library."""
90 return self.query
93class ParseResult(
94 namedtuple("ParseResult", PARSED_COMPONENTS), ParseResultMixin[str]
95):
96 """Implementation of urlparse compatibility class.
98 This uses the URIReference logic to handle compatibility with the
99 urlparse.ParseResult class.
100 """
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"
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
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 )
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.
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)
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 )
214 @property
215 def authority(self) -> t.Optional[str]:
216 """Return the normalized authority."""
217 return self.reference.authority
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)
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 )
265 def unsplit(self, use_idna: bool = False) -> str:
266 """Create a URI string from the components.
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()
279class ParseResultBytes(
280 namedtuple("ParseResultBytes", PARSED_COMPONENTS), ParseResultMixin[bytes]
281):
282 """Compatibility shim for the urlparse.ParseResultBytes object."""
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
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
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 )
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.
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)
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 )
406 @property
407 def authority(self) -> bytes:
408 """Return the normalized authority."""
409 return self.reference.authority.encode(self.encoding)
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
435 if t.TYPE_CHECKING:
436 attrs_dict = t.cast(t.Dict[str, t.Optional[bytes]], attrs_dict)
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 )
456 def unsplit(self, use_idna: bool = False) -> bytes:
457 """Create a URI bytes object from the components.
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)
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
485 if "@" in authority:
486 userinfo, rest = authority.rsplit("@", 1)
488 # Handle IPv6 host addresses
489 if rest.startswith("["):
490 host, rest = rest.split("]", 1)
491 host += "]"
493 if ":" in rest:
494 extra_host, port = rest.split(":", 1)
495 elif not host and rest:
496 host = rest
498 if extra_host and not host:
499 host = extra_host
501 return userinfo, host, port
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")
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