Skip to content

Commit 768deb8

Browse files
committed
Slightly improved logic, significantly improve logging
1 parent 624ffb6 commit 768deb8

File tree

4 files changed

+76
-26
lines changed

4 files changed

+76
-26
lines changed

src/manage/aliasutils.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .exceptions import FilesInUseError, NoLauncherTemplateError
44
from .fsutils import atomic_unlink, ensure_tree, unlink
55
from .logging import LOGGER
6-
from .pathutils import Path
6+
from .pathutils import Path, relative_to
77
from .tagutils import install_matches_any
88

99
_EXE = ".exe".casefold()
@@ -105,16 +105,18 @@ def _create_alias(
105105
if windowed:
106106
launcher = cmd.launcherw_exe or launcher
107107

108+
chosen_by = "default"
108109
if plat:
109-
LOGGER.debug("Checking for launcher for platform -%s", plat)
110110
launcher = _if_exists(launcher, f"-{plat}")
111+
chosen_by = "platform tag"
111112
if not launcher.is_file():
112-
LOGGER.debug("Checking for launcher for default platform %s", cmd.default_platform)
113113
launcher = _if_exists(launcher, cmd.default_platform)
114+
chosen_by = "default platform"
114115
if not launcher.is_file():
115-
LOGGER.debug("Checking for launcher for -64")
116116
launcher = _if_exists(launcher, "-64")
117-
LOGGER.debug("Create %s linking to %s using %s", name, target, launcher)
117+
chosen_by = "fallback default"
118+
LOGGER.debug("Create %s for %s using %s, chosen by %s", name,
119+
relative_to(target, cmd.install_dir), launcher, chosen_by)
118120
if not launcher or not launcher.is_file():
119121
raise NoLauncherTemplateError()
120122

@@ -128,8 +130,9 @@ def _create_alias(
128130
LOGGER.debug("Failed to read %s", launcher, exc_info=True)
129131
return
130132

133+
force = getattr(cmd, "force", False)
131134
existing_bytes = b''
132-
if getattr(cmd, "force", False):
135+
if force:
133136
# Only expect InstallCommand to have .force
134137
unlink(p)
135138
else:
@@ -146,8 +149,10 @@ def _create_alias(
146149
if existing_bytes != launcher_bytes and allow_link and _link:
147150
# Try to find an existing launcher we can hard-link
148151
launcher2 = launcher_remap.get(launcher.name)
149-
if not launcher2:
150-
# None known, so search existing files
152+
if (not launcher2 or not launcher2.is_file()) and not force:
153+
# None known, so search existing files. Or, user is forcing us, so
154+
# we only want to use an existing launcher if we've cached it this
155+
# session.
151156
try:
152157
LOGGER.debug("Searching %s for suitable launcher to link", cmd.global_dir)
153158
for p2 in cmd.global_dir.glob("*.exe"):
@@ -165,10 +170,11 @@ def _create_alias(
165170
except Exception:
166171
LOGGER.debug("Failed to find existing launcher", exc_info=True)
167172

168-
if launcher2:
173+
if launcher2 and launcher2.is_file():
169174
# We know that the target either doesn't exist or needs replacing
170175
unlink(p)
171176
try:
177+
LOGGER.debug("Creating %s as hard link to %s", p, launcher2)
172178
_link(launcher2, p)
173179
existing_bytes = launcher_bytes
174180
launcher_remap[launcher.name] = launcher2
@@ -256,13 +262,13 @@ def _readlines(path):
256262
return
257263

258264

259-
def _scan_one(install, root):
265+
def _scan_one(cmd, install, root):
260266
# Scan d for dist-info directories with entry_points.txt
261267
dist_info = [d for d in root.glob("*.dist-info") if d.is_dir()]
262268
entrypoints = [f for f in [d / "entry_points.txt" for d in dist_info] if f.is_file()]
263269
if len(entrypoints):
264270
LOGGER.debug("Found %i entry_points.txt files in %i dist-info in %s",
265-
len(entrypoints), len(dist_info), root)
271+
len(entrypoints), len(dist_info), relative_to(root, cmd.install_dir))
266272

267273
# Filter down to [console_scripts] and [gui_scripts]
268274
for ep in entrypoints:
@@ -281,10 +287,10 @@ def _scan_one(install, root):
281287
mod=mod, func=func, **alias)
282288

283289

284-
def _scan(install, prefix, dirs):
290+
def _scan(cmd, install, prefix, dirs):
285291
for dirname in dirs or ():
286292
root = prefix / dirname
287-
yield from _scan_one(install, root)
293+
yield from _scan_one(cmd, install, root)
288294

289295

290296
def calculate_aliases(cmd, install, *, _scan=_scan):
@@ -326,7 +332,7 @@ def calculate_aliases(cmd, install, *, _scan=_scan):
326332
site_dirs = s.get("dirs", ())
327333
break
328334

329-
for ai in _scan(install, prefix, site_dirs):
335+
for ai in _scan(cmd, install, prefix, site_dirs):
330336
if ai.windowed and default_alias_w:
331337
yield ai.replace(target=default_alias_w.target)
332338
elif not ai.windowed and default_alias:

src/manage/pathutils.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
77
import os
88

99

10+
def _eq(x, y):
11+
return x == y or x.casefold() == y.casefold()
12+
13+
1014
class PurePath:
1115
def __init__(self, *parts):
1216
total = ""
1317
for p in parts:
1418
try:
1519
p = p.__fspath__().replace("/", "\\")
1620
except AttributeError:
17-
p = str(p).replace("/", "\\")
21+
p = os.fsdecode(p).replace("/", "\\")
1822
p = p.replace("\\\\", "\\")
1923
if p == ".":
2024
continue
@@ -24,8 +28,11 @@ def __init__(self, *parts):
2428
total += "\\" + p
2529
else:
2630
total += p
27-
self._parent, _, self.name = total.rpartition("\\")
28-
self._p = total.rstrip("\\")
31+
drive, root, tail = os.path.splitroot(total)
32+
parent, _, name = tail.rpartition("\\")
33+
self._parent = drive + root + parent
34+
self.name = name
35+
self._p = drive + root + tail.rstrip("\\")
2936

3037
def __fspath__(self):
3138
return self._p
@@ -36,6 +43,9 @@ def __repr__(self):
3643
def __str__(self):
3744
return self._p
3845

46+
def __bytes__(self):
47+
return os.fsencode(self)
48+
3949
def __hash__(self):
4050
return hash(self._p.casefold())
4151

@@ -86,13 +96,13 @@ def __truediv__(self, other):
8696

8797
def __eq__(self, other):
8898
if isinstance(other, PurePath):
89-
return self._p.casefold() == other._p.casefold()
90-
return self._p.casefold() == str(other).casefold()
99+
return _eq(self._p, other._p)
100+
return _eq(self._p == str(other))
91101

92102
def __ne__(self, other):
93103
if isinstance(other, PurePath):
94-
return self._p.casefold() != other._p.casefold()
95-
return self._p.casefold() != str(other).casefold()
104+
return not _eq(self._p, other._p)
105+
return not _eq(self._p, str(other))
96106

97107
def with_name(self, name):
98108
return type(self)(os.path.join(self._parent, name))
@@ -105,7 +115,7 @@ def with_suffix(self, suffix):
105115
def relative_to(self, base):
106116
base = PurePath(base).parts
107117
parts = self.parts
108-
if not all(x.casefold() == y.casefold() for x, y in zip(base, parts)):
118+
if not all(_eq(x, y) for x, y in zip(base, parts)):
109119
raise ValueError("path not relative to base")
110120
return type(self)("\\".join(parts[len(base):]))
111121

@@ -128,7 +138,7 @@ def match(self, pattern, full_match=False):
128138
m = m.casefold()
129139

130140
if "*" not in p:
131-
return m.casefold() == p
141+
return m == p or m.casefold() == p
132142

133143
must_start_with = True
134144
for bit in p.split("*"):
@@ -219,3 +229,18 @@ def write_bytes(self, data):
219229
def write_text(self, text, encoding="utf-8", errors="strict"):
220230
with open(self._p, "w", encoding=encoding, errors=errors) as f:
221231
f.write(text)
232+
233+
234+
def relative_to(path, root):
235+
if not root:
236+
return path
237+
parts_1 = list(PurePath(path).parts)
238+
parts_2 = list(PurePath(root).parts)
239+
while parts_1 and parts_2 and _eq(parts_1[0], parts_2[0]):
240+
parts_1.pop(0)
241+
parts_2.pop(0)
242+
if parts_1 and not parts_2:
243+
if isinstance(path, PurePath):
244+
return type(path)(*parts_1)
245+
return type(path)(PurePath(*parts_1))
246+
return path

src/manage/startutils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .fsutils import rmtree, unlink
44
from .logging import LOGGER
5-
from .pathutils import Path
5+
from .pathutils import Path, relative_to
66
from .tagutils import install_matches_any
77

88

@@ -36,7 +36,7 @@ def _make(root, prefix, item, allow_warn=True):
3636

3737
lnk = root / (n + ".lnk")
3838
target = _unprefix(item["Target"], prefix)
39-
LOGGER.debug("Creating shortcut %s to %s", lnk, target)
39+
LOGGER.debug("Creating shortcut %s to %s", relative_to(lnk, root), target)
4040
try:
4141
lnk.relative_to(root)
4242
except ValueError:

tests/test_pathutils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from manage.pathutils import Path, PurePath
3+
from manage.pathutils import Path, PurePath, relative_to
44

55
def test_path_match():
66
p = Path("python3.12.exe")
@@ -30,3 +30,22 @@ def test_path_stem():
3030
p = Path(".exe")
3131
assert p.stem == ""
3232
assert p.suffix == ".exe"
33+
34+
35+
def test_path_relative_to():
36+
p = Path(r"C:\A\B\C\python.exe")
37+
actual = relative_to(p, r"C:\A\B\C")
38+
assert isinstance(actual, Path)
39+
assert str(actual) == "python.exe"
40+
actual = relative_to(p, "C:\\")
41+
assert isinstance(actual, Path)
42+
assert str(actual) == r"A\B\C\python.exe"
43+
actual = relative_to(str(p), r"C:\A\B")
44+
assert isinstance(actual, str)
45+
assert actual == r"C\python.exe"
46+
actual = relative_to(bytes(p), r"C:\A\B")
47+
assert isinstance(actual, bytes)
48+
assert actual == rb"C\python.exe"
49+
50+
assert relative_to(p, r"C:\A\B\C\D") is p
51+
assert relative_to(p, None) is p

0 commit comments

Comments
 (0)