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

1import re 

2 

3__all__ = ["quote", "unquote", "urldecode", "urlencode"] 

4 

5 

6# Matchs a sequence of one or more '%xx' escapes. 

7PERCENT_ENCODED_REGEX = re.compile("(%[A-Fa-f0-9][A-Fa-f0-9])+") 

8 

9# https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 

10SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" 

11 

12 

13def urlencode(multidict, safe=SAFE): 

14 pairs = [] 

15 for key, values in multidict.items(): 

16 pairs.extend([(key, value) for value in values]) 

17 

18 safe += "+" 

19 pairs = [(k.replace(" ", "+"), v.replace(" ", "+")) for k, v in pairs] 

20 

21 return "&".join( 

22 f"{quote(key, safe)}={quote(val, safe)}" 

23 for key, val in pairs 

24 ) 

25 

26 

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 ] 

33 

34 pairs = [(k.replace("+", " "), v.replace("+", " ")) for k, v in pairs] 

35 

36 ret = {} 

37 for k, v in pairs: 

38 ret.setdefault(k, []).append(v) 

39 return ret 

40 

41 

42def quote(string, safe=SAFE): 

43 # Fast path if the string is already safe. 

44 if not string.strip(safe): 

45 return string 

46 

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 ]) 

52 

53 

54def unquote(string): 

55 # Fast path if the string is not quoted. 

56 if '%' not in string: 

57 return string 

58 

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) 

69 

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 

75 

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) 

80 

81 return "".join(parts) 

82 

83 

84def percent(c): 

85 return ''.join(f"%{b:02X}" for b in c.encode("utf-8"))