From 804876b7c18b0a6947e1367ac1e9991c50379c66 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 14:32:02 +0100 Subject: [PATCH 01/10] Improves alias list messages after installation. Fixes #83 --- src/manage/install_command.py | 39 ++++++++++++++++++++++++--------- src/manage/uninstall_command.py | 2 +- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/manage/install_command.py b/src/manage/install_command.py index 5dea298..3094d59 100644 --- a/src/manage/install_command.py +++ b/src/manage/install_command.py @@ -10,7 +10,7 @@ ) from .fsutils import ensure_tree, rmtree, unlink from .indexutils import Index -from .logging import CONSOLE_MAX_WIDTH, LOGGER, ProgressPrinter +from .logging import CONSOLE_MAX_WIDTH, LOGGER, ProgressPrinter, VERBOSE from .pathutils import Path, PurePath from .tagutils import install_matches_any, tag_or_range from .urlutils import ( @@ -286,7 +286,7 @@ def _cleanup_arp_entries(cmd, install_shortcut_pairs): } -def update_all_shortcuts(cmd, path_warning=True): +def update_all_shortcuts(cmd): LOGGER.debug("Updating global shortcuts") alias_written = set() shortcut_written = {} @@ -329,25 +329,43 @@ def update_all_shortcuts(cmd, path_warning=True): for k, (_, cleanup) in SHORTCUT_HANDLERS.items(): cleanup(cmd, shortcut_written.get(k, [])) - if path_warning and cmd.global_dir and cmd.global_dir.is_dir() and any(cmd.global_dir.glob("*.exe")): + +def print_cli_shortcuts(cmd): + if cmd.global_dir and cmd.global_dir.is_dir() and any(cmd.global_dir.glob("*.exe")): try: if not any(cmd.global_dir.match(p) for p in os.getenv("PATH", "").split(os.pathsep) if p): LOGGER.info("") LOGGER.info("!B!Global shortcuts directory is not on PATH. " + - "Add it for easy access to global Python commands.!W!") + "Add it for easy access to global Python aliases.!W!") LOGGER.info("!B!Directory to add: !Y!%s!W!", cmd.global_dir) LOGGER.info("") + return except Exception: LOGGER.debug("Failed to display PATH warning", exc_info=True) + return - -def print_cli_shortcuts(cmd): installs = cmd.get_installs() - seen = set() + tags = getattr(cmd, "tags", None) + seen = set("python.exe".casefold()) + verbose = LOGGER.would_log_to_console(VERBOSE) for i in installs: - aliases = sorted(a["name"] for a in i["alias"] if a["name"].casefold() not in seen) - seen.update(n.casefold() for n in aliases) - if not install_matches_any(i, cmd.tags): + # We only show windowed aliases if -v is enabled. But we log them as + # debug info unconditionally. This involves a bit of a dance to keep the + # 'windowed' flag around and then drop entries based on whether + # LOGGER.verbose would be printed to the console. + aliases = sorted((a["name"], a.get("windowed", 0)) for a in i["alias"] + if a["name"].casefold() not in seen) + seen.update(n.casefold() for n, *_ in aliases) + if not verbose: + if i.get("default"): + LOGGER.debug("%s will be launched by !G!python.exe!W!", i["display-name"]) + LOGGER.debug("%s will be launched by %s", i["display-name"], + ", ".join([n for n, *_ in aliases])) + aliases = [n for n, w in aliases if not w] + else: + aliases = [n for n, *_ in aliases] + + if tags and not install_matches_any(i, cmd.tags): continue if i.get("default") and aliases: LOGGER.info("%s will be launched by !G!python.exe!W! and also %s", @@ -545,6 +563,7 @@ def execute(cmd): else: LOGGER.info("Refreshing install registrations.") update_all_shortcuts(cmd) + print_cli_shortcuts(cmd) LOGGER.debug("END install_command.execute") return diff --git a/src/manage/uninstall_command.py b/src/manage/uninstall_command.py index 6661112..da117b7 100644 --- a/src/manage/uninstall_command.py +++ b/src/manage/uninstall_command.py @@ -127,6 +127,6 @@ def execute(cmd): LOGGER.debug("TRACEBACK:", exc_info=True) if to_uninstall: - update_all_shortcuts(cmd, path_warning=False) + update_all_shortcuts(cmd) LOGGER.debug("END uninstall_command.execute") From d5e2a4782d1c5448d7ebfd7ece8509f5084517e9 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 16:19:13 +0100 Subject: [PATCH 02/10] Reimplement alias merging algorithm --- src/manage/install_command.py | 28 ++++++++---------- src/manage/installs.py | 56 +++++++++++++++++++++++++++++++++++ src/manage/list_command.py | 40 +++---------------------- 3 files changed, 72 insertions(+), 52 deletions(-) diff --git a/src/manage/install_command.py b/src/manage/install_command.py index 3094d59..1afcbce 100644 --- a/src/manage/install_command.py +++ b/src/manage/install_command.py @@ -344,37 +344,33 @@ def print_cli_shortcuts(cmd): LOGGER.debug("Failed to display PATH warning", exc_info=True) return + from .installs import get_install_alias_names installs = cmd.get_installs() tags = getattr(cmd, "tags", None) seen = set("python.exe".casefold()) verbose = LOGGER.would_log_to_console(VERBOSE) for i in installs: - # We only show windowed aliases if -v is enabled. But we log them as - # debug info unconditionally. This involves a bit of a dance to keep the - # 'windowed' flag around and then drop entries based on whether - # LOGGER.verbose would be printed to the console. - aliases = sorted((a["name"], a.get("windowed", 0)) for a in i["alias"] - if a["name"].casefold() not in seen) - seen.update(n.casefold() for n, *_ in aliases) + # We need to pre-filter aliases before getting the nice names. + aliases = [a for a in i["alias"] if a["name"].casefold() not in seen] + seen.update(n["name"].casefold() for n in aliases) if not verbose: if i.get("default"): LOGGER.debug("%s will be launched by !G!python.exe!W!", i["display-name"]) - LOGGER.debug("%s will be launched by %s", i["display-name"], - ", ".join([n for n, *_ in aliases])) - aliases = [n for n, w in aliases if not w] - else: - aliases = [n for n, *_ in aliases] + names = get_install_alias_names(aliases, windowed=True) + LOGGER.debug("%s will be launched by %s", i["display-name"], ", ".join(names)) if tags and not install_matches_any(i, cmd.tags): continue - if i.get("default") and aliases: + + names = get_install_alias_names(aliases, windowed=False) + if i.get("default") and names: LOGGER.info("%s will be launched by !G!python.exe!W! and also %s", - i["display-name"], ", ".join(aliases)) + i["display-name"], ", ".join(names)) elif i.get("default"): LOGGER.info("%s will be launched by !G!python.exe!W!.", i["display-name"]) - elif aliases: + elif names: LOGGER.info("%s will be launched by %s", - i["display-name"], ", ".join(aliases)) + i["display-name"], ", ".join(names)) else: LOGGER.info("Installed %s to %s", i["display-name"], i["prefix"]) diff --git a/src/manage/installs.py b/src/manage/installs.py index 569ce1b..c3d2286 100644 --- a/src/manage/installs.py +++ b/src/manage/installs.py @@ -120,6 +120,62 @@ def get_installs( return installs +def _make_alias_key(alias): + from .tagutils import SUPPORTED_PLATFORM_SUFFIXES + n1, sep, n3 = alias.rpartition(".") + n2 = "" + n3 = sep + n3 + if n1.endswith(SUPPORTED_PLATFORM_SUFFIXES): + n1, sep, plat = n1.rpartition("-") + plat = sep + plat + else: + plat = "" + + while n1 and n1[-1] in "0123456789.-": + n2 = n1[-1] + n2 + n1 = n1[:-1] + w = "" + if n1 and n1[-1] == "w": + w = "w" + n1 = n1[:-1] + return n1, w, n2, plat, n3 + + +def _make_opt_part(parts): + if not parts: + return "" + if len(parts) == 1: + return list(parts)[0] + return "[{}]".format("|".join(sorted(p for p in parts if p))) + + +def get_install_alias_names(aliases, friendly=True, windowed=True): + if not windowed: + aliases = [a for a in aliases if not a.get("windowed")] + if not friendly: + return sorted(a["name"] for a in aliases) + + seen = {} + has_w = {} + plats = {} + for n1, w, n2, plat, n3 in (_make_alias_key(a["name"]) for a in aliases): + k = n1.casefold(), n2.casefold(), n3.casefold() + seen.setdefault(k, (n1, n2, n3)) + has_w.setdefault(k, set()).add(w) + plats.setdefault(k, set()).add(plat) + + result = [] + for k, (n1, n2, n3) in seen.items(): + result.append("".join([ + n1, + _make_opt_part(has_w.get(k)), + n2, + _make_opt_part(plats.get(k)), + n3, + ])) + return sorted(result) + + def _patch_install_to_run(i, run_for): return { **i, diff --git a/src/manage/list_command.py b/src/manage/list_command.py index 46d78f0..8380e52 100644 --- a/src/manage/list_command.py +++ b/src/manage/list_command.py @@ -7,43 +7,11 @@ LOGGER = logging.LOGGER -def _exe_partition(n): - n1, sep, n2 = n.rpartition(".") - n2 = sep + n2 - while n1 and n1[-1] in "0123456789.-": - n2 = n1[-1] + n2 - n1 = n1[:-1] - w = "" - if n1 and n1[-1] == "w": - w = "w" - n1 = n1[:-1] - return n1, w, n2 - - def _format_alias(i, seen): - try: - alias = i["alias"] - except KeyError: - return "" - if not alias: - return "" - if len(alias) == 1: - a = i["alias"][0] - n = a["name"].casefold() - if n in seen: - return "" - seen.add(n) - return i["alias"][0]["name"] - names = {_exe_partition(a["name"].casefold()): a["name"] for a in alias - if a["name"].casefold() not in seen} - seen.update(a["name"].casefold() for a in alias) - for n1, w, n2 in list(names): - k = (n1, "", n2) - if w and k in names: - del names[n1, w, n2] - n1, _, n2 = _exe_partition(names[k]) - names[k] = f"{n1}[w]{n2}" - return ", ".join(names[n] for n in sorted(names)) + from manage.installs import get_install_alias_names + aliases = [a for a in i.get("alias", ()) if a["name"].casefold() not in seen] + seen.update(a["name"].casefold() for a in aliases) + return ", ".join(get_install_alias_names(aliases)) def _format_tag_with_co(cmd, i): From 40a9752cbf540fb32859d977425977c36d5c100f Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 16:27:49 +0100 Subject: [PATCH 03/10] Add some tests --- src/manage/installs.py | 2 +- tests/test_installs.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/manage/installs.py b/src/manage/installs.py index c3d2286..4500f37 100644 --- a/src/manage/installs.py +++ b/src/manage/installs.py @@ -135,7 +135,7 @@ def _make_alias_key(alias): n2 = n1[-1] + n2 n1 = n1[:-1] w = "" - if n1 and n1[-1] == "w": + if n1 and n1[-1].casefold() == "w".casefold(): w = "w" n1 = n1[:-1] return n1, w, n2, plat, n3 diff --git a/tests/test_installs.py b/tests/test_installs.py index 4a607ef..170f6a9 100644 --- a/tests/test_installs.py +++ b/tests/test_installs.py @@ -126,3 +126,28 @@ def test_get_install_to_run_with_range(patched_installs): i = installs.get_install_to_run("", None, ">1.0") assert i["id"] == "PythonCore-2.0-64" assert i["executable"].match("python.exe") + + +def test_install_alias_make_alias_key(): + assert ("python", "w", "3", "-64", ".exe") == installs._make_alias_key("pythonw3-64.exe") + assert ("python", "w", "3", "", ".exe") == installs._make_alias_key("pythonw3.exe") + assert ("pythonw3-xyz", "", "", "", ".exe") == installs._make_alias_key("pythonw3-xyz.exe") + assert ("python", "", "3", "-64", ".exe") == installs._make_alias_key("python3-64.exe") + assert ("python", "", "3", "", ".exe") == installs._make_alias_key("python3.exe") + assert ("python3-xyz", "", "", "", ".exe") == installs._make_alias_key("python3-xyz.exe") + + +def test_install_alias_opt_part(): + assert "" == installs._make_opt_part([]) + assert "x" == installs._make_opt_part(["x"]) + assert "[x]" == installs._make_opt_part(["x", ""]) + assert "[x|y]" == installs._make_opt_part(["", "y", "x"]) + + +def test_install_alias_names(): + input = [{"name": i} for i in ["py3.exe", "PY3-64.exe", "PYW3.exe", "pyw3-64.exe"]] + input.extend([{"name": i, "windowed": 1} for i in ["xy3.exe", "XY3-64.exe", "XYW3.exe", "xyw3-64.exe"]]) + expect = ["py[w]3[-64].exe"] + expectw = ["py[w]3[-64].exe", "xy[w]3[-64].exe"] + assert expect == installs.get_install_alias_names(input, friendly=True, windowed=False) + assert expectw == installs.get_install_alias_names(input, friendly=True, windowed=True) From ae7bcdf24bb68894230fee61114c35963d3b0c90 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 16:47:49 +0100 Subject: [PATCH 04/10] Improved sort algorithm for alias names --- src/manage/installs.py | 37 ++++++++++++++++++++++++++++--------- src/manage/list_command.py | 5 ++++- src/manage/tagutils.py | 4 ++-- tests/test_installs.py | 4 ++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/manage/installs.py b/src/manage/installs.py index 4500f37..fd473f6 100644 --- a/src/manage/installs.py +++ b/src/manage/installs.py @@ -3,7 +3,7 @@ from .exceptions import NoInstallFoundError, NoInstallsError from .logging import DEBUG, LOGGER from .pathutils import Path -from .tagutils import CompanyTag, tag_or_range, companies_match +from .tagutils import CompanyTag, tag_or_range, companies_match, split_platform from .verutils import Version @@ -121,23 +121,22 @@ def get_installs( def _make_alias_key(alias): - from .tagutils import SUPPORTED_PLATFORM_SUFFIXES n1, sep, n3 = alias.rpartition(".") n2 = "" n3 = sep + n3 - if n1.endswith(SUPPORTED_PLATFORM_SUFFIXES): - n1, sep, plat = n1.rpartition("-") - plat = sep + plat - else: - plat = "" + + n1, plat = split_platform(n1) while n1 and n1[-1] in "0123456789.-": n2 = n1[-1] + n2 n1 = n1[:-1] - w = "" + if n1 and n1[-1].casefold() == "w".casefold(): w = "w" n1 = n1[:-1] + else: + w = "" + return n1, w, n2, plat, n3 @@ -149,6 +148,26 @@ def _make_opt_part(parts): return "[{}]".format("|".join(sorted(p for p in parts if p))) +def _sk_sub(m): + n = m.group(1) + if not n: + return "" + if n == ".": + return "-" + if n == "-": + return "." + try: + return f"{int(n):020}" + except ValueError: + pass + return n + + +def _make_alias_name_sortkey(n): + import re + return re.sub(r"(\d+|.|-)", _sk_sub, n) + + def get_install_alias_names(aliases, friendly=True, windowed=True): if not windowed: aliases = [a for a in aliases if not a.get("windowed")] @@ -173,7 +192,7 @@ def get_install_alias_names(aliases, friendly=True, windowed=True): _make_opt_part(plats.get(k)), n3, ])) - return sorted(result) + return sorted(result, key=_make_alias_name_sortkey) def _patch_install_to_run(i, run_for): diff --git a/src/manage/list_command.py b/src/manage/list_command.py index 8380e52..fde28bb 100644 --- a/src/manage/list_command.py +++ b/src/manage/list_command.py @@ -11,7 +11,10 @@ def _format_alias(i, seen): from manage.installs import get_install_alias_names aliases = [a for a in i.get("alias", ()) if a["name"].casefold() not in seen] seen.update(a["name"].casefold() for a in aliases) - return ", ".join(get_install_alias_names(aliases)) + + include_w = LOGGER.would_log_to_console(logging.VERBOSE) + names = get_install_alias_names(aliases, windowed=include_w) + return ", ".join(names) def _format_tag_with_co(cmd, i): diff --git a/src/manage/tagutils.py b/src/manage/tagutils.py index cf00fd2..e2a22cd 100644 --- a/src/manage/tagutils.py +++ b/src/manage/tagutils.py @@ -123,7 +123,7 @@ def __lt__(self, other): return self.sortkey > other.sortkey -def _split_platform(tag): +def split_platform(tag): if tag.endswith(SUPPORTED_PLATFORM_SUFFIXES): for t in SUPPORTED_PLATFORM_SUFFIXES: if tag.endswith(t): @@ -178,7 +178,7 @@ def __init__(self, company_or_tag, tag=None, *, loose_company=True): else: assert isinstance(company_or_tag, _CompanyKey) self._company = company_or_tag - self.tag, self.platform = _split_platform(tag) + self.tag, self.platform = split_platform(tag) self._sortkey = _sort_tag(self.tag) @property diff --git a/tests/test_installs.py b/tests/test_installs.py index 170f6a9..b203af0 100644 --- a/tests/test_installs.py +++ b/tests/test_installs.py @@ -128,6 +128,10 @@ def test_get_install_to_run_with_range(patched_installs): assert i["executable"].match("python.exe") +def test_install_alias_make_alias_sortkey(): + assert ("pythonw00000000000000000003.00000000000000000064-exe" + == installs._make_alias_name_sortkey("pythonw3-64.exe")) + def test_install_alias_make_alias_key(): assert ("python", "w", "3", "-64", ".exe") == installs._make_alias_key("pythonw3-64.exe") assert ("python", "w", "3", "", ".exe") == installs._make_alias_key("pythonw3.exe") From eb07462aff234e9972059af7a400949225e3d844 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 17:07:15 +0100 Subject: [PATCH 05/10] Fix set --- src/manage/install_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manage/install_command.py b/src/manage/install_command.py index 1afcbce..dc35f70 100644 --- a/src/manage/install_command.py +++ b/src/manage/install_command.py @@ -347,7 +347,7 @@ def print_cli_shortcuts(cmd): from .installs import get_install_alias_names installs = cmd.get_installs() tags = getattr(cmd, "tags", None) - seen = set("python.exe".casefold()) + seen = {"python.exe".casefold()} verbose = LOGGER.would_log_to_console(VERBOSE) for i in installs: # We need to pre-filter aliases before getting the nice names. From b418ddb8693bae1db6d443092b1dec6bf8939207 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 17:09:37 +0100 Subject: [PATCH 06/10] Sort by omitting square brackets --- src/manage/installs.py | 8 +++----- tests/test_installs.py | 4 +++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/manage/installs.py b/src/manage/installs.py index fd473f6..4b1dfa5 100644 --- a/src/manage/installs.py +++ b/src/manage/installs.py @@ -152,10 +152,8 @@ def _sk_sub(m): n = m.group(1) if not n: return "" - if n == ".": - return "-" - if n == "-": - return "." + if n in "[]": + return "" try: return f"{int(n):020}" except ValueError: @@ -165,7 +163,7 @@ def _sk_sub(m): def _make_alias_name_sortkey(n): import re - return re.sub(r"(\d+|.|-)", _sk_sub, n) + return re.sub(r"(\d+|\[|\])", _sk_sub, n) def get_install_alias_names(aliases, friendly=True, windowed=True): diff --git a/tests/test_installs.py b/tests/test_installs.py index b203af0..a9ffc3f 100644 --- a/tests/test_installs.py +++ b/tests/test_installs.py @@ -129,8 +129,10 @@ def test_get_install_to_run_with_range(patched_installs): def test_install_alias_make_alias_sortkey(): - assert ("pythonw00000000000000000003.00000000000000000064-exe" + assert ("pythonw00000000000000000003-00000000000000000064.exe" == installs._make_alias_name_sortkey("pythonw3-64.exe")) + assert ("pythonw00000000000000000003-00000000000000000064.exe" + == installs._make_alias_name_sortkey("python[w]3[-64].exe")) def test_install_alias_make_alias_key(): assert ("python", "w", "3", "-64", ".exe") == installs._make_alias_key("pythonw3-64.exe") From 1bd22d503f507be6abd5a99185afe20e091ac46f Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 17:29:54 +0100 Subject: [PATCH 07/10] Add test coverage for new code --- src/manage/install_command.py | 2 +- tests/conftest.py | 11 ++++++++--- tests/test_install_command.py | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/manage/install_command.py b/src/manage/install_command.py index dc35f70..0462d93 100644 --- a/src/manage/install_command.py +++ b/src/manage/install_command.py @@ -351,7 +351,7 @@ def print_cli_shortcuts(cmd): verbose = LOGGER.would_log_to_console(VERBOSE) for i in installs: # We need to pre-filter aliases before getting the nice names. - aliases = [a for a in i["alias"] if a["name"].casefold() not in seen] + aliases = [a for a in i.get("alias", ()) if a["name"].casefold() not in seen] seen.update(n["name"].casefold() for n in aliases) if not verbose: if i.get("default"): diff --git a/tests/conftest.py b/tests/conftest.py index 68fc24c..200be10 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -201,7 +201,7 @@ def make_install(tag, **kwargs): run_for.append({"tag": t, "target": kwargs.get("target", "python.exe")}) run_for.append({"tag": t, "target": kwargs.get("targetw", "pythonw.exe"), "windowed": 1}) - return { + i = { "company": kwargs.get("company", "PythonCore"), "id": "{}-{}".format(kwargs.get("company", "PythonCore"), tag), "sort-version": kwargs.get("sort_version", tag), @@ -212,12 +212,17 @@ def make_install(tag, **kwargs): "prefix": PurePath(kwargs.get("prefix", rf"C:\{tag}")), "executable": kwargs.get("executable", "python.exe"), } + try: + i["alias"] = kwargs["alias"] + except LookupError: + pass + return i def fake_get_installs(install_dir): yield make_install("1.0") - yield make_install("1.0-32", sort_version="1.0") - yield make_install("1.0-64", sort_version="1.0") + yield make_install("1.0-32", sort_version="1.0", alias=[dict(name="py1.0.exe"), dict(name="py1.0-32.exe")]) + yield make_install("1.0-64", sort_version="1.0", alias=[dict(name="py1.0.exe"), dict(name="py1.0-64.exe")]) yield make_install("2.0-64", sort_version="2.0") yield make_install("2.0-arm64", sort_version="2.0") yield make_install("3.0a1-32", sort_version="3.0a1") diff --git a/tests/test_install_command.py b/tests/test_install_command.py index e0cc129..cf40336 100644 --- a/tests/test_install_command.py +++ b/tests/test_install_command.py @@ -1,6 +1,9 @@ import pytest import secrets +from pathlib import PurePath + from manage import install_command as IC +from manage import installs @pytest.fixture @@ -8,6 +11,7 @@ def alias_checker(tmp_path): with AliasChecker(tmp_path) as checker: yield checker + class AliasChecker: class Cmd: global_dir = "out" @@ -95,3 +99,18 @@ def test_write_alias_default_platform(alias_checker): def test_write_alias_fallback_platform(alias_checker): alias_checker.check_64(alias_checker.Cmd("-spam"), "1.0", "testA") alias_checker.check_w64(alias_checker.Cmd("-spam"), "1.0", "testB") + + +def test_print_cli_shortcuts(patched_installs, assert_log): + class Cmd: + global_dir = None + def get_installs(self): + return installs.get_installs(None) + + IC.print_cli_shortcuts(Cmd()) + print(assert_log) + assert_log( + assert_log.skip_until("Installed %s", ["Python 2.0-64", PurePath("C:\\2.0-64")]), + assert_log.skip_until("%s will be launched by %s", ["Python 1.0-64", "py1.0[-64].exe"]), + ("%s will be launched by %s", ["Python 1.0-32", "py1.0-32.exe"]), + ) From 0acfd7665c84d5b706457cf0ec3aec7f300138df Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 17:46:45 +0100 Subject: [PATCH 08/10] More test coverage --- tests/test_install_command.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/test_install_command.py b/tests/test_install_command.py index cf40336..0e15dfd 100644 --- a/tests/test_install_command.py +++ b/tests/test_install_command.py @@ -1,6 +1,7 @@ +import os import pytest import secrets -from pathlib import PurePath +from pathlib import Path, PurePath from manage import install_command as IC from manage import installs @@ -101,16 +102,32 @@ def test_write_alias_fallback_platform(alias_checker): alias_checker.check_w64(alias_checker.Cmd("-spam"), "1.0", "testB") -def test_print_cli_shortcuts(patched_installs, assert_log): +def test_print_cli_shortcuts(patched_installs, assert_log, monkeypatch, tmp_path): class Cmd: - global_dir = None + global_dir = Path(tmp_path) def get_installs(self): return installs.get_installs(None) + (tmp_path / "fake.exe").write_bytes(b"") + + monkeypatch.setitem(os.environ, "PATH", f"{os.environ['PATH']};{Cmd.global_dir}") IC.print_cli_shortcuts(Cmd()) - print(assert_log) assert_log( assert_log.skip_until("Installed %s", ["Python 2.0-64", PurePath("C:\\2.0-64")]), assert_log.skip_until("%s will be launched by %s", ["Python 1.0-64", "py1.0[-64].exe"]), ("%s will be launched by %s", ["Python 1.0-32", "py1.0-32.exe"]), ) + + +def test_print_path_warning(patched_installs, assert_log, tmp_path): + class Cmd: + global_dir = Path(tmp_path) + def get_installs(self): + return installs.get_installs(None) + + (tmp_path / "fake.exe").write_bytes(b"") + + IC.print_cli_shortcuts(Cmd()) + assert_log( + assert_log.skip_until(".*Global shortcuts directory is not on PATH") + ) From e314e32ca209ecd833c31841f0b2bffe035016dc Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 17:56:30 +0100 Subject: [PATCH 09/10] Fix registry configuration failure --- src/manage/config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/manage/config.py b/src/manage/config.py index c9500be..c4858e8 100644 --- a/src/manage/config.py +++ b/src/manage/config.py @@ -131,7 +131,13 @@ def load_registry_config(key_path, schema): "This is very unexpected. Please check your configuration " + "or report an issue at https://github.com/python/pymanager.", key_path) - resolve_config(cfg, key_path, _global_file().parent, schema=schema, error_unknown=True) + + try: + from _native import package_get_root + root = package_get_root() + except ImportError: + root = Path(sys.executable).parent + resolve_config(cfg, key_path, root, schema=schema, error_unknown=True) return cfg From 2e1eff6d140691cc12b274465a9e0e5274da7059 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 17:59:25 +0100 Subject: [PATCH 10/10] Fix when get_package_root works --- src/manage/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manage/config.py b/src/manage/config.py index c4858e8..5b83095 100644 --- a/src/manage/config.py +++ b/src/manage/config.py @@ -134,7 +134,7 @@ def load_registry_config(key_path, schema): try: from _native import package_get_root - root = package_get_root() + root = Path(package_get_root()) except ImportError: root = Path(sys.executable).parent resolve_config(cfg, key_path, root, schema=schema, error_unknown=True)