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
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ jobs:

- name: 'Launch default runtime'
run: pymanager exec -m site
env:
PYMANAGER_DEBUG: true

- name: 'Uninstall runtime'
run: pymanager uninstall -y default
env:
PYMANAGER_DEBUG: true

- name: 'Emulate first launch'
run: |
Expand Down
8 changes: 8 additions & 0 deletions ci/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ stages:
- powershell: |
pymanager exec -m site
displayName: 'Launch default runtime'
env:
PYMANAGER_DEBUG: true

- powershell: |
pymanager uninstall -y default
displayName: 'Uninstall runtime'
env:
PYMANAGER_DEBUG: true

- powershell: |
$i = (mkdir -force test_installs)
Expand Down
11 changes: 7 additions & 4 deletions src/manage/arputils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from .fsutils import rglob
from .logging import LOGGER
from .pathutils import Path
from .tagutils import install_matches_any

Check warning on line 8 in src/manage/arputils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/arputils.py#L8

Added line #L8 was not covered by tests


def _root():
return winreg.CreateKey(
Expand Down Expand Up @@ -59,7 +61,7 @@
winreg.SetValueEx(key, name, None, winreg.REG_SZ, str(value))


def _make(key, item, shortcut):
def _make(key, item, shortcut, *, allow_warn=True):

Check warning on line 64 in src/manage/arputils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/arputils.py#L64

Added line #L64 was not covered by tests
prefix = Path(item["prefix"])

for value, from_dict, value_name, relative_to in [
Expand Down Expand Up @@ -122,21 +124,22 @@
return


def create_one(install, shortcut):
def create_one(install, shortcut, warn_for=[]):
allow_warn = install_matches_any(install, warn_for)

Check warning on line 128 in src/manage/arputils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/arputils.py#L127-L128

Added lines #L127 - L128 were not covered by tests
with _root() as root:
install_id = f"pymanager-{install['id']}"
LOGGER.debug("Creating ARP entry for %s", install_id)
try:
with winreg.CreateKey(root, install_id) as key:
_make(key, install, shortcut)
_make(key, install, shortcut, allow_warn=allow_warn)

Check warning on line 134 in src/manage/arputils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/arputils.py#L134

Added line #L134 was not covered by tests
except OSError:
LOGGER.debug("Failed to create entry for %s", install_id)
LOGGER.debug("TRACEBACK:", exc_info=True)
_delete_key(root, install_id)
raise


def cleanup(preserve_installs):
def cleanup(preserve_installs, warn_for=[]):

Check warning on line 142 in src/manage/arputils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/arputils.py#L142

Added line #L142 was not covered by tests
keep = {f"pymanager-{i['id']}".casefold() for i in preserve_installs}
to_delete = []
with _root() as root:
Expand Down
19 changes: 11 additions & 8 deletions src/manage/install_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,41 +242,44 @@
launcher = _if_exists(launcher, "-64")
LOGGER.debug("Create %s linking to %s using %s", alias["name"], target, launcher)
if not launcher or not launcher.is_file():
LOGGER.warn("Skipping %s alias because the launcher template was not found.", alias["name"])
if install_matches_any(install, getattr(cmd, "tags", None)):
LOGGER.warn("Skipping %s alias because the launcher template was not found.", alias["name"])

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

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L246

Added line #L246 was not covered by tests
else:
LOGGER.debug("Skipping %s alias because the launcher template was not found.", alias["name"])

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

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L248

Added line #L248 was not covered by tests
return
p.write_bytes(launcher.read_bytes())
p.with_name(p.name + ".__target__").write_text(str(target), encoding="utf-8")


def _create_shortcut_pep514(cmd, install, shortcut):
from .pep514utils import update_registry
update_registry(cmd.pep514_root, install, shortcut)
update_registry(cmd.pep514_root, install, shortcut, cmd.tags)

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

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L256

Added line #L256 was not covered by tests


def _cleanup_shortcut_pep514(cmd, install_shortcut_pairs):
from .pep514utils import cleanup_registry
cleanup_registry(cmd.pep514_root, {s["Key"] for i, s in install_shortcut_pairs})
cleanup_registry(cmd.pep514_root, {s["Key"] for i, s in install_shortcut_pairs}, cmd.tags)

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

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L261

Added line #L261 was not covered by tests


def _create_start_shortcut(cmd, install, shortcut):
from .startutils import create_one
create_one(cmd.start_folder, install, shortcut)
create_one(cmd.start_folder, install, shortcut, cmd.tags)

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

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L266

Added line #L266 was not covered by tests


def _cleanup_start_shortcut(cmd, install_shortcut_pairs):
from .startutils import cleanup
cleanup(cmd.start_folder, [s for i, s in install_shortcut_pairs])
cleanup(cmd.start_folder, [s for i, s in install_shortcut_pairs], cmd.tags)

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

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L271

Added line #L271 was not covered by tests


def _create_arp_entry(cmd, install, shortcut):
# ARP = Add/Remove Programs
from .arputils import create_one
create_one(install, shortcut)
create_one(install, shortcut, cmd.tags)

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

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L277

Added line #L277 was not covered by tests


def _cleanup_arp_entries(cmd, install_shortcut_pairs):
from .arputils import cleanup
cleanup([i for i, s in install_shortcut_pairs])
cleanup([i for i, s in install_shortcut_pairs], cmd.tags)

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

View check run for this annotation

Codecov / codecov/patch

src/manage/install_command.py#L282

Added line #L282 was not covered by tests


SHORTCUT_HANDLERS = {
Expand Down Expand Up @@ -359,7 +362,7 @@
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):
if not install_matches_any(i, tags):
continue

names = get_install_alias_names(aliases, windowed=False)
Expand Down
45 changes: 31 additions & 14 deletions src/manage/pep514utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .logging import LOGGER
from .pathutils import Path
from .tagutils import install_matches_any
from .verutils import Version


Expand Down Expand Up @@ -136,7 +137,7 @@
winreg.SetValueEx(key, k, None, v_kind, v)


def _is_tag_managed(company_key, tag_name, *, creating=False):
def _is_tag_managed(company_key, tag_name, *, creating=False, allow_warn=True):
try:
tag = winreg.OpenKey(company_key, tag_name)
except FileNotFoundError:
Expand Down Expand Up @@ -188,12 +189,15 @@
new_name = f"{orig_name}.{i}"
# Raises standard PermissionError (5) if new_name exists
reg_rename_key(tag.handle, orig_name, new_name)
LOGGER.warn("An existing registry key for %s was renamed to %s "
"because it appeared to be invalid. If this is "
"correct, the registry key can be safely deleted. "
"To avoid this in future, ensure that the "
"InstallPath key refers to a valid path.",
tag_name, new_name)
if allow_warn:
LOGGER.warn("An existing registry key for %s was renamed to %s "
"because it appeared to be invalid. If this is "
"correct, the registry key can be safely deleted. "
"To avoid this in future, ensure that the "
"InstallPath key refers to a valid path.",
tag_name, new_name)
else:
LOGGER.debug("Renamed %s to %s", tag_name, new_name)
break
except FileNotFoundError:
LOGGER.debug("Original key disappeared, so we will claim it")
Expand All @@ -207,8 +211,12 @@
orig_name, new_name, exc_info=True)
raise
else:
LOGGER.warn("Attempted to clean up invalid registry key %s but "
"failed after too many attempts.", tag_name)
if allow_warn:
LOGGER.warn("Attempted to clean up invalid registry key %s but "

Check warning on line 215 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L215

Added line #L215 was not covered by tests
"failed after too many attempts.", tag_name)
else:
LOGGER.debug("Attempted to clean up invalid registry key %s but "

Check warning on line 218 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L218

Added line #L218 was not covered by tests
"failed after too many attempts.", tag_name)
return False
return True

Expand All @@ -226,31 +234,40 @@
return hive, name


def update_registry(root_name, install, data):
def update_registry(root_name, install, data, warn_for=[]):
hive, name = _split_root(root_name)
with winreg.CreateKey(hive, name) as root:
if _is_tag_managed(root, data["Key"], creating=True):
allow_warn = install_matches_any(install, warn_for)
if _is_tag_managed(root, data["Key"], creating=True, allow_warn=allow_warn):
with winreg.CreateKey(root, data["Key"]) as tag:
LOGGER.debug("Creating/updating %s\\%s", root_name, data["Key"])
winreg.SetValueEx(tag, "ManagedByPyManager", None, winreg.REG_DWORD, 1)
_update_reg_values(tag, data, install, {"kind", "Key", "ManagedByPyManager"})
else:
elif allow_warn:
LOGGER.warn("An existing runtime is registered at %s in the registry, "
"and so the new one has not been registered.", data["Key"])
LOGGER.info("This may prevent some other applications from detecting "
"the new installation, although 'py -V:...' will work. "
"To register the new installation, remove the existing "
"runtime and then run 'py install --refresh'")
else:
LOGGER.debug("An existing runtime is registered at %s and so the new "

Check warning on line 254 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L254

Added line #L254 was not covered by tests
"install has not been registered.", data["Key"])


def cleanup_registry(root_name, keep):
def cleanup_registry(root_name, keep, warn_for=[]):
hive, name = _split_root(root_name)
with _reg_open(hive, name, writable=True) as root:
for company_name in _iter_keys(root):
any_left = False
with winreg.OpenKey(root, company_name, access=winreg.KEY_ALL_ACCESS) as company:
for tag_name in _iter_keys(company):
if f"{company_name}\\{tag_name}" in keep or not _is_tag_managed(company, tag_name):
# Calculate whether to show warnings or not
install = {"company": company_name, "tag": tag_name}
allow_warn = install_matches_any(install, warn_for)

Check warning on line 267 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L266-L267

Added lines #L266 - L267 were not covered by tests

if (f"{company_name}\\{tag_name}" in keep
or not _is_tag_managed(company, tag_name, allow_warn=allow_warn)):
any_left = True
else:
_reg_rmtree(company, tag_name)
Expand Down
14 changes: 9 additions & 5 deletions src/manage/startutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .fsutils import rmtree, unlink
from .logging import LOGGER
from .pathutils import Path
from .tagutils import install_matches_any

Check warning on line 6 in src/manage/startutils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/startutils.py#L6

Added line #L6 was not covered by tests


def _unprefix(p, prefix):
Expand All @@ -25,7 +26,7 @@
return p


def _make(root, prefix, item):
def _make(root, prefix, item, allow_warn=True):

Check warning on line 29 in src/manage/startutils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/startutils.py#L29

Added line #L29 was not covered by tests
n = item["Name"]
try:
_make_directory(root, n, prefix, item["Items"])
Expand All @@ -39,7 +40,10 @@
try:
lnk.relative_to(root)
except ValueError:
LOGGER.warn("Package attempted to create shortcut outside of its directory")
if allow_warn:
LOGGER.warn("Package attempted to create shortcut outside of its directory")

Check warning on line 44 in src/manage/startutils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/startutils.py#L44

Added line #L44 was not covered by tests
else:
LOGGER.debug("Package attempted to create shortcut outside of its directory")

Check warning on line 46 in src/manage/startutils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/startutils.py#L46

Added line #L46 was not covered by tests
LOGGER.debug("Path: %s", lnk)
LOGGER.debug("Directory: %s", root)
return None
Expand Down Expand Up @@ -136,12 +140,12 @@
pass


def create_one(root, install, shortcut):
def create_one(root, install, shortcut, warn_for=[]):

Check warning on line 143 in src/manage/startutils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/startutils.py#L143

Added line #L143 was not covered by tests
root = Path(_native.shortcut_get_start_programs()) / root
_make(root, install["prefix"], shortcut)
_make(root, install["prefix"], shortcut, allow_warn=install_matches_any(install, warn_for))

Check warning on line 145 in src/manage/startutils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/startutils.py#L145

Added line #L145 was not covered by tests


def cleanup(root, preserve):
def cleanup(root, preserve, warn_for=[]):

Check warning on line 148 in src/manage/startutils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/startutils.py#L148

Added line #L148 was not covered by tests
root = Path(_native.shortcut_get_start_programs()) / root

if not root.is_dir():
Expand Down
14 changes: 9 additions & 5 deletions src/manage/uninstall_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
cmd.virtual_env = None
installed = list(cmd.get_installs())

cmd.tags = []

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

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L29

Added line #L29 was not covered by tests

if cmd.purge:
if cmd.ask_yn("Uninstall all runtimes?"):
for i in installed:
Expand Down Expand Up @@ -61,16 +63,18 @@
to_uninstall = []
if not cmd.by_id:
for tag in cmd.args:
if tag.casefold() == "default".casefold():
tag = cmd.default_tag
try:
t_or_r = tag_or_range(tag)
if tag.casefold() == "default".casefold():
cmd.tags.append(tag_or_range(cmd.default_tag))

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

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L68

Added line #L68 was not covered by tests
else:
cmd.tags.append(tag_or_range(tag))

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

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L70

Added line #L70 was not covered by tests
except ValueError as ex:
LOGGER.warn("%s", ex)
continue

for tag in cmd.tags:
candidates = get_matching_install_tags(
installed,
t_or_r,
tag,
default_platform=cmd.default_platform,
)
if not candidates:
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ def quiet_log():


class LogCaptureHandler(list):
def skip_until(self, pattern, args=()):
def skip_until(self, pattern, args=None):
return ('until', pattern, args)

def not_logged(self, pattern, args=()):
def not_logged(self, pattern, args=None):
return ('not', pattern, args)

def __call__(self, *cmp):
Expand Down
35 changes: 35 additions & 0 deletions tests/test_pep514utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import winreg

from manage import pep514utils
from manage import tagutils

def test_is_tag_managed(registry, tmp_path):
registry.setup(Company={
Expand All @@ -23,3 +24,37 @@ def test_is_tag_managed(registry, tmp_path):
assert pep514utils._is_tag_managed(registry.key, r"Company\3.0", creating=True)
with winreg.OpenKey(registry.key, r"Company\3.0.2"):
pass


def test_is_tag_managed_warning_suppressed(registry, tmp_path, assert_log):
registry.setup(Company={
"3.0.0": {"": "Just in the way here"},
"3.0.1": {"": "Also in the way here"},
})
pep514utils.update_registry(
rf"HKEY_CURRENT_USER\{registry.root}",
dict(company="Company", tag="3.0.0"),
dict(kind="pep514", Key="Company\\3.0.0", InstallPath=dict(_="dir")),
warn_for=[tagutils.tag_or_range(r"Company\3.0.1")],
)
assert_log(
"Registry key %s appears invalid.+",
assert_log.not_logged("An existing runtime is registered at %s"),
)


def test_is_tag_managed_warning(registry, tmp_path, assert_log):
registry.setup(Company={
"3.0.0": {"": "Just in the way here"},
"3.0.1": {"": "Also in the way here"},
})
pep514utils.update_registry(
rf"HKEY_CURRENT_USER\{registry.root}",
dict(company="Company", tag="3.0.0"),
dict(kind="pep514", Key="Company\\3.0.0", InstallPath=dict(_="dir")),
warn_for=[tagutils.tag_or_range(r"Company\3.0.0")],
)
assert_log(
"Registry key %s appears invalid.+",
assert_log.skip_until("An existing registry key for %s"),
)