Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/yarl/_path.py: 65%

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

80 statements  

1"""Utilities for working with paths.""" 

2 

3from collections.abc import Sequence 

4from contextlib import suppress 

5 

6 

7def normalize_path_segments(segments: Sequence[str]) -> list[str]: 

8 """Drop '.' and '..' from a sequence of str segments""" 

9 

10 resolved_path: list[str] = [] 

11 

12 for seg in segments: 

13 if seg == "..": 

14 # ignore any .. segments that would otherwise cause an 

15 # IndexError when popped from resolved_path if 

16 # resolving for rfc3986 

17 with suppress(IndexError): 

18 resolved_path.pop() 

19 elif seg != ".": 

20 resolved_path.append(seg) 

21 

22 if segments and segments[-1] in (".", ".."): 

23 # do some post-processing here. 

24 # if the last segment was a relative dir, 

25 # then we need to append the trailing '/' 

26 resolved_path.append("") 

27 

28 return resolved_path 

29 

30 

31def normalize_path(path: str) -> str: 

32 # Drop '.' and '..' from str path 

33 prefix = "" 

34 if path and path[0] == "/": 

35 # preserve the "/" root element of absolute paths, copying it to the 

36 # normalised output as per sections 5.2.4 and 6.2.2.3 of rfc3986. 

37 prefix = "/" 

38 path = path[1:] 

39 

40 segments = path.split("/") 

41 return prefix + "/".join(normalize_path_segments(segments)) 

42 

43 

44def remove_dot_segments(path: str) -> str: 

45 """Remove '.' and '..' as the remove_dot_segments algorithm does. 

46 

47 The steps of RFC 3986 section 5.2.4, also for a path without a leading 

48 "/": unlike normalize_path(), "a/../b" is "/b". The input buffer is 

49 path[pos:], so each step takes constant time apart from the segment it 

50 copies. 

51 """ 

52 output: list[str] = [] 

53 pos = 0 

54 end = len(path) 

55 while pos < end: 

56 if path.startswith("../", pos): 

57 pos += 3 

58 elif path.startswith(("./", "/./"), pos): 

59 pos += 2 

60 elif path.startswith("/../", pos): 

61 pos += 3 

62 if output: 

63 output.pop() 

64 elif end - pos == 2 and path.startswith("/.", pos): 

65 # "/." is replaced by "/", the last segment. 

66 output.append("/") 

67 break 

68 elif end - pos == 3 and path.startswith("/..", pos): 

69 # "/.." is replaced by "/" after dropping the previous segment. 

70 if output: 

71 output.pop() 

72 output.append("/") 

73 break 

74 elif end - pos <= 2 and path[pos:] in (".", ".."): 

75 break 

76 else: 

77 next_pos = path.find("/", pos + 1) 

78 if next_pos == -1: 

79 next_pos = end 

80 output.append(path[pos:next_pos]) 

81 pos = next_pos 

82 return "".join(output) 

83 

84 

85# Windows drive letters in file URLs, as the WHATWG URL Standard reads them: 

86# https://url.spec.whatwg.org/#windows-drive-letter 

87 

88 

89def is_drive_letter(segment: str) -> bool: 

90 """Tell if segment is a Windows drive letter, as "C:" or "C|".""" 

91 return ( 

92 len(segment) == 2 

93 and segment[1] in ":|" 

94 and segment[0].isascii() 

95 and segment[0].isalpha() 

96 ) 

97 

98 

99def starts_with_drive_letter(path: str, pos: int = 0) -> bool: 

100 """Tell if path[pos:] starts with a Windows drive letter. 

101 

102 The drive letter must be the whole first segment: "C:/a" and "C|" do, 

103 "C|a" does not. 

104 """ 

105 return is_drive_letter(path[pos : pos + 2]) and ( 

106 len(path) == pos + 2 or path[pos + 2] == "/" 

107 ) 

108 

109 

110def drive_letter(path: str) -> str: 

111 """Return the normalized drive letter, as "C:", that path starts with. 

112 

113 It is the first segment of an absolute or a rootless path, and the 

114 empty string when that is not a normalized drive letter. 

115 """ 

116 pos = 1 if path[:1] == "/" else 0 

117 if starts_with_drive_letter(path, pos) and path[pos + 1] == ":": 

118 return path[pos : pos + 2] 

119 return "" 

120 

121 

122def normalize_drive_letter(path: str) -> str: 

123 """Write the "|" of a drive letter that starts an unquoted path as ":". 

124 

125 The WHATWG path state does it only for the first segment, so "/C|/a" is 

126 "/C:/a" while "//C|/a" and "/a/C|" are kept. 

127 """ 

128 pos = 1 if path[:1] == "/" else 0 

129 if starts_with_drive_letter(path, pos) and path[pos + 1] == "|": 

130 return f"{path[: pos + 1]}:{path[pos + 2 :]}" 

131 return path 

132 

133 

134def normalize_file_path(path: str) -> str: 

135 """Drop '.' and '..' from the path of a file URL, keeping its drive letter. 

136 

137 As normalize_path(), except that '..' never removes a drive letter that 

138 is the first segment: the WHATWG "shorten a path" keeps "C:" in 

139 "/C:/.." and in "/C:/a/../..". 

140 """ 

141 prefix = "" 

142 if path and path[0] == "/": 

143 prefix = "/" 

144 path = path[1:] 

145 segments = path.split("/") 

146 resolved: list[str] = [] 

147 for seg in segments: 

148 if seg == "..": 

149 if resolved and not (len(resolved) == 1 and drive_letter(resolved[0])): 

150 resolved.pop() 

151 elif seg != ".": 

152 resolved.append(seg) 

153 if segments[-1] in (".", ".."): 

154 resolved.append("") 

155 return prefix + "/".join(resolved)