Skip to content

Commit d14e65d

Browse files
authored
Prefer default install for shared aliases (#415)
Fixes #413 get_installs() keeps precedence order and only flags an overridden default (e.g. via PYTHON_MANAGER_DEFAULT), so update_all_shortcuts() generated entrypoint aliases such as pip from the first install while python/pythonw came from the default. Move the default install front before alias creation so first-match-wins picks it. Ordering is kept local to preserve py list output.
1 parent d1a7221 commit d14e65d

2 files changed

Lines changed: 74 additions & 1 deletion

File tree

src/manage/install_command.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,15 @@ def _cleanup_arp_entries(cmd, install_shortcut_pairs):
299299

300300
def update_all_shortcuts(cmd, *, _aliasutils=None):
301301
LOGGER.debug("Updating global shortcuts")
302-
installs = cmd.get_installs()
302+
installs = list(cmd.get_installs())
303+
# get_installs() keeps precedence order and only flags the overridden
304+
# default, so move it front for first-match-wins alias creation.
305+
# Kept here instead of get_installs() to preserve `py list` order.
306+
for n, i in enumerate(installs):
307+
if i.get("default"):
308+
if n:
309+
installs.insert(0, installs.pop(n))
310+
break
303311
shortcut_written = {}
304312

305313
if cmd.global_dir:

tests/test_install_command.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,71 @@ def cleanup_aliases(cmd, preserve):
246246
assert set(a.target for a in created) == {"p.exe", "pw.exe"}
247247

248248

249+
def test_update_all_shortcuts_default_first(tmp_path):
250+
# Regression test for https://github.com/python/pymanager/issues/413
251+
# When the default install is not first in precedence order (e.g. via
252+
# PYTHON_MANAGER_DEFAULT), its entrypoints must still win over the
253+
# first install's, so pip and python resolve to the same runtime.
254+
prefix_new = Path(tmp_path) / "new"
255+
prefix_default = Path(tmp_path) / "default"
256+
for prefix in (prefix_new, prefix_default):
257+
prefix.mkdir(exist_ok=True, parents=True)
258+
(prefix / "python.exe").write_bytes(b"")
259+
(prefix / "shared-target.exe").write_bytes(b"")
260+
261+
class Cmd:
262+
global_dir = Path(tmp_path) / "bin"
263+
launcher_exe = None
264+
scratch = {}
265+
enable_shortcut_kinds = disable_shortcut_kinds = None
266+
enable_entrypoints = False
267+
def get_installs(self):
268+
return [
269+
{
270+
"id": "new",
271+
"alias": [
272+
{"name": "python3.14.exe", "target": "python.exe"},
273+
{"name": "shared.exe", "target": "shared-target.exe"},
274+
],
275+
"prefix": prefix_new,
276+
},
277+
{
278+
"id": "default",
279+
"alias": [
280+
{"name": "python3.13.exe", "target": "python.exe"},
281+
{"name": "shared.exe", "target": "shared-target.exe"},
282+
],
283+
"default": True,
284+
"prefix": prefix_default,
285+
},
286+
]
287+
288+
created = []
289+
290+
class AliasUtils:
291+
import manage.aliasutils as AU
292+
calculate_aliases = staticmethod(AU.calculate_aliases)
293+
294+
@staticmethod
295+
def create_aliases(cmd, aliases, *, allow_link=True):
296+
created.extend(aliases)
297+
298+
@staticmethod
299+
def cleanup_aliases(cmd, preserve):
300+
pass
301+
302+
IC.update_all_shortcuts(Cmd(), _aliasutils=AliasUtils)
303+
304+
shared = [a for a in created if a.name.casefold() == "shared.exe"]
305+
assert len(shared) == 2
306+
# Default install must come first so first-match-wins picks it
307+
assert shared[0].install["id"] == "default"
308+
assert shared[1].install["id"] == "new"
309+
python_aliases = [a for a in created if a.name in ("python", "pythonw")]
310+
assert python_aliases
311+
assert all(a.install["id"] == "default" for a in python_aliases)
312+
313+
249314
class InstallCommandTestCmd:
250315
def __init__(self, tmp_path, *args, **kwargs):
251316
self.args = args

0 commit comments

Comments
 (0)