diff --git a/relenv/create.py b/relenv/create.py index 7595b936..d8b5c3e6 100644 --- a/relenv/create.py +++ b/relenv/create.py @@ -23,6 +23,7 @@ format_shebang, relative_interpreter, ) +from .pyversions import Version, python_versions @contextlib.contextmanager @@ -251,8 +252,32 @@ def main(args: argparse.Namespace) -> None: print( "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") + 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] + try: - create(name, arch=args.arch, version=args.python) + create(name, arch=args.arch, version=str(create_version)) except CreateException as exc: print(exc) sys.exit(1) diff --git a/tests/test_create.py b/tests/test_create.py index d8884f1d..6d4d6f31 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -52,3 +52,146 @@ def test_create_arches_directory_exists(tmp_path: pathlib.Path) -> None: with patch("relenv.create.arches", mocked_arches): with pytest.raises(CreateException): create("foo", dest=tmp_path) + + +def test_create_with_minor_version(tmp_path: pathlib.Path) -> None: + """Test that minor version (e.g., '3.12') resolves to latest micro version.""" + import argparse + import sys + + from relenv.create import main + from relenv.pyversions import Version + + # Mock python_versions to return some test versions + all_versions = { + Version("3.11.5"): "aaa111", + Version("3.12.5"): "abc123", + Version("3.12.6"): "def456", + Version("3.12.7"): "ghi789", + Version("3.13.1"): "zzz999", + } + + def mock_python_versions(minor: str | None = None) -> dict[Version, str]: + """Mock that filters versions by minor version like the real function.""" + if minor is None: + return all_versions + # Filter versions matching the minor version + mv = Version(minor) + return { + v: h + for v, h in all_versions.items() + if v.major == mv.major and v.minor == mv.minor + } + + # Create a fake archive + to_be_archived = tmp_path / "to_be_archived" + to_be_archived.mkdir() + test_file = to_be_archived / "testfile" + test_file.touch() + tar_file = tmp_path / "fake_archive" + with tarfile.open(str(tar_file), "w:xz") as tar: + tar.add(str(to_be_archived), to_be_archived.name) + + # Use appropriate architecture for the platform + test_arch = "amd64" if sys.platform == "win32" else "x86_64" + args = argparse.Namespace(name="test_env", arch=test_arch, python="3.12") + + with chdir(str(tmp_path)): + with patch("relenv.create.python_versions", side_effect=mock_python_versions): + with patch("relenv.create.archived_build", return_value=tar_file): + with patch("relenv.create.build_arch", return_value=test_arch): + main(args) + + to_dir = tmp_path / "test_env" + assert to_dir.exists() + + +def test_create_with_full_version(tmp_path: pathlib.Path) -> None: + """Test that full version (e.g., '3.12.7') still works.""" + import argparse + import sys + + from relenv.create import main + from relenv.pyversions import Version + + # Mock python_versions to return some test versions + all_versions = { + Version("3.11.5"): "aaa111", + Version("3.12.5"): "abc123", + Version("3.12.6"): "def456", + Version("3.12.7"): "ghi789", + Version("3.13.1"): "zzz999", + } + + def mock_python_versions(minor: str | None = None) -> dict[Version, str]: + """Mock that filters versions by minor version like the real function.""" + if minor is None: + return all_versions + # Filter versions matching the minor version + mv = Version(minor) + return { + v: h + for v, h in all_versions.items() + if v.major == mv.major and v.minor == mv.minor + } + + # Create a fake archive + to_be_archived = tmp_path / "to_be_archived" + to_be_archived.mkdir() + test_file = to_be_archived / "testfile" + test_file.touch() + tar_file = tmp_path / "fake_archive" + with tarfile.open(str(tar_file), "w:xz") as tar: + tar.add(str(to_be_archived), to_be_archived.name) + + # Use appropriate architecture for the platform + test_arch = "amd64" if sys.platform == "win32" else "x86_64" + args = argparse.Namespace(name="test_env", arch=test_arch, python="3.12.7") + + with chdir(str(tmp_path)): + with patch("relenv.create.python_versions", side_effect=mock_python_versions): + with patch("relenv.create.archived_build", return_value=tar_file): + with patch("relenv.create.build_arch", return_value=test_arch): + main(args) + + to_dir = tmp_path / "test_env" + assert to_dir.exists() + + +def test_create_with_unknown_minor_version(tmp_path: pathlib.Path) -> None: + """Test that unknown minor version produces an error.""" + import argparse + import sys + + from relenv.create import main + from relenv.pyversions import Version + + # Mock python_versions to return empty dict for unknown version + all_versions = { + Version("3.11.5"): "aaa111", + Version("3.12.5"): "abc123", + Version("3.12.6"): "def456", + Version("3.12.7"): "ghi789", + Version("3.13.1"): "zzz999", + } + + # Use appropriate architecture for the platform + test_arch = "amd64" if sys.platform == "win32" else "x86_64" + args = argparse.Namespace(name="test_env", arch=test_arch, python="3.99") + + def mock_python_versions(minor: str | None = None) -> dict[Version, str]: + """Mock that filters versions by minor version like the real function.""" + if minor is None: + return all_versions + # Filter versions matching the minor version + mv = Version(minor) + return { + v: h + for v, h in all_versions.items() + if v.major == mv.major and v.minor == mv.minor + } + + with patch("relenv.create.python_versions", side_effect=mock_python_versions): + with patch("relenv.create.build_arch", return_value=test_arch): + with pytest.raises(SystemExit): + main(args)