Skip to content

Commit 9ca0ebe

Browse files
committed
Makes uninstall --purge also remove PATH entry.
Fixes #95
1 parent e3b6475 commit 9ca0ebe

File tree

3 files changed

+102
-25
lines changed

3 files changed

+102
-25
lines changed

src/manage/uninstall_command.py

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,70 @@
33
from .installs import get_matching_install_tags
44
from .install_command import SHORTCUT_HANDLERS, update_all_shortcuts
55
from .logging import LOGGER
6-
from .pathutils import PurePath
6+
from .pathutils import Path, PurePath
77
from .tagutils import tag_or_range
88

99

1010
def _iterdir(p, only_files=False):
1111
try:
1212
if only_files:
13-
return [f for f in p.iterdir() if p.is_file()]
14-
return list(p.iterdir())
13+
return [f for f in Path(p).iterdir() if f.is_file()]
14+
return list(Path(p).iterdir())
1515
except FileNotFoundError:
1616
LOGGER.debug("Skipping %s because it does not exist", p)
1717
return []
1818

1919

20+
def _do_purge_global_dir(global_dir, warn_msg, *, hive=None, subkey="Environment"):
21+
import os
22+
import winreg
23+
24+
if hive is None:
25+
hive = winreg.HKEY_CURRENT_USER
26+
try:
27+
with winreg.OpenKeyEx(hive, subkey) as key:
28+
path, kind = winreg.QueryValueEx(key, "Path")
29+
if kind not in (winreg.REG_SZ, winreg.REG_EXPAND_SZ):
30+
raise ValueError("Value kind is not a string")
31+
except (OSError, ValueError):
32+
LOGGER.debug("Not removing global commands directory from PATH", exc_info=True)
33+
else:
34+
LOGGER.debug("Current PATH contains %s", path)
35+
paths = path.split(";")
36+
newpaths = []
37+
for p in paths:
38+
ep = os.path.expandvars(p) if kind == winreg.REG_EXPAND_SZ else p
39+
if PurePath(ep).match(global_dir):
40+
LOGGER.debug("Removing from PATH: %s", p)
41+
else:
42+
newpaths.append(p)
43+
if len(newpaths) < len(paths):
44+
newpath = ";".join(newpaths)
45+
with winreg.CreateKeyEx(hive, subkey, access=winreg.KEY_READ|winreg.KEY_WRITE) as key:
46+
path2, kind2 = winreg.QueryValueEx(key, "Path")
47+
if path2 == path and kind2 == kind:
48+
LOGGER.info("Removing global commands directory from PATH")
49+
LOGGER.debug("New PATH contains %s", newpath)
50+
winreg.SetValueEx(key, "Path", 0, kind, newpath)
51+
else:
52+
LOGGER.debug("Not removing global commands directory from PATH "
53+
"because the registry changed while processing.")
54+
55+
try:
56+
from _native import broadcast_settings_change
57+
broadcast_settings_change()
58+
except (ImportError, OSError):
59+
LOGGER.debug("Did not broadcast settings change notification",
60+
exc_info=True)
61+
62+
if not global_dir.is_dir():
63+
return
64+
LOGGER.info("Purging global commands from %s", global_dir)
65+
for f in _iterdir(global_dir):
66+
LOGGER.debug("Purging %s", f)
67+
rmtree(f, after_5s_warning=warn_msg)
68+
69+
2070
def execute(cmd):
2171
LOGGER.debug("BEGIN uninstall_command.execute: %r", cmd.args)
2272

@@ -31,28 +81,28 @@ def execute(cmd):
3181
cmd.tags = []
3282

3383
if cmd.purge:
34-
if cmd.ask_yn("Uninstall all runtimes?"):
35-
for i in installed:
36-
LOGGER.info("Purging %s from %s", i["display-name"], i["prefix"])
37-
try:
38-
rmtree(
39-
i["prefix"],
40-
after_5s_warning=warn_msg.format(i["display-name"]),
41-
remove_ext_first=("exe", "dll", "json")
42-
)
43-
except FilesInUseError:
44-
LOGGER.warn("Unable to purge %s because it is still in use.",
45-
i["display-name"])
46-
continue
47-
LOGGER.info("Purging saved downloads from %s", cmd.download_dir)
48-
rmtree(cmd.download_dir, after_5s_warning=warn_msg.format("cached downloads"))
49-
LOGGER.info("Purging global commands from %s", cmd.global_dir)
50-
for f in _iterdir(cmd.global_dir):
51-
LOGGER.debug("Purging %s", f)
52-
rmtree(f, after_5s_warning=warn_msg.format("global commands"))
53-
LOGGER.info("Purging all shortcuts")
54-
for _, cleanup in SHORTCUT_HANDLERS.values():
55-
cleanup(cmd, [])
84+
if not cmd.ask_yn("Uninstall all runtimes?"):
85+
LOGGER.debug("END uninstall_command.execute")
86+
return
87+
for i in installed:
88+
LOGGER.info("Purging %s from %s", i["display-name"], i["prefix"])
89+
try:
90+
rmtree(
91+
i["prefix"],
92+
after_5s_warning=warn_msg.format(i["display-name"]),
93+
remove_ext_first=("exe", "dll", "json")
94+
)
95+
except FilesInUseError:
96+
LOGGER.warn("Unable to purge %s because it is still in use.",
97+
i["display-name"])
98+
continue
99+
LOGGER.info("Purging saved downloads from %s", cmd.download_dir)
100+
rmtree(cmd.download_dir, after_5s_warning=warn_msg.format("cached downloads"))
101+
# Purge global commands directory
102+
_do_purge_global_dir(cmd.global_dir, warn_msg.format("global commands"))
103+
LOGGER.info("Purging all shortcuts")
104+
for _, cleanup in SHORTCUT_HANDLERS.values():
105+
cleanup(cmd, [])
56106
LOGGER.debug("END uninstall_command.execute")
57107
return
58108

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ def setup(self, _subkey=None, **keys):
205205
else:
206206
raise TypeError("unsupported type in registry")
207207

208+
def getvalue(self, subkey, valuename):
209+
with winreg.OpenKeyEx(self.key, subkey) as key:
210+
return winreg.QueryValueEx(key, valuename)[0]
211+
212+
def getvalueandkind(self, subkey, valuename):
213+
with winreg.OpenKeyEx(self.key, subkey) as key:
214+
return winreg.QueryValueEx(key, valuename)
215+
208216

209217
@pytest.fixture(scope='function')
210218
def registry():

tests/test_uninstall_command.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
import pytest
3+
import winreg
4+
5+
from pathlib import Path
6+
7+
from manage import uninstall_command as UC
8+
9+
10+
def test_purge_global_dir(monkeypatch, registry, tmp_path):
11+
registry.setup(Path=rf"C:\A;{tmp_path}\X;{tmp_path};C:\B;%PTH%;C:\%D%\E")
12+
(tmp_path / "test.txt").write_bytes(b"")
13+
(tmp_path / "test2.txt").write_bytes(b"")
14+
15+
monkeypatch.setitem(os.environ, "PTH", str(tmp_path))
16+
UC._do_purge_global_dir(tmp_path, "SLOW WARNING", hive=registry.hive, subkey=registry.root)
17+
assert registry.getvalueandkind("", "Path") == (
18+
rf"C:\A;{tmp_path}\X;C:\B;%PTH%;C:\%D%\E", winreg.REG_SZ)
19+
assert not list(tmp_path.iterdir())

0 commit comments

Comments
 (0)