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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy