Skip to content

Commit 47b81d6

Browse files
committed
Extend functionality of posix and mixed path conversion
Do not allow functions to fail silently Remove usage of shlex.split Raise the appropriate errors if handed something unexpected Test for errors being raised Do not support relative paths Handle UNC paths and long paths Handle same style paths gracefully Signed-off-by: javrin <[email protected]>
1 parent 793a96c commit 47b81d6

File tree

2 files changed

+270
-31
lines changed

2 files changed

+270
-31
lines changed

src/rez/tests/test_utils.py

Lines changed: 202 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,157 @@ def test_convert_mixed_override_path_sep(self):
103103
self.assertEqual(converted_path, expected_path)
104104

105105

106+
class TestToPosixPath(TestBase):
107+
108+
@platform_dependent(["windows"])
109+
def test_normal_windows_paths(self):
110+
self.assertEqual(cygpath.to_posix_path(
111+
"C:\\Users\\John\\Documents"), "/c/Users/John/Documents"
112+
)
113+
self.assertEqual(
114+
cygpath.to_posix_path("D:\\Projects\\Python"), "/d/Projects/Python"
115+
)
116+
117+
@platform_dependent(["windows"])
118+
def test_windows_paths_with_spaces(self):
119+
self.assertEqual(cygpath.to_posix_path(
120+
"C:\\Program Files\\Python"), "/c/Program Files/Python"
121+
)
122+
self.assertEqual(cygpath.to_posix_path(
123+
"D:\\My Documents\\Photos"), "/d/My Documents/Photos"
124+
)
125+
126+
@platform_dependent(["windows"])
127+
def test_windows_paths_with_special_characters(self):
128+
self.assertEqual(cygpath.to_posix_path(
129+
"C:\\Users\\John\\#Projects"), "/c/Users/John/#Projects"
130+
)
131+
self.assertEqual(cygpath.to_posix_path(
132+
"D:\\Projects\\Python@Home"), "/d/Projects/Python@Home"
133+
)
134+
135+
@platform_dependent(["windows"])
136+
def test_windows_paths_with_mixed_slashes(self):
137+
self.assertEqual(cygpath.to_posix_path(
138+
"C:\\Users/John/Documents"), "/c/Users/John/Documents"
139+
)
140+
self.assertEqual(
141+
cygpath.to_posix_path("D:/Projects\\Python"), "/d/Projects/Python"
142+
)
143+
144+
@platform_dependent(["windows"])
145+
def test_windows_paths_with_lowercase_drive_letters(self):
146+
self.assertEqual(cygpath.to_posix_path(
147+
"c:\\Users\\John\\Documents"), "/c/Users/John/Documents"
148+
)
149+
self.assertEqual(
150+
cygpath.to_posix_path("d:\\Projects\\Python"), "/d/Projects/Python"
151+
)
152+
153+
@platform_dependent(["windows"])
154+
def test_already_posix_style_paths(self):
155+
self.assertEqual(cygpath.to_posix_path(
156+
"/c/Users/John/Documents"), "/c/Users/John/Documents"
157+
)
158+
self.assertEqual(
159+
cygpath.to_posix_path("/d/projects/python"), "/d/projects/python")
160+
self.assertRaisesRegexp(
161+
ValueError,
162+
"Cannot convert path to posix path: '.*' "
163+
"Please ensure that the path is absolute",
164+
cygpath.to_posix_path,
165+
"/home/john/documents"
166+
)
167+
self.assertRaisesRegexp(
168+
ValueError,
169+
"Cannot convert path to posix path: '.*' "
170+
"Please ensure that the path is absolute",
171+
cygpath.to_posix_path,
172+
"/projects/python"
173+
)
174+
175+
@platform_dependent(["windows"])
176+
def test_relative_paths(self):
177+
self.assertRaisesRegexp(
178+
ValueError,
179+
"Cannot convert path to posix path: '.*' "
180+
"Please ensure that the path is absolute",
181+
cygpath.to_posix_path,
182+
"jane/documents"
183+
)
184+
185+
self.assertRaisesRegexp(
186+
ValueError,
187+
"Cannot convert path to posix path: '.*' "
188+
"Please ensure that the path is absolute",
189+
cygpath.to_posix_path,
190+
"projects/python/file.py"
191+
)
192+
193+
@platform_dependent(["windows"])
194+
def test_windows_unc_paths(self):
195+
self.assertEqual(cygpath.to_posix_path(
196+
"\\\\Server\\Share\\folder"), "//Server/Share/folder"
197+
)
198+
self.assertEqual(cygpath.to_posix_path(
199+
"\\\\server\\share\\folder\\file.txt"), "//server/share/folder/file.txt"
200+
)
201+
202+
@platform_dependent(["windows"])
203+
def test_windows_long_paths(self):
204+
self.assertEqual(cygpath.to_posix_path(
205+
"\\\\?\\C:\\Users\\Jane\\Documents"), "/c/Users/Jane/Documents"
206+
)
207+
self.assertEqual(cygpath.to_posix_path(
208+
"\\\\?\\d:\\projects\\python"), "/d/projects/python"
209+
)
210+
211+
@platform_dependent(["windows"])
212+
def test_windows_malformed_paths(self):
213+
self.assertEqual(cygpath.to_posix_path(
214+
"C:\\Users/Jane/\\Documents"), "/c/Users/Jane/Documents"
215+
)
216+
self.assertEqual(
217+
cygpath.to_posix_path("D:/Projects\\/Python"), "/d/Projects/Python"
218+
)
219+
self.assertEqual(cygpath.to_posix_path(
220+
"C:/Users\\Jane/Documents"), "/c/Users/Jane/Documents"
221+
)
222+
self.assertEqual(
223+
cygpath.to_posix_path("D:\\projects/python"), "/d/projects/python"
224+
)
225+
self.assertRaisesRegexp(
226+
ValueError,
227+
"Cannot convert path to posix path: '.*' "
228+
"This is most likely due to a malformed path",
229+
cygpath.to_posix_path,
230+
"D:\\..\\Projects"
231+
)
232+
self.assertRaisesRegexp(
233+
ValueError,
234+
"Cannot convert path to posix path: '.*' "
235+
"This is most likely due to a malformed path",
236+
cygpath.to_posix_path,
237+
"/d/..\\projects"
238+
)
239+
240+
@platform_dependent(["windows"])
241+
def test_dotted_paths(self):
242+
self.assertEqual(cygpath.to_posix_path(
243+
"C:\\Users\\John\\..\\Projects"), "/c/Users/Projects"
244+
)
245+
self.assertEqual(cygpath.to_posix_path(
246+
"/c/users/./jane"), "/c/users/jane"
247+
)
248+
self.assertRaisesRegexp(
249+
ValueError,
250+
"Cannot convert path to posix path: '.*' "
251+
"Please ensure that the path is absolute",
252+
cygpath.to_posix_path,
253+
"./projects/python"
254+
)
255+
256+
106257
class TestToCygdrive(TestBase):
107258
"""Test cygpath.to_cygdrive() function."""
108259

@@ -150,7 +301,9 @@ def test_edge_cases(self):
150301
self.assertEqual(cygpath.to_cygdrive("C:/"), "/c/")
151302
self.assertEqual(cygpath.to_cygdrive("D:\\folder with space"), "/d/")
152303
# Unsupported and reserved characters
153-
self.assertEqual(cygpath.to_cygdrive("E:\\folder!@#$%^&*()_+-={}[]|;:,.<>?"), "/e/")
304+
self.assertEqual(
305+
cygpath.to_cygdrive("E:\\folder!@#$%^&*()_+-={}[]|;:,.<>?"), "/e/"
306+
)
154307
self.assertEqual(cygpath.to_cygdrive("F:\\folder_日本語"), "/f/")
155308
self.assertEqual(cygpath.to_cygdrive("\\\\?\\C:\\folder\\file.txt"), "/c/")
156309

@@ -165,6 +318,15 @@ def test_normal_windows_paths(self):
165318
self.assertEqual(cygpath.to_mixed_path(
166319
'E:\\projects\\python\\main.py'), 'E:/projects/python/main.py')
167320

321+
@platform_dependent(["windows"])
322+
def test_already_mixed_style_paths(self):
323+
self.assertEqual(
324+
cygpath.to_mixed_path('C:/home/john/documents'), 'C:/home/john/documents'
325+
)
326+
self.assertEqual(cygpath.to_mixed_path(
327+
'Z:/projects/python'), 'Z:/projects/python'
328+
)
329+
168330
@platform_dependent(["windows"])
169331
def test_paths_with_escaped_backslashes(self):
170332
self.assertEqual(cygpath.to_mixed_path('C:\\\\foo\\\\bar'), 'C:/foo/bar')
@@ -187,18 +349,48 @@ def test_paths_with_mixed_slashes(self):
187349

188350
@platform_dependent(["windows"])
189351
def test_paths_with_no_drive_letter(self):
190-
self.assertEqual(cygpath.to_mixed_path(
191-
'\\foo\\bar'), '/foo/bar'
352+
self.assertRaisesRegexp(
353+
ValueError,
354+
"Cannot convert path to mixed path: '.*' "
355+
"Please ensure that the path is absolute",
356+
cygpath.to_mixed_path,
357+
'\\foo\\bar'
192358
)
193-
self.assertEqual(cygpath.to_mixed_path(
194-
'\\\\my_folder\\my_file.txt'), '//my_folder/my_file.txt'
359+
360+
self.assertRaisesRegexp(
361+
ValueError,
362+
"Cannot convert path to mixed path: '.*' "
363+
"Please ensure that the path is absolute",
364+
cygpath.to_mixed_path,
365+
'\\\\my_folder\\my_file.txt'
195366
)
196-
self.assertEqual(cygpath.to_mixed_path(
197-
'/projects/python/main.py'), '/projects/python/main.py'
367+
368+
self.assertRaisesRegexp(
369+
ValueError,
370+
"Cannot convert path to mixed path: '.*' "
371+
"Please ensure that the path is absolute",
372+
cygpath.to_mixed_path,
373+
'/projects/python/main.py'
198374
)
199375

200376
@platform_dependent(["windows"])
201377
def test_paths_with_only_a_drive_letter(self):
202-
self.assertEqual(cygpath.to_mixed_path('C:'), 'C:')
203-
self.assertEqual(cygpath.to_mixed_path('D:'), 'D:')
204-
self.assertEqual(cygpath.to_mixed_path('E:'), 'E:')
378+
self.assertEqual(cygpath.to_mixed_path('C:'), 'C:/')
379+
self.assertEqual(cygpath.to_mixed_path('D:'), 'D:/')
380+
self.assertEqual(cygpath.to_mixed_path('E:'), 'E:/')
381+
382+
@platform_dependent(["windows"])
383+
def test_dotted_paths(self):
384+
self.assertEqual(cygpath.to_mixed_path(
385+
"C:\\Users\\John\\..\\Projects"), "C:/Users/Projects"
386+
)
387+
self.assertEqual(cygpath.to_mixed_path(
388+
"C:/users/./jane"), "C:/users/jane"
389+
)
390+
self.assertRaisesRegexp(
391+
ValueError,
392+
"Cannot convert path to posix path: '.*' "
393+
"Please ensure that the path is absolute",
394+
cygpath.to_posix_path,
395+
"./projects/python"
396+
)

src/rez/utils/cygpath.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,10 @@
1111
import os
1212
import posixpath
1313
import re
14-
import shlex
1514

1615
from rez.config import config
1716
from rez.utils.logging_ import print_debug
1817

19-
_drive_start_regex = re.compile(r"^([A-Za-z]):[\\/]")
20-
_drive_regex_mixed = re.compile(r"([a-z]):/")
21-
2218

2319
def log(*msg):
2420
if config.debug("cygpath"):
@@ -88,12 +84,42 @@ def to_posix_path(path):
8884
Returns:
8985
str: Converted path.
9086
"""
87+
# Handle Windows long paths
88+
if path.startswith("\\\\?\\"):
89+
path = path[4:]
90+
91+
unc, path = os.path.splitunc(path)
92+
if unc:
93+
path = unc.replace("\\", "/") + path.replace("\\", "/")
94+
return path
95+
9196
drive = to_cygdrive(path)
97+
98+
# Relative, or already in posix format (but missing a drive!)
99+
if not drive:
100+
raise ValueError(
101+
"Cannot convert path to posix path: {!r} "
102+
"Please ensure that the path is absolute".format(path)
103+
)
104+
92105
_, path = os.path.splitdrive(path)
93-
path = path.replace("\\", "", 1)
94-
path = drive + path.replace("\\", "/")
95106

96-
return path
107+
# Already posix style
108+
if path.startswith(drive):
109+
path = path[len(drive):]
110+
111+
# Remove leading slashes
112+
path = re.sub(r"^[\\/]+", "", path)
113+
path = slashify(path)
114+
115+
# Drive and path will concatenate into an unexpected result
116+
if drive and path[0] == ".":
117+
raise ValueError(
118+
"Cannot convert path to posix path: {!r} "
119+
"This is most likely due to a malformed path".format(path)
120+
)
121+
122+
return drive + path
97123

98124

99125
def to_mixed_path(path):
@@ -105,22 +131,36 @@ def to_mixed_path(path):
105131
Returns:
106132
str: Converted path.
107133
"""
108-
def slashify(path):
109-
path = path.replace("\\", "/")
110-
path = re.sub(r'/{2,}', '/', path)
111-
return path
112-
113134
drive, path = os.path.splitdrive(path)
135+
114136
if not drive:
115-
return slashify(path)
137+
raise ValueError(
138+
"Cannot convert path to mixed path: {!r} "
139+
"Please ensure that the path is absolute".format(path)
140+
)
116141
if drive and not path:
117-
return drive.replace("\\", "/")
142+
if len(drive) == 2:
143+
return drive + posixpath.sep
144+
raise ValueError(
145+
"Cannot convert path to mixed path: {!r} "
146+
"Please ensure that the path is absolute".format(path)
147+
)
118148

119149
path = slashify(path)
120150

121151
return drive + path
122152

123153

154+
def slashify(path):
155+
# Remove double backslashes and dots
156+
path = os.path.normpath(path)
157+
# Normalize slashes
158+
path = path.replace("\\", "/")
159+
# Remove double slashes
160+
path = re.sub(r'/{2,}', '/', path)
161+
return path
162+
163+
124164
def to_cygdrive(path):
125165
r"""Convert an NT drive to a cygwin-style drive.
126166
@@ -139,15 +179,21 @@ def to_cygdrive(path):
139179
# Normalize forward backslashes to slashes
140180
path = path.replace("\\", "/")
141181

142-
# Split the path into tokens using shlex
143-
tokens = shlex.split(path) or path # no tokens
144-
145-
# Empty paths are invalid
146-
if not tokens:
182+
# UNC paths are not supported
183+
unc, _ = os.path.splitunc(path)
184+
if unc:
147185
return ""
148186

149-
# Extract the drive letter from the first token
150-
drive, _ = os.path.splitdrive(tokens[0])
187+
if (
188+
path.startswith(posixpath.sep)
189+
and len(path) >= 2
190+
and path[1].isalpha()
191+
and path[2] == posixpath.sep
192+
):
193+
drive = path[1]
194+
else:
195+
# Extract the drive letter from the first token
196+
drive, _ = os.path.splitdrive(path)
151197

152198
if drive:
153199
# Check if the drive letter is valid
@@ -156,4 +202,5 @@ def to_cygdrive(path):
156202
# Valid drive letter format
157203
return posixpath.sep + drive_letter + posixpath.sep
158204

205+
# Most likely a relative path
159206
return ""

0 commit comments

Comments
 (0)