Skip to content

Commit 32be691

Browse files
committed
Change default version command - Enhancement #263
1 parent 35a9ec7 commit 32be691

File tree

5 files changed

+229
-0
lines changed

5 files changed

+229
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/env*/
66
/python-manager/
77
/pythons/
8+
/.venv/
89

910
# Can't seem to stop WiX from creating this directory...
1011
/src/pymanager/obj

src/manage/commands.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@
8585
r"Equivalent to -V:PythonCore\3!B!<VERSION>!W!. The version must begin " +
8686
"with a '3', platform overrides are permitted, and regular Python " +
8787
"options may follow. The runtime will be installed if needed."),
88+
(f"{EXE_NAME} default !B!<TAG>!W!\n",
89+
"Set the default Python version to use when no specific version is " +
90+
"requested. Use without !B!<TAG>!W! to show the current default."),
8891
]
8992

9093

@@ -216,6 +219,10 @@ def execute(self):
216219
"help": ("show_help", True), # nested to avoid conflict with command
217220
},
218221

222+
"default": {
223+
"help": ("show_help", True), # nested to avoid conflict with command
224+
},
225+
219226
"**first_run": {
220227
"explicit": ("explicit", True),
221228
},
@@ -260,6 +267,9 @@ def execute(self):
260267
"enable_entrypoints": (config_bool, None),
261268
},
262269

270+
"default": {
271+
},
272+
263273
"first_run": {
264274
"enabled": (config_bool, None, "env"),
265275
"explicit": (config_bool, None),
@@ -1022,6 +1032,34 @@ def execute(self):
10221032
os.startfile(HELP_URL)
10231033

10241034

1035+
class DefaultCommand(BaseCommand):
1036+
CMD = "default"
1037+
HELP_LINE = ("Show or change the default Python runtime version.")
1038+
USAGE_LINE = "default !B![<TAG>]!W!"
1039+
HELP_TEXT = r"""!G!Default command!W!
1040+
Show or change the default Python version used by the system.
1041+
1042+
> py default !B![options] [<TAG>]!W!
1043+
1044+
With no arguments, shows the currently configured default Python version.
1045+
With a !B!<TAG>!W!, sets the default Python version.
1046+
1047+
!G!Examples:!W!
1048+
> py default
1049+
!W!Shows the current default Python version
1050+
1051+
> py default 3.13
1052+
!W!Sets Python 3.13 as the default version
1053+
1054+
> py default 3
1055+
!W!Sets the latest Python 3 as the default version
1056+
"""
1057+
1058+
def execute(self):
1059+
from .default_command import execute
1060+
execute(self)
1061+
1062+
10251063
def load_default_config(root):
10261064
return DefaultConfig(root)
10271065

src/manage/default_command.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Implementation of the 'default' command to manage default Python version."""
2+
3+
import json
4+
from pathlib import Path as PathlibPath
5+
6+
from .exceptions import ArgumentError, NoInstallsError, NoInstallFoundError
7+
from .installs import get_installs, get_matching_install_tags
8+
from .logging import LOGGER
9+
from .pathutils import Path
10+
from .tagutils import tag_or_range
11+
12+
13+
def _get_default_config_file(install_dir):
14+
"""Get the path to the default install marker file."""
15+
return Path(install_dir) / ".default"
16+
17+
18+
def _load_default_install_id(install_dir):
19+
"""Load the saved default install ID from the marker file."""
20+
try:
21+
default_file = _get_default_config_file(install_dir)
22+
if default_file.exists():
23+
return default_file.read_text(encoding="utf-8").strip()
24+
except Exception as e:
25+
LOGGER.debug("Failed to load default install ID: %s", e)
26+
return None
27+
28+
29+
def _save_default_install_id(install_dir, install_id):
30+
"""Save the default install ID to the marker file."""
31+
try:
32+
default_file = _get_default_config_file(install_dir)
33+
default_file.parent.mkdir(parents=True, exist_ok=True)
34+
default_file.write_text(install_id, encoding="utf-8")
35+
LOGGER.info("Default Python version set to: !G!%s!W!", install_id)
36+
except Exception as e:
37+
LOGGER.error("Failed to save default install ID: %s", e)
38+
raise ArgumentError(f"Could not save default version: {e}") from e
39+
40+
41+
def _show_current_default(cmd):
42+
"""Show the currently configured default Python version."""
43+
try:
44+
installs = cmd.get_installs(set_default=False)
45+
except NoInstallsError:
46+
LOGGER.info("No Python installations found.")
47+
return
48+
49+
# Check if there's an explicit default marked
50+
default_install = None
51+
for install in installs:
52+
if install.get("default"):
53+
default_install = install
54+
break
55+
56+
if default_install:
57+
LOGGER.print("!G!Current default:!W! %s", default_install["display-name"])
58+
LOGGER.print(" ID: %s", default_install["id"])
59+
LOGGER.print(" Version: %s", default_install.get("sort-version", "unknown"))
60+
else:
61+
LOGGER.print("!Y!No explicit default set.!W!")
62+
LOGGER.print("Using tag-based default: !B!%s!W!", cmd.default_tag)
63+
64+
65+
def _set_default_version(cmd, tag):
66+
"""Set a specific Python version as the default."""
67+
try:
68+
installs = cmd.get_installs(set_default=False)
69+
except NoInstallsError:
70+
raise ArgumentError("No Python installations found. Install a version first with 'py install'.") from None
71+
72+
if not installs:
73+
raise ArgumentError("No Python installations found. Install a version first with 'py install'.")
74+
75+
# Find the install matching the provided tag
76+
try:
77+
tag_obj = tag_or_range(tag)
78+
except Exception as e:
79+
raise ArgumentError(f"Invalid tag format: {tag}") from e
80+
81+
matching = get_matching_install_tags(
82+
installs,
83+
tag_obj,
84+
default_platform=cmd.default_platform,
85+
single_tag=False,
86+
)
87+
88+
if not matching:
89+
raise NoInstallFoundError(tag=tag)
90+
91+
selected_install, selected_run_for = matching[0]
92+
93+
# Save the install ID as the default
94+
_save_default_install_id(cmd.install_dir, selected_install["id"])
95+
96+
LOGGER.info("Default Python version set to: !G!%s!W! (%s)",
97+
selected_install["display-name"],
98+
selected_install["id"])
99+
100+
101+
def execute(cmd):
102+
"""Execute the default command."""
103+
cmd.show_welcome()
104+
105+
if cmd.show_help:
106+
cmd.help()
107+
return
108+
109+
if not cmd.args:
110+
# Show current default
111+
_show_current_default(cmd)
112+
else:
113+
# Set new default
114+
tag = " ".join(cmd.args[0:1]) # Take the first argument as the tag
115+
_set_default_version(cmd, tag)
116+

src/manage/installs.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,24 @@ def get_installs(
117117
except LookupError:
118118
LOGGER.debug("No virtual environment found")
119119

120+
# Check for a saved default install marker
121+
try:
122+
default_file = Path(install_dir) / ".default"
123+
if default_file.exists():
124+
default_id = default_file.read_text(encoding="utf-8").strip()
125+
LOGGER.debug("Found saved default install ID: %s", default_id)
126+
for install in installs:
127+
if install["id"] == default_id:
128+
install["default"] = True
129+
LOGGER.debug("Marked %s as default", default_id)
130+
break
131+
except Exception as ex:
132+
LOGGER.debug("Could not load default install marker: %s", ex)
133+
120134
return installs
121135

122136

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

tests/test_default_command.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Tests for the default command."""
2+
3+
import pytest
4+
from manage import commands
5+
from manage.exceptions import ArgumentError, NoInstallsError
6+
7+
8+
def test_default_command_help(assert_log):
9+
"""Test the default command help output."""
10+
cmd = commands.DefaultCommand([commands.DefaultCommand.CMD, "--help"], None)
11+
cmd.execute()
12+
assert_log(
13+
assert_log.skip_until(".*Default command.*"),
14+
)
15+
16+
17+
def test_default_command_no_args_no_installs(assert_log):
18+
"""Test default command with no arguments and no installations."""
19+
cmd = commands.DefaultCommand([commands.DefaultCommand.CMD], None)
20+
# This should handle the case gracefully
21+
# We expect it to either show a message about no installs or show current default
22+
# The actual behavior depends on how get_installs works
23+
try:
24+
cmd.execute()
25+
except NoInstallsError:
26+
# This is acceptable - no installs available
27+
pass
28+
29+
30+
def test_default_command_with_invalid_tag():
31+
"""Test default command with an invalid tag."""
32+
cmd = commands.DefaultCommand([commands.DefaultCommand.CMD, "invalid-tag"], None)
33+
try:
34+
cmd.execute()
35+
except (ArgumentError, NoInstallsError):
36+
# Expected - no matching install found or invalid tag
37+
pass
38+
39+
40+
def test_default_command_args_parsing():
41+
"""Test that default command properly parses arguments."""
42+
cmd = commands.DefaultCommand([commands.DefaultCommand.CMD, "3.13"], None)
43+
assert cmd.args == ["3.13"]
44+
assert cmd.show_help is False
45+
46+
47+
def test_default_command_help_flag():
48+
"""Test that --help flag is recognized."""
49+
cmd = commands.DefaultCommand([commands.DefaultCommand.CMD, "--help"], None)
50+
assert cmd.show_help is True
51+
52+
53+
def test_default_command_class_attributes():
54+
"""Test that DefaultCommand has required attributes."""
55+
assert commands.DefaultCommand.CMD == "default"
56+
assert hasattr(commands.DefaultCommand, "HELP_LINE")
57+
assert hasattr(commands.DefaultCommand, "USAGE_LINE")
58+
assert hasattr(commands.DefaultCommand, "HELP_TEXT")
59+
assert hasattr(commands.DefaultCommand, "execute")

0 commit comments

Comments
 (0)