Skip to content

Commit ff84a74

Browse files
authored
Merge pull request gitpython-developers#1739 from EliahKagan/rmtree
Make the rmtree callback Windows-only
2 parents 74cc671 + b226f3d commit ff84a74

File tree

2 files changed

+50
-17
lines changed

2 files changed

+50
-17
lines changed

Diff for: git/util.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None:
219219
raise SkipTest(f"FIXME: fails with: PermissionError\n {ex}") from ex
220220
raise
221221

222-
if sys.version_info >= (3, 12):
222+
if os.name != "nt":
223+
shutil.rmtree(path)
224+
elif sys.version_info >= (3, 12):
223225
shutil.rmtree(path, onexc=handler)
224226
else:
225227
shutil.rmtree(path, onerror=handler)

Diff for: test/test_util.py

+47-16
Original file line numberDiff line numberDiff line change
@@ -109,31 +109,66 @@ def test_deletes_dir_with_readonly_files(self, tmp_path):
109109
sys.platform == "cygwin",
110110
reason="Cygwin can't set the permissions that make the test meaningful.",
111111
)
112-
def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
113-
"""rmtree wraps PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is true."""
112+
def test_avoids_changing_permissions_outside_tree(self, tmp_path):
113+
# Automatically works on Windows, but on Unix requires either special handling
114+
# or refraining from attempting to fix PermissionError by making chmod calls.
115+
116+
dir1 = tmp_path / "dir1"
117+
dir1.mkdir()
118+
(dir1 / "file").write_bytes(b"")
119+
(dir1 / "file").chmod(stat.S_IRUSR)
120+
old_mode = (dir1 / "file").stat().st_mode
121+
122+
dir2 = tmp_path / "dir2"
123+
dir2.mkdir()
124+
(dir2 / "symlink").symlink_to(dir1 / "file")
125+
dir2.chmod(stat.S_IRUSR | stat.S_IXUSR)
126+
127+
try:
128+
rmtree(dir2)
129+
except PermissionError:
130+
pass # On Unix, dir2 is not writable, so dir2/symlink may not be deleted.
131+
except SkipTest as ex:
132+
self.fail(f"rmtree unexpectedly attempts skip: {ex!r}")
133+
134+
new_mode = (dir1 / "file").stat().st_mode
135+
assert old_mode == new_mode, f"Should stay {old_mode:#o}, became {new_mode:#o}."
136+
137+
def _patch_for_wrapping_test(self, mocker, hide_windows_known_errors):
114138
# Access the module through sys.modules so it is unambiguous which module's
115139
# attribute we patch: the original git.util, not git.index.util even though
116140
# git.index.util "replaces" git.util and is what "import git.util" gives us.
117-
mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", True)
141+
mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
118142

119-
# Disable common chmod functions so the callback can never fix the problem.
143+
# Disable common chmod functions so the callback can't fix a PermissionError.
120144
mocker.patch.object(os, "chmod")
121145
mocker.patch.object(pathlib.Path, "chmod")
122146

123-
# Now we can see how an intractable PermissionError is treated.
147+
@pytest.mark.skipif(
148+
os.name != "nt",
149+
reason="PermissionError is only ever wrapped on Windows",
150+
)
151+
def test_wraps_perm_error_if_enabled(self, mocker, permission_error_tmpdir):
152+
"""rmtree wraps PermissionError on Windows when HIDE_WINDOWS_KNOWN_ERRORS is true."""
153+
self._patch_for_wrapping_test(mocker, True)
154+
124155
with pytest.raises(SkipTest):
125156
rmtree(permission_error_tmpdir)
126157

127158
@pytest.mark.skipif(
128159
sys.platform == "cygwin",
129160
reason="Cygwin can't set the permissions that make the test meaningful.",
130161
)
131-
def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir):
132-
"""rmtree does not wrap PermissionError when HIDE_WINDOWS_KNOWN_ERRORS is false."""
133-
# See comments in test_wraps_perm_error_if_enabled for details about patching.
134-
mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", False)
135-
mocker.patch.object(os, "chmod")
136-
mocker.patch.object(pathlib.Path, "chmod")
162+
@pytest.mark.parametrize(
163+
"hide_windows_known_errors",
164+
[
165+
pytest.param(False),
166+
pytest.param(True, marks=pytest.mark.skipif(os.name == "nt", reason="We would wrap on Windows")),
167+
],
168+
)
169+
def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_tmpdir, hide_windows_known_errors):
170+
"""rmtree does not wrap PermissionError on non-Windows systems or when HIDE_WINDOWS_KNOWN_ERRORS is false."""
171+
self._patch_for_wrapping_test(mocker, hide_windows_known_errors)
137172

138173
with pytest.raises(PermissionError):
139174
try:
@@ -144,11 +179,7 @@ def test_does_not_wrap_perm_error_unless_enabled(self, mocker, permission_error_
144179
@pytest.mark.parametrize("hide_windows_known_errors", [False, True])
145180
def test_does_not_wrap_other_errors(self, tmp_path, mocker, hide_windows_known_errors):
146181
file_not_found_tmpdir = tmp_path / "testdir" # It is deliberately never created.
147-
148-
# See comments in test_wraps_perm_error_if_enabled for details about patching.
149-
mocker.patch.object(sys.modules["git.util"], "HIDE_WINDOWS_KNOWN_ERRORS", hide_windows_known_errors)
150-
mocker.patch.object(os, "chmod")
151-
mocker.patch.object(pathlib.Path, "chmod")
182+
self._patch_for_wrapping_test(mocker, hide_windows_known_errors)
152183

153184
with pytest.raises(FileNotFoundError):
154185
try:

0 commit comments

Comments
 (0)