Skip to content
Open
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
44 changes: 20 additions & 24 deletions relenv/build/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions relenv/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@

MODULE_DIR = pathlib.Path(__file__).resolve().parent

DEFAULT_PYTHON = "3.10.18"

LINUX = "linux"
WIN32 = "win32"
DARWIN = "darwin"
Expand Down
44 changes: 18 additions & 26 deletions relenv/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]",
)


Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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)
10 changes: 6 additions & 4 deletions relenv/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
from .common import (
CHECK_HOSTS,
DATA_DIR,
DEFAULT_PYTHON,
__version__,
build_arch,
check_url,
download_url,
get_triplet,
work_dir,
)
from .pyversions import get_default_python_version, resolve_python_version


def setup_parser(
Expand All @@ -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]",
)


Expand Down Expand Up @@ -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"]]
Expand Down
48 changes: 48 additions & 0 deletions relenv/pyversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
77 changes: 77 additions & 0 deletions tests/test_pyversions_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading