Skip to content

Commit 56e6482

Browse files
torcolvinnicoddemus
andcommitted
Fix removal of very long paths on Windows (#6755)
Co-authored-by: Bruno Oliveira <[email protected]>
1 parent 589176e commit 56e6482

File tree

4 files changed

+58
-0
lines changed

4 files changed

+58
-0
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ Tom Dalton
267267
Tom Viner
268268
Tomáš Gavenčiak
269269
Tomer Keren
270+
Tor Colvin
270271
Trevor Bekolay
271272
Tyler Goodlet
272273
Tzu-ping Chung

changelog/6755.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support deleting paths longer than 260 characters on windows created inside tmpdir.

src/_pytest/pathlib.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,41 @@ def chmod_rw(p: str) -> None:
100100
return True
101101

102102

103+
def ensure_extended_length_path(path: Path) -> Path:
104+
"""Get the extended-length version of a path (Windows).
105+
106+
On Windows, by default, the maximum length of a path (MAX_PATH) is 260
107+
characters, and operations on paths longer than that fail. But it is possible
108+
to overcome this by converting the path to "extended-length" form before
109+
performing the operation:
110+
https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
111+
112+
On Windows, this function returns the extended-length absolute version of path.
113+
On other platforms it returns path unchanged.
114+
"""
115+
if sys.platform.startswith("win32"):
116+
path = path.resolve()
117+
path = Path(get_extended_length_path_str(str(path)))
118+
return path
119+
120+
121+
def get_extended_length_path_str(path: str) -> str:
122+
"""Converts to extended length path as a str"""
123+
long_path_prefix = "\\\\?\\"
124+
unc_long_path_prefix = "\\\\?\\UNC\\"
125+
if path.startswith((long_path_prefix, unc_long_path_prefix)):
126+
return path
127+
# UNC
128+
if path.startswith("\\\\"):
129+
return unc_long_path_prefix + path[2:]
130+
return long_path_prefix + path
131+
132+
103133
def rm_rf(path: Path) -> None:
104134
"""Remove the path contents recursively, even if some elements
105135
are read-only.
106136
"""
137+
path = ensure_extended_length_path(path)
107138
onerror = partial(on_rm_rf_error, start_path=path)
108139
shutil.rmtree(str(path), onerror=onerror)
109140

@@ -220,6 +251,7 @@ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> Non
220251

221252
def maybe_delete_a_numbered_dir(path: Path) -> None:
222253
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
254+
path = ensure_extended_length_path(path)
223255
lock_path = None
224256
try:
225257
lock_path = create_cleanup_lock(path)

testing/test_pathlib.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77
from _pytest.pathlib import fnmatch_ex
8+
from _pytest.pathlib import get_extended_length_path_str
89
from _pytest.pathlib import get_lock_path
910
from _pytest.pathlib import maybe_delete_a_numbered_dir
1011
from _pytest.pathlib import Path
@@ -89,3 +90,26 @@ def renamed_failed(*args):
8990
lock_path = get_lock_path(path)
9091
maybe_delete_a_numbered_dir(path)
9192
assert not lock_path.is_file()
93+
94+
95+
def test_long_path_during_cleanup(tmp_path):
96+
"""Ensure that deleting long path works (particularly on Windows (#6775))."""
97+
path = (tmp_path / ("a" * 250)).resolve()
98+
if sys.platform == "win32":
99+
# make sure that the full path is > 260 characters without any
100+
# component being over 260 characters
101+
assert len(str(path)) > 260
102+
extended_path = "\\\\?\\" + str(path)
103+
else:
104+
extended_path = str(path)
105+
os.mkdir(extended_path)
106+
assert os.path.isdir(extended_path)
107+
maybe_delete_a_numbered_dir(path)
108+
assert not os.path.isdir(extended_path)
109+
110+
111+
def test_get_extended_length_path_str():
112+
assert get_extended_length_path_str(r"c:\foo") == r"\\?\c:\foo"
113+
assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo"
114+
assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo"
115+
assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo"

0 commit comments

Comments
 (0)