Coverage for src/httpx/_urlencode.py: 100%
44 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-23 22:12 +0100
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-23 22:12 +0100
1import re
3__all__ = ["quote", "unquote", "urldecode", "urlencode"]
6# Matchs a sequence of one or more '%xx' escapes.
7PERCENT_ENCODED_REGEX = re.compile("(%[A-Fa-f0-9][A-Fa-f0-9])+")
9# https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
10SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
13def urlencode(multidict, safe=SAFE):
14 pairs = []
15 for key, values in multidict.items():
16 pairs.extend([(key, value) for value in values])
18 safe += "+"
19 pairs = [(k.replace(" ", "+"), v.replace(" ", "+")) for k, v in pairs]
21 return "&".join(
22 f"{quote(key, safe)}={quote(val, safe)}"
23 for key, val in pairs
24 )
27def urldecode(string):
28 parts = [part.partition("=") for part in string.split("&") if part]
29 pairs = [
30 (unquote(key), unquote(val))
31 for key, _, val in parts
32 ]
34 pairs = [(k.replace("+", " "), v.replace("+", " ")) for k, v in pairs]
36 ret = {}
37 for k, v in pairs:
38 ret.setdefault(k, []).append(v)
39 return ret
42def quote(string, safe=SAFE):
43 # Fast path if the string is already safe.
44 if not string.strip(safe):
45 return string
47 # Replace any characters not in the safe set with '%xx' escape sequences.
48 return "".join([
49 char if char in safe else percent(char)
50 for char in string
51 ])
54def unquote(string):
55 # Fast path if the string is not quoted.
56 if '%' not in string:
57 return string
59 # Unquote.
60 parts = []
61 current_position = 0
62 for match in re.finditer(PERCENT_ENCODED_REGEX, string):
63 start_position, end_position = match.start(), match.end()
64 matched_text = match.group(0)
65 # Include any text up to the '%xx' escape sequence.
66 if start_position != current_position:
67 leading_text = string[current_position:start_position]
68 parts.append(leading_text)
70 # Decode the '%xx' escape sequence.
71 hex = matched_text.replace('%', '')
72 decoded = bytes.fromhex(hex).decode('utf-8')
73 parts.append(decoded)
74 current_position = end_position
76 # Include any text after the final '%xx' escape sequence.
77 if current_position != len(string):
78 trailing_text = string[current_position:]
79 parts.append(trailing_text)
81 return "".join(parts)
84def percent(c):
85 return ''.join(f"%{b:02X}" for b in c.encode("utf-8"))