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
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"""Utilities for working with paths."""
3from collections.abc import Sequence
4from contextlib import suppress
7def normalize_path_segments(segments: Sequence[str]) -> list[str]:
8 """Drop '.' and '..' from a sequence of str segments"""
10 resolved_path: list[str] = []
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)
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("")
28 return resolved_path
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:]
40 segments = path.split("/")
41 return prefix + "/".join(normalize_path_segments(segments))
44def remove_dot_segments(path: str) -> str:
45 """Remove '.' and '..' as the remove_dot_segments algorithm does.
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)
85# Windows drive letters in file URLs, as the WHATWG URL Standard reads them:
86# https://url.spec.whatwg.org/#windows-drive-letter
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 )
99def starts_with_drive_letter(path: str, pos: int = 0) -> bool:
100 """Tell if path[pos:] starts with a Windows drive letter.
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 )
110def drive_letter(path: str) -> str:
111 """Return the normalized drive letter, as "C:", that path starts with.
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 ""
122def normalize_drive_letter(path: str) -> str:
123 """Write the "|" of a drive letter that starts an unquoted path as ":".
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
134def normalize_file_path(path: str) -> str:
135 """Drop '.' and '..' from the path of a file URL, keeping its drive letter.
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)