From 97a76245d3242e6c43dc187e3cb3c1cde13e33d9 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sat, 20 Dec 2025 16:12:17 -0700 Subject: [PATCH] Ensure default versions are always correct relenv create, build, and fetch should all have proper default versions. This change ensures the defaults are updated dynamically as our python versions are updated. --- relenv/build/__init__.py | 44 +++++++++--------- relenv/common.py | 2 - relenv/create.py | 44 ++++++++---------- relenv/fetch.py | 10 +++-- relenv/pyversions.py | 48 ++++++++++++++++++++ tests/test_pyversions_runtime.py | 77 ++++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 56 deletions(-) diff --git a/relenv/build/__init__.py b/relenv/build/__init__.py index 14613727..a5d91c0a 100644 --- a/relenv/build/__init__.py +++ b/relenv/build/__init__.py @@ -15,8 +15,13 @@ from . import darwin, linux, windows from .common import builds -from ..common import DEFAULT_PYTHON, build_arch -from ..pyversions import Version, python_versions +from ..common import build_arch +from ..pyversions import ( + Version, + get_default_python_version, + python_versions, + resolve_python_version, +) def platform_module() -> ModuleType: @@ -62,11 +67,12 @@ def setup_parser( "logs, src, build, and previous tarball." ), ) + default_version = get_default_python_version() build_subparser.add_argument( "--python", - default=DEFAULT_PYTHON, + default=default_version, type=str, - help="The python version [default: %(default)s]", + help="The python version (e.g., 3.10, 3.13.7) [default: %(default)s]", ) build_subparser.add_argument( "--no-cleanup", @@ -146,27 +152,17 @@ def main(args: argparse.Namespace) -> None: print(f"Unsupported platform: {sys.platform}") sys.exit(1) - requested = Version(args.python) - - if requested.micro: + try: + build_version_str = resolve_python_version(args.python) + except RuntimeError as e: + print(f"Error: {e}") pyversions = python_versions() - if requested not in pyversions: - print(f"Unknown version {requested}") - strversions = "\n".join([str(_) for _ in pyversions]) - print(f"Known versions are:\n{strversions}") - sys.exit(1) - build_version = requested - else: - pyversions = python_versions(args.python) - build_version = sorted(list(pyversions.keys()))[-1] - - # print(pyversions) - # print(pyversions[0].major) - # print(pyversions[0].minor) - # print(pyversions[0].micro) - # print(pyversions[0].pre) - # print(pyversions[0].post) - # print(pyversions) + strversions = "\n".join([str(_) for _ in pyversions]) + print(f"Known versions are:\n{strversions}") + sys.exit(1) + + build_version = Version(build_version_str) + pyversions = python_versions() print(f"Build Python {build_version}") # XXX diff --git a/relenv/common.py b/relenv/common.py index 70f88f38..f73817ae 100644 --- a/relenv/common.py +++ b/relenv/common.py @@ -40,8 +40,6 @@ MODULE_DIR = pathlib.Path(__file__).resolve().parent -DEFAULT_PYTHON = "3.10.18" - LINUX = "linux" WIN32 = "win32" DARWIN = "darwin" diff --git a/relenv/create.py b/relenv/create.py index d8b5c3e6..e9145f3c 100644 --- a/relenv/create.py +++ b/relenv/create.py @@ -23,7 +23,11 @@ format_shebang, relative_interpreter, ) -from .pyversions import Version, python_versions +from .pyversions import ( + get_default_python_version, + python_versions, + resolve_python_version, +) @contextlib.contextmanager @@ -73,11 +77,12 @@ def setup_parser( type=str, help="The host architecture [default: %(default)s]", ) + default_version = get_default_python_version() subparser.add_argument( "--python", - default="3.10.17", + default=default_version, type=str, - help="The python version [default: %(default)s]", + help="The python version (e.g., 3.10, 3.13.7) [default: %(default)s]", ) @@ -106,8 +111,9 @@ def create( else: writeto = pathlib.Path(name).resolve() + # Version should be provided by main(), but handle None just in case if version is None: - version = "3.10.17" + version = get_default_python_version() if pathlib.Path(writeto).exists(): raise CreateException("The requested path already exists.") @@ -253,31 +259,17 @@ def main(args: argparse.Namespace) -> None: "Warning: Cross compilation support is experimental and is not fully tested or working!" ) - # Resolve version (support minor version like "3.12" or full version like "3.12.7") - requested = Version(args.python) - - if requested.micro: - # Full version specified (e.g., "3.12.7") + try: + create_version = resolve_python_version(args.python) + except RuntimeError as e: + print(f"Error: {e}") pyversions = python_versions() - if requested not in pyversions: - print(f"Unknown version {requested}") - strversions = "\n".join([str(_) for _ in pyversions]) - print(f"Known versions are:\n{strversions}") - sys.exit(1) - create_version = requested - else: - # Minor version specified (e.g., "3.12"), resolve to latest - pyversions = python_versions(args.python) - if not pyversions: - print(f"Unknown minor version {requested}") - all_versions = python_versions() - strversions = "\n".join([str(_) for _ in all_versions]) - print(f"Known versions are:\n{strversions}") - sys.exit(1) - create_version = sorted(list(pyversions.keys()))[-1] + strversions = "\n".join([str(_) for _ in pyversions]) + print(f"Known versions are:\n{strversions}") + sys.exit(1) try: - create(name, arch=args.arch, version=str(create_version)) + create(name, arch=args.arch, version=create_version) except CreateException as exc: print(exc) sys.exit(1) diff --git a/relenv/fetch.py b/relenv/fetch.py index 59ae25a0..b814c48e 100644 --- a/relenv/fetch.py +++ b/relenv/fetch.py @@ -16,7 +16,6 @@ from .common import ( CHECK_HOSTS, DATA_DIR, - DEFAULT_PYTHON, __version__, build_arch, check_url, @@ -24,6 +23,7 @@ get_triplet, work_dir, ) +from .pyversions import get_default_python_version, resolve_python_version def setup_parser( @@ -45,11 +45,12 @@ def setup_parser( type=str, help="Architecture to download. [default: %(default)s]", ) + default_version = get_default_python_version() subparser.add_argument( "--python", - default=DEFAULT_PYTHON, + default=default_version, type=str, - help="The python version [default: %(default)s]", + help="The python version (e.g., 3.10, 3.13.7) [default: %(default)s]", ) @@ -87,7 +88,8 @@ def main(args: argparse.Namespace) -> None: """ version = os.environ.get("RELENV_FETCH_VERSION", __version__) triplet = get_triplet(machine=args.arch) - python = args.python + # args.python will be the default version or user-specified version + python = resolve_python_version(args.python) check_hosts = CHECK_HOSTS if os.environ.get("RELENV_FETCH_HOST", ""): check_hosts = [os.environ["RELENV_FETCH_HOST"]] diff --git a/relenv/pyversions.py b/relenv/pyversions.py index e191afcc..f501631c 100644 --- a/relenv/pyversions.py +++ b/relenv/pyversions.py @@ -947,6 +947,54 @@ def python_versions( return {version: pyversions[str(version)] for version in versions} +def get_default_python_version() -> str: + """ + Get the default Python version to use when none is specified. + + :return: The default Python version string (e.g., "3.10.19") + """ + # Default to latest 3.10 version + pyversions = python_versions("3.10") + if not pyversions: + raise RuntimeError("No 3.10 versions found") + latest = sorted(list(pyversions.keys()))[-1] + return str(latest) + + +def resolve_python_version(version_spec: str | None = None) -> str: + """ + Resolve a Python version specification to a full version string. + + If version_spec is None, returns the latest Python 3.10 version. + If version_spec is partial (e.g., "3.10"), returns the latest micro version. + If version_spec is full (e.g., "3.10.19"), returns it as-is after validation. + + :param version_spec: Version specification (None, "3.10", or "3.10.19") + :return: Full version string (e.g., "3.10.19") + :raises RuntimeError: If the version is not found + """ + if version_spec is None: + # Default to latest 3.10 version + return get_default_python_version() + + requested = Version(version_spec) + + if requested.micro is not None: + # Full version specified - validate it exists + pyversions = python_versions() + if requested not in pyversions: + raise RuntimeError(f"Unknown version {requested}") + return str(requested) + else: + # Partial version (major.minor) - get latest micro + pyversions = python_versions(version_spec) + if not pyversions: + raise RuntimeError(f"Unknown minor version {requested}") + # Return the latest version for this major.minor + latest = sorted(list(pyversions.keys()))[-1] + return str(latest) + + def setup_parser( subparsers: argparse._SubParsersAction[argparse.ArgumentParser], ) -> None: diff --git a/tests/test_pyversions_runtime.py b/tests/test_pyversions_runtime.py index 1158ce28..0daf8907 100644 --- a/tests/test_pyversions_runtime.py +++ b/tests/test_pyversions_runtime.py @@ -175,3 +175,80 @@ def fake_fetch(url: str) -> str: assert "5.6.3" in versions # Verify sorting (latest first) assert versions[0] == "5.8.1" + + +def test_resolve_python_version_none_defaults_to_latest_310() -> None: + """Test that None resolves to the latest 3.10 version.""" + result = pyversions.resolve_python_version(None) + assert result.startswith("3.10.") + # Verify it's a valid version in the registry + versions = pyversions.python_versions("3.10") + assert pyversions.Version(result) in versions + # Verify it's the latest 3.10 version + latest = sorted(list(versions.keys()))[-1] + assert result == str(latest) + + +def test_resolve_python_version_partial_minor() -> None: + """Test that partial versions (3.10) resolve to latest micro.""" + result = pyversions.resolve_python_version("3.10") + assert result.startswith("3.10.") + # Verify it resolves to the latest micro version + versions = pyversions.python_versions("3.10") + latest = sorted(list(versions.keys()))[-1] + assert result == str(latest) + + +def test_resolve_python_version_different_minors() -> None: + """Test resolution works for different minor versions.""" + result_311 = pyversions.resolve_python_version("3.11") + assert result_311.startswith("3.11.") + + result_313 = pyversions.resolve_python_version("3.13") + assert result_313.startswith("3.13.") + + # Verify they're different + assert result_311 != result_313 + + # Verify each is the latest for its minor version + versions_311 = pyversions.python_versions("3.11") + latest_311 = sorted(list(versions_311.keys()))[-1] + assert result_311 == str(latest_311) + + versions_313 = pyversions.python_versions("3.13") + latest_313 = sorted(list(versions_313.keys()))[-1] + assert result_313 == str(latest_313) + + +def test_resolve_python_version_full_version() -> None: + """Test that full versions are validated and returned as-is.""" + # Get any valid version from the registry + all_versions = pyversions.python_versions() + some_version = str(next(iter(all_versions))) + + result = pyversions.resolve_python_version(some_version) + assert result == some_version + + +def test_resolve_python_version_invalid_full_version() -> None: + """Test that invalid full versions raise RuntimeError.""" + with pytest.raises(RuntimeError, match="Unknown version"): + pyversions.resolve_python_version("3.10.999") + + +def test_resolve_python_version_invalid_minor_version() -> None: + """Test that invalid minor versions raise RuntimeError.""" + with pytest.raises(RuntimeError, match="Unknown minor version"): + pyversions.resolve_python_version("3.99") + + +def test_resolve_python_version_consistency() -> None: + """Test that resolve_python_version is idempotent for full versions.""" + # Get a valid full version from the registry + all_versions = pyversions.python_versions() + some_version = str(next(iter(all_versions))) + + # Resolving a full version twice should give the same result + first = pyversions.resolve_python_version(some_version) + second = pyversions.resolve_python_version(first) + assert first == second