Skip to content

Commit 0c2dd81

Browse files
authored
Improves alias list messages after installation. (#92)
1 parent 34add60 commit 0c2dd81

File tree

9 files changed

+195
-58
lines changed

9 files changed

+195
-58
lines changed

src/manage/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,13 @@ def load_registry_config(key_path, schema):
131131
"This is very unexpected. Please check your configuration " +
132132
"or report an issue at https://github.com/python/pymanager.",
133133
key_path)
134-
resolve_config(cfg, key_path, _global_file().parent, schema=schema, error_unknown=True)
134+
135+
try:
136+
from _native import package_get_root
137+
root = Path(package_get_root())
138+
except ImportError:
139+
root = Path(sys.executable).parent
140+
resolve_config(cfg, key_path, root, schema=schema, error_unknown=True)
135141
return cfg
136142

137143

src/manage/install_command.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111
from .fsutils import ensure_tree, rmtree, unlink
1212
from .indexutils import Index
13-
from .logging import CONSOLE_MAX_WIDTH, LOGGER, ProgressPrinter
13+
from .logging import CONSOLE_MAX_WIDTH, LOGGER, ProgressPrinter, VERBOSE
1414
from .pathutils import Path, PurePath
1515
from .tagutils import install_matches_any, tag_or_range
1616
from .urlutils import (
@@ -286,7 +286,7 @@ def _cleanup_arp_entries(cmd, install_shortcut_pairs):
286286
}
287287

288288

289-
def update_all_shortcuts(cmd, path_warning=True):
289+
def update_all_shortcuts(cmd):
290290
LOGGER.debug("Updating global shortcuts")
291291
alias_written = set()
292292
shortcut_written = {}
@@ -329,34 +329,48 @@ def update_all_shortcuts(cmd, path_warning=True):
329329
for k, (_, cleanup) in SHORTCUT_HANDLERS.items():
330330
cleanup(cmd, shortcut_written.get(k, []))
331331

332-
if path_warning and cmd.global_dir and cmd.global_dir.is_dir() and any(cmd.global_dir.glob("*.exe")):
332+
333+
def print_cli_shortcuts(cmd):
334+
if cmd.global_dir and cmd.global_dir.is_dir() and any(cmd.global_dir.glob("*.exe")):
333335
try:
334336
if not any(cmd.global_dir.match(p) for p in os.getenv("PATH", "").split(os.pathsep) if p):
335337
LOGGER.info("")
336338
LOGGER.info("!B!Global shortcuts directory is not on PATH. " +
337-
"Add it for easy access to global Python commands.!W!")
339+
"Add it for easy access to global Python aliases.!W!")
338340
LOGGER.info("!B!Directory to add: !Y!%s!W!", cmd.global_dir)
339341
LOGGER.info("")
342+
return
340343
except Exception:
341344
LOGGER.debug("Failed to display PATH warning", exc_info=True)
345+
return
342346

343-
344-
def print_cli_shortcuts(cmd):
347+
from .installs import get_install_alias_names
345348
installs = cmd.get_installs()
346-
seen = set()
349+
tags = getattr(cmd, "tags", None)
350+
seen = {"python.exe".casefold()}
351+
verbose = LOGGER.would_log_to_console(VERBOSE)
347352
for i in installs:
348-
aliases = sorted(a["name"] for a in i["alias"] if a["name"].casefold() not in seen)
349-
seen.update(n.casefold() for n in aliases)
350-
if not install_matches_any(i, cmd.tags):
353+
# We need to pre-filter aliases before getting the nice names.
354+
aliases = [a for a in i.get("alias", ()) if a["name"].casefold() not in seen]
355+
seen.update(n["name"].casefold() for n in aliases)
356+
if not verbose:
357+
if i.get("default"):
358+
LOGGER.debug("%s will be launched by !G!python.exe!W!", i["display-name"])
359+
names = get_install_alias_names(aliases, windowed=True)
360+
LOGGER.debug("%s will be launched by %s", i["display-name"], ", ".join(names))
361+
362+
if tags and not install_matches_any(i, cmd.tags):
351363
continue
352-
if i.get("default") and aliases:
364+
365+
names = get_install_alias_names(aliases, windowed=False)
366+
if i.get("default") and names:
353367
LOGGER.info("%s will be launched by !G!python.exe!W! and also %s",
354-
i["display-name"], ", ".join(aliases))
368+
i["display-name"], ", ".join(names))
355369
elif i.get("default"):
356370
LOGGER.info("%s will be launched by !G!python.exe!W!.", i["display-name"])
357-
elif aliases:
371+
elif names:
358372
LOGGER.info("%s will be launched by %s",
359-
i["display-name"], ", ".join(aliases))
373+
i["display-name"], ", ".join(names))
360374
else:
361375
LOGGER.info("Installed %s to %s", i["display-name"], i["prefix"])
362376

@@ -545,6 +559,7 @@ def execute(cmd):
545559
else:
546560
LOGGER.info("Refreshing install registrations.")
547561
update_all_shortcuts(cmd)
562+
print_cli_shortcuts(cmd)
548563
LOGGER.debug("END install_command.execute")
549564
return
550565

src/manage/installs.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .exceptions import NoInstallFoundError, NoInstallsError
44
from .logging import DEBUG, LOGGER
55
from .pathutils import Path
6-
from .tagutils import CompanyTag, tag_or_range, companies_match
6+
from .tagutils import CompanyTag, tag_or_range, companies_match, split_platform
77
from .verutils import Version
88

99

@@ -120,6 +120,79 @@ def get_installs(
120120
return installs
121121

122122

123+
def _make_alias_key(alias):
124+
n1, sep, n3 = alias.rpartition(".")
125+
n2 = ""
126+
n3 = sep + n3
127+
128+
n1, plat = split_platform(n1)
129+
130+
while n1 and n1[-1] in "0123456789.-":
131+
n2 = n1[-1] + n2
132+
n1 = n1[:-1]
133+
134+
if n1 and n1[-1].casefold() == "w".casefold():
135+
w = "w"
136+
n1 = n1[:-1]
137+
else:
138+
w = ""
139+
140+
return n1, w, n2, plat, n3
141+
142+
143+
def _make_opt_part(parts):
144+
if not parts:
145+
return ""
146+
if len(parts) == 1:
147+
return list(parts)[0]
148+
return "[{}]".format("|".join(sorted(p for p in parts if p)))
149+
150+
151+
def _sk_sub(m):
152+
n = m.group(1)
153+
if not n:
154+
return ""
155+
if n in "[]":
156+
return ""
157+
try:
158+
return f"{int(n):020}"
159+
except ValueError:
160+
pass
161+
return n
162+
163+
164+
def _make_alias_name_sortkey(n):
165+
import re
166+
return re.sub(r"(\d+|\[|\])", _sk_sub, n)
167+
168+
169+
def get_install_alias_names(aliases, friendly=True, windowed=True):
170+
if not windowed:
171+
aliases = [a for a in aliases if not a.get("windowed")]
172+
if not friendly:
173+
return sorted(a["name"] for a in aliases)
174+
175+
seen = {}
176+
has_w = {}
177+
plats = {}
178+
for n1, w, n2, plat, n3 in (_make_alias_key(a["name"]) for a in aliases):
179+
k = n1.casefold(), n2.casefold(), n3.casefold()
180+
seen.setdefault(k, (n1, n2, n3))
181+
has_w.setdefault(k, set()).add(w)
182+
plats.setdefault(k, set()).add(plat)
183+
184+
result = []
185+
for k, (n1, n2, n3) in seen.items():
186+
result.append("".join([
187+
n1,
188+
_make_opt_part(has_w.get(k)),
189+
n2,
190+
_make_opt_part(plats.get(k)),
191+
n3,
192+
]))
193+
return sorted(result, key=_make_alias_name_sortkey)
194+
195+
123196
def _patch_install_to_run(i, run_for):
124197
return {
125198
**i,

src/manage/list_command.py

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,14 @@
77
LOGGER = logging.LOGGER
88

99

10-
def _exe_partition(n):
11-
n1, sep, n2 = n.rpartition(".")
12-
n2 = sep + n2
13-
while n1 and n1[-1] in "0123456789.-":
14-
n2 = n1[-1] + n2
15-
n1 = n1[:-1]
16-
w = ""
17-
if n1 and n1[-1] == "w":
18-
w = "w"
19-
n1 = n1[:-1]
20-
return n1, w, n2
21-
22-
2310
def _format_alias(i, seen):
24-
try:
25-
alias = i["alias"]
26-
except KeyError:
27-
return ""
28-
if not alias:
29-
return ""
30-
if len(alias) == 1:
31-
a = i["alias"][0]
32-
n = a["name"].casefold()
33-
if n in seen:
34-
return ""
35-
seen.add(n)
36-
return i["alias"][0]["name"]
37-
names = {_exe_partition(a["name"].casefold()): a["name"] for a in alias
38-
if a["name"].casefold() not in seen}
39-
seen.update(a["name"].casefold() for a in alias)
40-
for n1, w, n2 in list(names):
41-
k = (n1, "", n2)
42-
if w and k in names:
43-
del names[n1, w, n2]
44-
n1, _, n2 = _exe_partition(names[k])
45-
names[k] = f"{n1}[w]{n2}"
46-
return ", ".join(names[n] for n in sorted(names))
11+
from manage.installs import get_install_alias_names
12+
aliases = [a for a in i.get("alias", ()) if a["name"].casefold() not in seen]
13+
seen.update(a["name"].casefold() for a in aliases)
14+
15+
include_w = LOGGER.would_log_to_console(logging.VERBOSE)
16+
names = get_install_alias_names(aliases, windowed=include_w)
17+
return ", ".join(names)
4718

4819

4920
def _format_tag_with_co(cmd, i):

src/manage/tagutils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def __lt__(self, other):
123123
return self.sortkey > other.sortkey
124124

125125

126-
def _split_platform(tag):
126+
def split_platform(tag):
127127
if tag.endswith(SUPPORTED_PLATFORM_SUFFIXES):
128128
for t in SUPPORTED_PLATFORM_SUFFIXES:
129129
if tag.endswith(t):
@@ -178,7 +178,7 @@ def __init__(self, company_or_tag, tag=None, *, loose_company=True):
178178
else:
179179
assert isinstance(company_or_tag, _CompanyKey)
180180
self._company = company_or_tag
181-
self.tag, self.platform = _split_platform(tag)
181+
self.tag, self.platform = split_platform(tag)
182182
self._sortkey = _sort_tag(self.tag)
183183

184184
@property

src/manage/uninstall_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,6 @@ def execute(cmd):
127127
LOGGER.debug("TRACEBACK:", exc_info=True)
128128

129129
if to_uninstall:
130-
update_all_shortcuts(cmd, path_warning=False)
130+
update_all_shortcuts(cmd)
131131

132132
LOGGER.debug("END uninstall_command.execute")

tests/conftest.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def make_install(tag, **kwargs):
201201
run_for.append({"tag": t, "target": kwargs.get("target", "python.exe")})
202202
run_for.append({"tag": t, "target": kwargs.get("targetw", "pythonw.exe"), "windowed": 1})
203203

204-
return {
204+
i = {
205205
"company": kwargs.get("company", "PythonCore"),
206206
"id": "{}-{}".format(kwargs.get("company", "PythonCore"), tag),
207207
"sort-version": kwargs.get("sort_version", tag),
@@ -212,12 +212,17 @@ def make_install(tag, **kwargs):
212212
"prefix": PurePath(kwargs.get("prefix", rf"C:\{tag}")),
213213
"executable": kwargs.get("executable", "python.exe"),
214214
}
215+
try:
216+
i["alias"] = kwargs["alias"]
217+
except LookupError:
218+
pass
219+
return i
215220

216221

217222
def fake_get_installs(install_dir):
218223
yield make_install("1.0")
219-
yield make_install("1.0-32", sort_version="1.0")
220-
yield make_install("1.0-64", sort_version="1.0")
224+
yield make_install("1.0-32", sort_version="1.0", alias=[dict(name="py1.0.exe"), dict(name="py1.0-32.exe")])
225+
yield make_install("1.0-64", sort_version="1.0", alias=[dict(name="py1.0.exe"), dict(name="py1.0-64.exe")])
221226
yield make_install("2.0-64", sort_version="2.0")
222227
yield make_install("2.0-arm64", sort_version="2.0")
223228
yield make_install("3.0a1-32", sort_version="3.0a1")

tests/test_install_command.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
import os
12
import pytest
23
import secrets
4+
from pathlib import Path, PurePath
5+
36
from manage import install_command as IC
7+
from manage import installs
48

59

610
@pytest.fixture
711
def alias_checker(tmp_path):
812
with AliasChecker(tmp_path) as checker:
913
yield checker
1014

15+
1116
class AliasChecker:
1217
class Cmd:
1318
global_dir = "out"
@@ -95,3 +100,34 @@ def test_write_alias_default_platform(alias_checker):
95100
def test_write_alias_fallback_platform(alias_checker):
96101
alias_checker.check_64(alias_checker.Cmd("-spam"), "1.0", "testA")
97102
alias_checker.check_w64(alias_checker.Cmd("-spam"), "1.0", "testB")
103+
104+
105+
def test_print_cli_shortcuts(patched_installs, assert_log, monkeypatch, tmp_path):
106+
class Cmd:
107+
global_dir = Path(tmp_path)
108+
def get_installs(self):
109+
return installs.get_installs(None)
110+
111+
(tmp_path / "fake.exe").write_bytes(b"")
112+
113+
monkeypatch.setitem(os.environ, "PATH", f"{os.environ['PATH']};{Cmd.global_dir}")
114+
IC.print_cli_shortcuts(Cmd())
115+
assert_log(
116+
assert_log.skip_until("Installed %s", ["Python 2.0-64", PurePath("C:\\2.0-64")]),
117+
assert_log.skip_until("%s will be launched by %s", ["Python 1.0-64", "py1.0[-64].exe"]),
118+
("%s will be launched by %s", ["Python 1.0-32", "py1.0-32.exe"]),
119+
)
120+
121+
122+
def test_print_path_warning(patched_installs, assert_log, tmp_path):
123+
class Cmd:
124+
global_dir = Path(tmp_path)
125+
def get_installs(self):
126+
return installs.get_installs(None)
127+
128+
(tmp_path / "fake.exe").write_bytes(b"")
129+
130+
IC.print_cli_shortcuts(Cmd())
131+
assert_log(
132+
assert_log.skip_until(".*Global shortcuts directory is not on PATH")
133+
)

tests/test_installs.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,34 @@ def test_get_install_to_run_with_range(patched_installs):
126126
i = installs.get_install_to_run("<none>", None, ">1.0")
127127
assert i["id"] == "PythonCore-2.0-64"
128128
assert i["executable"].match("python.exe")
129+
130+
131+
def test_install_alias_make_alias_sortkey():
132+
assert ("pythonw00000000000000000003-00000000000000000064.exe"
133+
== installs._make_alias_name_sortkey("pythonw3-64.exe"))
134+
assert ("pythonw00000000000000000003-00000000000000000064.exe"
135+
== installs._make_alias_name_sortkey("python[w]3[-64].exe"))
136+
137+
def test_install_alias_make_alias_key():
138+
assert ("python", "w", "3", "-64", ".exe") == installs._make_alias_key("pythonw3-64.exe")
139+
assert ("python", "w", "3", "", ".exe") == installs._make_alias_key("pythonw3.exe")
140+
assert ("pythonw3-xyz", "", "", "", ".exe") == installs._make_alias_key("pythonw3-xyz.exe")
141+
assert ("python", "", "3", "-64", ".exe") == installs._make_alias_key("python3-64.exe")
142+
assert ("python", "", "3", "", ".exe") == installs._make_alias_key("python3.exe")
143+
assert ("python3-xyz", "", "", "", ".exe") == installs._make_alias_key("python3-xyz.exe")
144+
145+
146+
def test_install_alias_opt_part():
147+
assert "" == installs._make_opt_part([])
148+
assert "x" == installs._make_opt_part(["x"])
149+
assert "[x]" == installs._make_opt_part(["x", ""])
150+
assert "[x|y]" == installs._make_opt_part(["", "y", "x"])
151+
152+
153+
def test_install_alias_names():
154+
input = [{"name": i} for i in ["py3.exe", "PY3-64.exe", "PYW3.exe", "pyw3-64.exe"]]
155+
input.extend([{"name": i, "windowed": 1} for i in ["xy3.exe", "XY3-64.exe", "XYW3.exe", "xyw3-64.exe"]])
156+
expect = ["py[w]3[-64].exe"]
157+
expectw = ["py[w]3[-64].exe", "xy[w]3[-64].exe"]
158+
assert expect == installs.get_install_alias_names(input, friendly=True, windowed=False)
159+
assert expectw == installs.get_install_alias_names(input, friendly=True, windowed=True)

0 commit comments

Comments
 (0)