Skip to content

Commit 2a3839b

Browse files
committed
Minor refactor, improved test coverage
1 parent 81808f3 commit 2a3839b

File tree

3 files changed

+63
-65
lines changed

3 files changed

+63
-65
lines changed

src/manage/aliasutils.py

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22

3-
from .fsutils import ensure_tree, unlink
3+
from .exceptions import FilesInUseError
4+
from .fsutils import atomic_unlink, ensure_tree, unlink
45
from .logging import LOGGER
56
from .pathutils import Path
67
from .tagutils import install_matches_any
@@ -165,24 +166,6 @@ def create_alias(cmd, install, alias, target, *, script_code=None, _link=os.link
165166
LOGGER.info("Failed to remove %s.", p_script, exc_info=True)
166167

167168

168-
def cleanup_alias(cmd):
169-
if not cmd.global_dir or not cmd.global_dir.is_dir():
170-
return
171-
172-
alias_written = cmd.scratch.get("aliasutils.create_alias.alias_written") or ()
173-
174-
for alias in cmd.global_dir.glob("*.exe"):
175-
target = alias.with_name(alias.name + ".__target__")
176-
script = alias.with_name(alias.name + ".__script__.py")
177-
if alias.stem.casefold() not in alias_written:
178-
LOGGER.debug("Unlink %s", alias)
179-
unlink(alias, f"Attempting to remove {alias} is taking some time. " +
180-
"Ensure it is not is use, and please continue to wait " +
181-
"or press Ctrl+C to abort.")
182-
unlink(target)
183-
unlink(script)
184-
185-
186169
def _parse_entrypoint_line(line):
187170
line = line.partition("#")[0]
188171
name, sep, rest = line.partition("=")
@@ -283,32 +266,27 @@ def scan_and_create_entrypoints(cmd, install, shortcut, _create_alias=create_ali
283266
_create_alias(cmd, install, alias, target, script_code=code)
284267

285268

286-
def cleanup_entrypoints(cmd, install_shortcut_pairs):
287-
seen_names = set()
288-
for install, shortcut in install_shortcut_pairs:
289-
for alias, code in _scan(install["prefix"], shortcut.get("dirs")):
290-
seen_names.add(alias["name"].casefold())
269+
def cleanup_alias(cmd, site_dirs_written, *, _unlink_many=atomic_unlink, _scan=_scan):
270+
if not cmd.global_dir or not cmd.global_dir.is_dir():
271+
return
291272

292-
# Scan existing aliases
293-
scripts = cmd.global_dir.glob("*-script.py")
273+
expected = set()
274+
for i in cmd.get_installs():
275+
expected.update(a.get("name", "").casefold() for a in i.get("alias", ()))
294276

295-
# Excluding any in seen_names, delete unused aliases
296-
for script in scripts:
297-
name = script.name.rpartition("-")[0]
298-
if name.casefold() in seen_names:
299-
continue
277+
for i, s in site_dirs_written or ():
278+
for alias, code in _scan(i["prefix"], s.get("dirs")):
279+
expected.add(alias.get("name", "").casefold())
300280

301-
alias = cmd.global_dir / (name + ".exe")
302-
if not alias.is_file():
281+
for alias in cmd.global_dir.glob("*.exe"):
282+
if alias.stem.casefold() in expected or alias.name.casefold() in expected:
303283
continue
304-
305-
try:
306-
unlink(alias)
307-
LOGGER.debug("Deleted %s", alias)
308-
except OSError:
309-
LOGGER.warn("Failed to delete %s", alias)
284+
target = alias.with_name(alias.name + ".__target__")
285+
script = alias.with_name(alias.name + ".__script__.py")
286+
LOGGER.debug("Unlink %s", alias)
310287
try:
311-
unlink(script)
312-
LOGGER.debug("Deleted %s", script)
313-
except OSError:
314-
LOGGER.warn("Failed to delete %s", script)
288+
_unlink_many([alias, target, script])
289+
except (OSError, FilesInUseError):
290+
LOGGER.warn("Failed to remove %s. Ensure it is not in use and run "
291+
"py install --refresh to try again.", alias.name)
292+
LOGGER.debug("TRACEBACK", exc_info=True)

src/manage/install_command.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ def _create_entrypoints(cmd, install, shortcut):
254254

255255

256256
def _cleanup_entrypoints(cmd, install_shortcut_pairs):
257-
from .aliasutils import cleanup_entrypoints
258-
cleanup_entrypoints(cmd, install_shortcut_pairs)
257+
# Entry point aliases are cleaned up with regular aliases
258+
pass
259259

260260

261261
SHORTCUT_HANDLERS = {
@@ -271,6 +271,7 @@ def update_all_shortcuts(cmd, *, _create_alias=None, _cleanup_alias=None):
271271
from .aliasutils import create_alias as _create_alias
272272
if not _cleanup_alias:
273273
from .aliasutils import cleanup_alias as _cleanup_alias
274+
from .aliasutils import get_site_dirs
274275

275276
LOGGER.debug("Updating global shortcuts")
276277
shortcut_written = {}
@@ -309,7 +310,7 @@ def update_all_shortcuts(cmd, *, _create_alias=None, _cleanup_alias=None):
309310
create(cmd, i, s)
310311
shortcut_written.setdefault(s["kind"], []).append((i, s))
311312

312-
# Earlier releases may not have site_dirs. If not, assume
313+
# Earlier releases may not have site_dirs. If not, assume defaults
313314
if ("site-dirs" in (cmd.enable_shortcut_kinds or ("site-dirs",)) and
314315
"site-dirs" not in (cmd.disable_shortcut_kinds or ()) and
315316
all(s["kind"] != "site-dirs" for s in i.get("shortcuts", ()))):
@@ -319,11 +320,11 @@ def update_all_shortcuts(cmd, *, _create_alias=None, _cleanup_alias=None):
319320
create(cmd, i, s)
320321
shortcut_written.setdefault("site-dirs", []).append((i, s))
321322

322-
_cleanup_alias(cmd)
323-
324323
for k, (_, cleanup) in SHORTCUT_HANDLERS.items():
325324
cleanup(cmd, shortcut_written.get(k, []))
326325

326+
_cleanup_alias(cmd, shortcut_written.get("site-dirs", []))
327+
327328

328329
def print_cli_shortcuts(cmd):
329330
if cmd.global_dir and cmd.global_dir.is_dir() and any(cmd.global_dir.glob("*.exe")):

tests/test_alias.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -354,22 +354,41 @@ def test_scan_entrypoints(tmp_path):
354354

355355

356356
def test_cleanup_aliases(fake_config):
357+
fake_config.installs = [
358+
dict(id="A", alias=[dict(name="A", target="a.exe")], prefix=fake_config.global_dir),
359+
]
360+
361+
def fake_scan(*a):
362+
yield dict(name="B"), "CODE"
363+
364+
# install/shortcut pairs are irrelevant, since we fake the scan entirely.
365+
# It just can't be empty or the scan is skipped.
366+
pairs = [
367+
(fake_config.installs[0], dict(kind="site-dirs", dirs=[])),
368+
]
369+
357370
root = fake_config.global_dir
358371
root.mkdir(parents=True, exist_ok=True)
359-
(root / "alias1.exe").write_bytes(b"")
360-
(root / "alias1.exe.__target__").write_bytes(b"")
361-
(root / "alias1.exe.__script__.py").write_bytes(b"")
362-
(root / "alias2.exe").write_bytes(b"")
363-
(root / "alias2.exe.__target__").write_bytes(b"")
364-
(root / "alias2.exe.__script__.py").write_bytes(b"")
365-
(root / "alias3.exe").write_bytes(b"")
366-
(root / "alias3.exe.__target__").write_bytes(b"")
367-
fake_config.scratch["aliasutils.create_alias.alias_written"] = set([
368-
"alias1".casefold(),
369-
"alias3".casefold(),
370-
])
371-
AU.cleanup_alias(fake_config)
372-
assert set(f.name for f in root.glob("*")) == set([
373-
"alias1.exe", "alias1.exe.__target__", "alias1.exe.__script__.py",
374-
"alias3.exe", "alias3.exe.__target__",
375-
])
372+
files = ["A.exe", "A.exe.__target__",
373+
"B.exe", "B.exe.__script__.py", "B.exe.__target__",
374+
"C.exe", "C.exe.__script__.py", "C.exe.__target__"]
375+
for f in files:
376+
(root / f).write_bytes(b"")
377+
378+
# Ensure the expect files get requested to be unlinked
379+
class Unlinker(list):
380+
def __call__(self, names):
381+
self.extend(names)
382+
383+
unlinked = Unlinker()
384+
AU.cleanup_alias(fake_config, pairs, _unlink_many=unlinked, _scan=fake_scan)
385+
assert set(f.name for f in unlinked) == set(["C.exe", "C.exe.__script__.py", "C.exe.__target__"])
386+
387+
# Ensure we don't break if unlinking fails
388+
def unlink2(names):
389+
raise PermissionError("Simulated error")
390+
AU.cleanup_alias(fake_config, pairs, _unlink_many=unlink2, _scan=fake_scan)
391+
392+
# Ensure the actual unlink works
393+
AU.cleanup_alias(fake_config, pairs, _scan=fake_scan)
394+
assert set(f.name for f in root.glob("*")) == set(files[:-3])

0 commit comments

Comments
 (0)