Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/manage/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,13 @@
"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 = Path(package_get_root())
except ImportError:
root = Path(sys.executable).parent
resolve_config(cfg, key_path, root, schema=schema, error_unknown=True)

Check warning on line 140 in src/manage/config.py

View check run for this annotation

Codecov / codecov/patch

src/manage/config.py#L135-L140

Added lines #L135 - L140 were not covered by tests
return cfg


Expand Down
43 changes: 29 additions & 14 deletions src/manage/install_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -286,7 +286,7 @@
}


def update_all_shortcuts(cmd, path_warning=True):
def update_all_shortcuts(cmd):
LOGGER.debug("Updating global shortcuts")
alias_written = set()
shortcut_written = {}
Expand Down Expand Up @@ -329,34 +329,48 @@
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

Check warning on line 345 in src/manage/install_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L345

Added line #L345 was not covered by tests


def print_cli_shortcuts(cmd):
from .installs import get_install_alias_names
installs = cmd.get_installs()
seen = set()
tags = getattr(cmd, "tags", None)
seen = {"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 need to pre-filter aliases before getting the nice names.
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"):
LOGGER.debug("%s will be launched by !G!python.exe!W!", i["display-name"])
names = get_install_alias_names(aliases, windowed=True)
LOGGER.debug("%s will be launched by %s", i["display-name"], ", ".join(names))

Check warning on line 360 in src/manage/install_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L358-L360

Added lines #L358 - L360 were not covered by tests

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"])

Expand Down Expand Up @@ -545,6 +559,7 @@
else:
LOGGER.info("Refreshing install registrations.")
update_all_shortcuts(cmd)
print_cli_shortcuts(cmd)

Check warning on line 562 in src/manage/install_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L562

Added line #L562 was not covered by tests
LOGGER.debug("END install_command.execute")
return

Expand Down
75 changes: 74 additions & 1 deletion src/manage/installs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -120,6 +120,79 @@
return installs


def _make_alias_key(alias):
n1, sep, n3 = alias.rpartition(".")
n2 = ""
n3 = sep + n3

n1, plat = split_platform(n1)

while n1 and n1[-1] in "0123456789.-":
n2 = n1[-1] + n2
n1 = n1[:-1]

if n1 and n1[-1].casefold() == "w".casefold():
w = "w"
n1 = n1[:-1]
else:
w = ""

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 _sk_sub(m):
n = m.group(1)
if not n:
return ""

Check warning on line 154 in src/manage/installs.py

View check run for this annotation

Codecov / codecov/patch

src/manage/installs.py#L154

Added line #L154 was not covered by tests
if n in "[]":
return ""
try:
return f"{int(n):020}"
except ValueError:
pass
return n

Check warning on line 161 in src/manage/installs.py

View check run for this annotation

Codecov / codecov/patch

src/manage/installs.py#L159-L161

Added lines #L159 - L161 were not covered by tests


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")]
if not friendly:
return sorted(a["name"] for a in aliases)

Check warning on line 173 in src/manage/installs.py

View check run for this annotation

Codecov / codecov/patch

src/manage/installs.py#L173

Added line #L173 was not covered by tests

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, key=_make_alias_name_sortkey)


def _patch_install_to_run(i, run_for):
return {
**i,
Expand Down
43 changes: 7 additions & 36 deletions src/manage/list_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,14 @@
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)

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):
Expand Down
4 changes: 2 additions & 2 deletions src/manage/tagutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/manage/uninstall_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,6 @@
LOGGER.debug("TRACEBACK:", exc_info=True)

if to_uninstall:
update_all_shortcuts(cmd, path_warning=False)
update_all_shortcuts(cmd)

Check warning on line 130 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L130

Added line #L130 was not covered by tests

LOGGER.debug("END uninstall_command.execute")
11 changes: 8 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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")
Expand Down
36 changes: 36 additions & 0 deletions tests/test_install_command.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import os
import pytest
import secrets
from pathlib import Path, PurePath

from manage import install_command as IC
from manage import installs


@pytest.fixture
def alias_checker(tmp_path):
with AliasChecker(tmp_path) as checker:
yield checker


class AliasChecker:
class Cmd:
global_dir = "out"
Expand Down Expand Up @@ -95,3 +100,34 @@
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, monkeypatch, 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"")

monkeypatch.setitem(os.environ, "PATH", f"{os.environ['PATH']};{Cmd.global_dir}")
IC.print_cli_shortcuts(Cmd())
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)

Check warning on line 126 in tests/test_install_command.py

View check run for this annotation

Codecov / codecov/patch

tests/test_install_command.py#L126

Added line #L126 was not covered by tests

(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")
)
31 changes: 31 additions & 0 deletions tests/test_installs.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,34 @@ def test_get_install_to_run_with_range(patched_installs):
i = installs.get_install_to_run("<none>", None, ">1.0")
assert i["id"] == "PythonCore-2.0-64"
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"))
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")
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)