From 5ea8513385d0faff25a77feb77a371d9029851e2 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 16 Sep 2025 07:51:14 -0700 Subject: [PATCH 1/3] Add helper function for version ranges --- constructor/main.py | 25 +++++++++++++++---------- constructor/utils.py | 27 +++++++++++++++++++++++++++ tests/test_examples.py | 25 ++++++++++++------------- 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/constructor/main.py b/constructor/main.py index c3c1e61ef..6a7b33755 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -25,7 +25,7 @@ from .construct import parse as construct_parse from .construct import verify as construct_verify from .fcp import main as fcp_main -from .utils import StandaloneExe, identify_conda_exe, normalize_path, yield_lines +from .utils import StandaloneExe, check_version, identify_conda_exe, normalize_path, yield_lines DEFAULT_CACHE_DIR = os.getenv("CONSTRUCTOR_CACHE", "~/.conda/constructor") @@ -118,7 +118,7 @@ def main_build( sys.exit("Error: micromamba is not supported on Windows installers.") if info.get("uninstall_with_conda_exe") and not ( - exe_type == StandaloneExe.CONDA and exe_version and exe_version >= Version("24.11.0") + exe_type == StandaloneExe.CONDA and check_version(exe_version, min_version="24.11.0") ): sys.exit("Error: uninstalling with conda.exe requires conda-standalone 24.11.0 or newer.") @@ -177,13 +177,18 @@ def main_build( new_extras.append({orig: dest}) info[extra_type] = new_extras - if (any((isinstance(path, str) and "/conda-meta/frozen" in path) or - (isinstance(path, dict) and any("conda-meta/frozen" in v for v in path.values())) - for path in info.get("extra_files", [])) + if ( + any( + (isinstance(path, str) and "/conda-meta/frozen" in path) + or (isinstance(path, dict) and any("conda-meta/frozen" in v for v in path.values())) + for path in info.get("extra_files", []) + ) and exe_type == StandaloneExe.CONDA - and exe_version - and exe_version >= Version("25.5.0") and exe_version < Version("25.7.0")): - sys.exit("Error: installing with protected base environment requires conda-standalone newer than 25.5.x") + and check_version(exe_version, min_version="25.5.0", max_version="25.7.0") + ): + sys.exit( + "Error: installing with protected base environment requires conda-standalone newer than 25.5.x" + ) for key in "channels", "specs", "exclude", "packages", "menu_packages", "virtual_specs": if key in info: @@ -223,14 +228,14 @@ def main_build( "Will assume it is compatible with shortcuts." ) elif sys.platform != "win32" and ( - exe_type != StandaloneExe.CONDA or (exe_version and exe_version < Version("23.11.0")) + exe_type != StandaloneExe.CONDA or not check_version(exe_version, min_version="23.11.0") ): logger.warning("conda-standalone 23.11.0 or above is required for shortcuts on Unix.") info["_enable_shortcuts"] = "incompatible" # Add --no-rc option to CONDA_EXE command so that existing # .condarc files do not pollute the installation process. - if exe_type == StandaloneExe.CONDA and exe_version and exe_version >= Version("24.9.0"): + if exe_type == StandaloneExe.CONDA and check_version(exe_version, min_version="24.9.0"): info["_ignore_condarcs_arg"] = "--no-rc" elif exe_type == StandaloneExe.MAMBA: info["_ignore_condarcs_arg"] = "--no-rc" diff --git a/constructor/utils.py b/constructor/utils.py index 33a629b7d..eb89a09a6 100644 --- a/constructor/utils.py +++ b/constructor/utils.py @@ -25,6 +25,13 @@ from ruamel.yaml import YAML +try: + from conda.models.version import VersionOrder + + has_conda_interface = True +except ImportError: + has_conda_interface = False + logger = logging.getLogger(__name__) yaml = YAML(typ="rt") yaml.default_flow_style = False @@ -343,6 +350,26 @@ def identify_conda_exe(conda_exe: str | Path | None = None) -> tuple[StandaloneE return None, None +def check_version( + exe_version: str | VersionOrder | None = None, + min_version: str | None = None, + max_version: str | None = None, +) -> bool: + """Check if a version is within a version range. + + The minimum version is assumed to be inclusive, the maximum version is not inclusive. + """ + if not exe_version or not has_conda_interface: + return False + if isinstance(exe_version, str): + exe_version = VersionOrder(exe_version) + if min_version and exe_version < VersionOrder(min_version): + return False + if max_version and exe_version >= VersionOrder(max_version): + return False + return True + + def win_str_esc(s, newlines=True): maps = [("$", "$$"), ('"', '$\\"'), ("\t", "$\\t")] if newlines: diff --git a/tests/test_examples.py b/tests/test_examples.py index 4b7960d9c..5c30b8a19 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -21,7 +21,7 @@ from conda.models.version import VersionOrder as Version from ruamel.yaml import YAML -from constructor.utils import StandaloneExe, identify_conda_exe +from constructor.utils import StandaloneExe, check_version, identify_conda_exe if TYPE_CHECKING: from collections.abc import Generator, Iterable @@ -502,8 +502,7 @@ def test_example_mirrored_channels(tmp_path, request): @pytest.mark.xfail( ( CONDA_EXE == StandaloneExe.CONDA - and CONDA_EXE_VERSION is not None - and CONDA_EXE_VERSION < Version("23.11.0a0") + and not check_version(CONDA_EXE_VERSION, min_version="23.11.0a0") ), reason="Known issue with conda-standalone<=23.10: shortcuts are created but not removed.", ) @@ -683,8 +682,7 @@ def test_example_scripts(tmp_path, request): @pytest.mark.skipif( ( CONDA_EXE == StandaloneExe.MAMBA - or CONDA_EXE_VERSION is None - or CONDA_EXE_VERSION < Version("23.11.0a0") + and not check_version(CONDA_EXE_VERSION, min_version="23.11.0a0") ), reason="menuinst v2 requires conda-standalone>=23.11.0; micromamba is not supported yet", ) @@ -1208,7 +1206,7 @@ def _get_dacl_information(filepath: Path) -> dict: @pytest.mark.xfail( - CONDA_EXE == StandaloneExe.CONDA and CONDA_EXE_VERSION < Version("24.9.0"), + CONDA_EXE == StandaloneExe.CONDA and not check_version(CONDA_EXE_VERSION, min_version="24.9.0"), reason="Pre-existing .condarc breaks installation", ) def test_ignore_condarc_files(tmp_path, monkeypatch, request): @@ -1258,7 +1256,7 @@ def test_ignore_condarc_files(tmp_path, monkeypatch, request): @pytest.mark.skipif( - CONDA_EXE == StandaloneExe.CONDA and CONDA_EXE_VERSION < Version("24.11.0"), + CONDA_EXE == StandaloneExe.CONDA and check_version(CONDA_EXE_VERSION, min_version="24.11.0"), reason="Requires conda-standalone 24.11.x or newer", ) @pytest.mark.skipif(not sys.platform == "win32", reason="Windows only") @@ -1371,13 +1369,14 @@ def test_regressions(tmp_path, request): uninstall=True, ) + @pytest.mark.xfail( - condition=(CONDA_EXE == StandaloneExe.CONDA and - CONDA_EXE_VERSION and - CONDA_EXE_VERSION >= Version("25.5.0") and - CONDA_EXE_VERSION < Version("25.7.0")), + condition=( + CONDA_EXE == StandaloneExe.CONDA + and check_version(CONDA_EXE_VERSION, min_version="25.5.0", max_version="25.7.0"), + ), reason="conda-standalone 25.5.x fails with protected base environments and older versions are ignored", - strict=True + strict=True, ) def test_frozen_environment(tmp_path, request): input_path = _example_path("protected_base") @@ -1389,6 +1388,6 @@ def test_frozen_environment(tmp_path, request): install_dir, request=request, check_subprocess=True, - uninstall=False + uninstall=False, ) assert frozen_file.exists() From 596f10dc622f5b30097259d401284d5da2b48790 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 16 Sep 2025 09:08:31 -0700 Subject: [PATCH 2/3] Remove stray comma --- tests/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 5c30b8a19..ef8824e14 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1373,7 +1373,7 @@ def test_regressions(tmp_path, request): @pytest.mark.xfail( condition=( CONDA_EXE == StandaloneExe.CONDA - and check_version(CONDA_EXE_VERSION, min_version="25.5.0", max_version="25.7.0"), + and check_version(CONDA_EXE_VERSION, min_version="25.5.0", max_version="25.7.0") ), reason="conda-standalone 25.5.x fails with protected base environments and older versions are ignored", strict=True, From 023f8154b420b488e215c3d7583f0e1803e301be Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 16 Sep 2025 13:55:17 -0700 Subject: [PATCH 3/3] Invert boolean logic for shortcut check --- constructor/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constructor/main.py b/constructor/main.py index 6a7b33755..147db20cd 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -228,7 +228,7 @@ def main_build( "Will assume it is compatible with shortcuts." ) elif sys.platform != "win32" and ( - exe_type != StandaloneExe.CONDA or not check_version(exe_version, min_version="23.11.0") + exe_type != StandaloneExe.CONDA or check_version(exe_version, max_version="23.11.0") ): logger.warning("conda-standalone 23.11.0 or above is required for shortcuts on Unix.") info["_enable_shortcuts"] = "incompatible"