From 32be69140238985cdd50e63adaefdd538e152c01 Mon Sep 17 00:00:00 2001 From: NilayGanvit <79703214+NilayGanvit@users.noreply.github.com> Date: Sat, 31 Jan 2026 17:46:56 +0530 Subject: [PATCH] Change default version command - Enhancement #263 --- .gitignore | 1 + src/manage/commands.py | 38 +++++++++++ src/manage/default_command.py | 116 ++++++++++++++++++++++++++++++++++ src/manage/installs.py | 15 +++++ tests/test_default_command.py | 59 +++++++++++++++++ 5 files changed, 229 insertions(+) create mode 100644 src/manage/default_command.py create mode 100644 tests/test_default_command.py diff --git a/.gitignore b/.gitignore index b177635..cce950f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /env*/ /python-manager/ /pythons/ +/.venv/ # Can't seem to stop WiX from creating this directory... /src/pymanager/obj diff --git a/src/manage/commands.py b/src/manage/commands.py index b0cda96..4e9cc8a 100644 --- a/src/manage/commands.py +++ b/src/manage/commands.py @@ -85,6 +85,9 @@ r"Equivalent to -V:PythonCore\3!B!!W!. The version must begin " + "with a '3', platform overrides are permitted, and regular Python " + "options may follow. The runtime will be installed if needed."), + (f"{EXE_NAME} default !B!!W!\n", + "Set the default Python version to use when no specific version is " + + "requested. Use without !B!!W! to show the current default."), ] @@ -216,6 +219,10 @@ def execute(self): "help": ("show_help", True), # nested to avoid conflict with command }, + "default": { + "help": ("show_help", True), # nested to avoid conflict with command + }, + "**first_run": { "explicit": ("explicit", True), }, @@ -260,6 +267,9 @@ def execute(self): "enable_entrypoints": (config_bool, None), }, + "default": { + }, + "first_run": { "enabled": (config_bool, None, "env"), "explicit": (config_bool, None), @@ -1022,6 +1032,34 @@ def execute(self): os.startfile(HELP_URL) +class DefaultCommand(BaseCommand): + CMD = "default" + HELP_LINE = ("Show or change the default Python runtime version.") + USAGE_LINE = "default !B![]!W!" + HELP_TEXT = r"""!G!Default command!W! +Show or change the default Python version used by the system. + +> py default !B![options] []!W! + +With no arguments, shows the currently configured default Python version. +With a !B!!W!, sets the default Python version. + +!G!Examples:!W! +> py default +!W!Shows the current default Python version + +> py default 3.13 +!W!Sets Python 3.13 as the default version + +> py default 3 +!W!Sets the latest Python 3 as the default version +""" + + def execute(self): + from .default_command import execute + execute(self) + + def load_default_config(root): return DefaultConfig(root) diff --git a/src/manage/default_command.py b/src/manage/default_command.py new file mode 100644 index 0000000..474381c --- /dev/null +++ b/src/manage/default_command.py @@ -0,0 +1,116 @@ +"""Implementation of the 'default' command to manage default Python version.""" + +import json +from pathlib import Path as PathlibPath + +from .exceptions import ArgumentError, NoInstallsError, NoInstallFoundError +from .installs import get_installs, get_matching_install_tags +from .logging import LOGGER +from .pathutils import Path +from .tagutils import tag_or_range + + +def _get_default_config_file(install_dir): + """Get the path to the default install marker file.""" + return Path(install_dir) / ".default" + + +def _load_default_install_id(install_dir): + """Load the saved default install ID from the marker file.""" + try: + default_file = _get_default_config_file(install_dir) + if default_file.exists(): + return default_file.read_text(encoding="utf-8").strip() + except Exception as e: + LOGGER.debug("Failed to load default install ID: %s", e) + return None + + +def _save_default_install_id(install_dir, install_id): + """Save the default install ID to the marker file.""" + try: + default_file = _get_default_config_file(install_dir) + default_file.parent.mkdir(parents=True, exist_ok=True) + default_file.write_text(install_id, encoding="utf-8") + LOGGER.info("Default Python version set to: !G!%s!W!", install_id) + except Exception as e: + LOGGER.error("Failed to save default install ID: %s", e) + raise ArgumentError(f"Could not save default version: {e}") from e + + +def _show_current_default(cmd): + """Show the currently configured default Python version.""" + try: + installs = cmd.get_installs(set_default=False) + except NoInstallsError: + LOGGER.info("No Python installations found.") + return + + # Check if there's an explicit default marked + default_install = None + for install in installs: + if install.get("default"): + default_install = install + break + + if default_install: + LOGGER.print("!G!Current default:!W! %s", default_install["display-name"]) + LOGGER.print(" ID: %s", default_install["id"]) + LOGGER.print(" Version: %s", default_install.get("sort-version", "unknown")) + else: + LOGGER.print("!Y!No explicit default set.!W!") + LOGGER.print("Using tag-based default: !B!%s!W!", cmd.default_tag) + + +def _set_default_version(cmd, tag): + """Set a specific Python version as the default.""" + try: + installs = cmd.get_installs(set_default=False) + except NoInstallsError: + raise ArgumentError("No Python installations found. Install a version first with 'py install'.") from None + + if not installs: + raise ArgumentError("No Python installations found. Install a version first with 'py install'.") + + # Find the install matching the provided tag + try: + tag_obj = tag_or_range(tag) + except Exception as e: + raise ArgumentError(f"Invalid tag format: {tag}") from e + + matching = get_matching_install_tags( + installs, + tag_obj, + default_platform=cmd.default_platform, + single_tag=False, + ) + + if not matching: + raise NoInstallFoundError(tag=tag) + + selected_install, selected_run_for = matching[0] + + # Save the install ID as the default + _save_default_install_id(cmd.install_dir, selected_install["id"]) + + LOGGER.info("Default Python version set to: !G!%s!W! (%s)", + selected_install["display-name"], + selected_install["id"]) + + +def execute(cmd): + """Execute the default command.""" + cmd.show_welcome() + + if cmd.show_help: + cmd.help() + return + + if not cmd.args: + # Show current default + _show_current_default(cmd) + else: + # Set new default + tag = " ".join(cmd.args[0:1]) # Take the first argument as the tag + _set_default_version(cmd, tag) + diff --git a/src/manage/installs.py b/src/manage/installs.py index 4b1dfa5..99c1f5c 100644 --- a/src/manage/installs.py +++ b/src/manage/installs.py @@ -117,9 +117,24 @@ def get_installs( except LookupError: LOGGER.debug("No virtual environment found") + # Check for a saved default install marker + try: + default_file = Path(install_dir) / ".default" + if default_file.exists(): + default_id = default_file.read_text(encoding="utf-8").strip() + LOGGER.debug("Found saved default install ID: %s", default_id) + for install in installs: + if install["id"] == default_id: + install["default"] = True + LOGGER.debug("Marked %s as default", default_id) + break + except Exception as ex: + LOGGER.debug("Could not load default install marker: %s", ex) + return installs + def _make_alias_key(alias): n1, sep, n3 = alias.rpartition(".") n2 = "" diff --git a/tests/test_default_command.py b/tests/test_default_command.py new file mode 100644 index 0000000..b2fda26 --- /dev/null +++ b/tests/test_default_command.py @@ -0,0 +1,59 @@ +"""Tests for the default command.""" + +import pytest +from manage import commands +from manage.exceptions import ArgumentError, NoInstallsError + + +def test_default_command_help(assert_log): + """Test the default command help output.""" + cmd = commands.DefaultCommand([commands.DefaultCommand.CMD, "--help"], None) + cmd.execute() + assert_log( + assert_log.skip_until(".*Default command.*"), + ) + + +def test_default_command_no_args_no_installs(assert_log): + """Test default command with no arguments and no installations.""" + cmd = commands.DefaultCommand([commands.DefaultCommand.CMD], None) + # This should handle the case gracefully + # We expect it to either show a message about no installs or show current default + # The actual behavior depends on how get_installs works + try: + cmd.execute() + except NoInstallsError: + # This is acceptable - no installs available + pass + + +def test_default_command_with_invalid_tag(): + """Test default command with an invalid tag.""" + cmd = commands.DefaultCommand([commands.DefaultCommand.CMD, "invalid-tag"], None) + try: + cmd.execute() + except (ArgumentError, NoInstallsError): + # Expected - no matching install found or invalid tag + pass + + +def test_default_command_args_parsing(): + """Test that default command properly parses arguments.""" + cmd = commands.DefaultCommand([commands.DefaultCommand.CMD, "3.13"], None) + assert cmd.args == ["3.13"] + assert cmd.show_help is False + + +def test_default_command_help_flag(): + """Test that --help flag is recognized.""" + cmd = commands.DefaultCommand([commands.DefaultCommand.CMD, "--help"], None) + assert cmd.show_help is True + + +def test_default_command_class_attributes(): + """Test that DefaultCommand has required attributes.""" + assert commands.DefaultCommand.CMD == "default" + assert hasattr(commands.DefaultCommand, "HELP_LINE") + assert hasattr(commands.DefaultCommand, "USAGE_LINE") + assert hasattr(commands.DefaultCommand, "HELP_TEXT") + assert hasattr(commands.DefaultCommand, "execute")