From 9a4068faf700ee22ad801a4df327374e4550f24a Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 8 Apr 2024 15:35:00 -0400 Subject: [PATCH] feat: add micromamba support (#807) * feat: add micromamba support Signed-off-by: Henry Schreiner * tests: add micromamba test Signed-off-by: Henry Schreiner * tests: support Python 3.7 Signed-off-by: Henry Schreiner * fix: don't override user set channels for micromamba env creation Signed-off-by: Henry Schreiner --------- Signed-off-by: Henry Schreiner --- docs/config.rst | 6 +++--- docs/tutorial.rst | 10 ++++++--- docs/usage.rst | 12 +++++------ nox/sessions.py | 4 +++- nox/virtualenv.py | 14 +++++++++++-- noxfile.py | 31 +++++++++++++++++++++++---- tests/test_virtualenv.py | 45 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 19 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 53ab6931..9464b68a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -167,7 +167,7 @@ You can also specify that the virtualenv should *always* be reused instead of re def tests(session): pass -You are not limited to virtualenv, there is a selection of backends you can choose from as venv, uv, conda, mamba, or virtualenv (default): +You are not limited to virtualenv, there is a selection of backends you can choose from as venv, uv, conda, mamba, micromamba, or virtualenv (default): .. code-block:: python @@ -176,8 +176,8 @@ You are not limited to virtualenv, there is a selection of backends you can choo pass You can chain together optional backends with ``|``, such as ``uv|virtualenv`` -or ``mamba|conda``, and the first available backend will be selected. You -cannot put anything after a backend that can't be missing like ``venv`` or +or ``micromamba|mamba|conda``, and the first available backend will be selected. +You cannot put anything after a backend that can't be missing like ``venv`` or ``virtualenv``. Finally, custom backend parameters are supported: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 4b3131f1..e3420d8a 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -397,12 +397,13 @@ Install packages with conda: .. code-block:: python - session.conda_install("pytest") + session.conda_install("pytest", channels=["conda-forge"]) It is possible to install packages with pip into the conda environment, but it's a best practice only install pip packages with the ``--no-deps`` option. -This prevents pip from breaking the conda environment by installing -incompatible versions of packages already installed with conda. +This prevents pip from breaking the conda environment by installing incompatible +versions of packages already installed with conda. You should always specify +channels for consistency; default channels can vary (and ``micromamba`` has none). .. code-block:: python @@ -412,6 +413,9 @@ incompatible versions of packages already installed with conda. ``"mamba"`` is also allowed as a choice for ``venv_backend``, which will use/require `mamba `_ instead of conda. +``"micromamba"`` is also allowed as a choice for ``venv_backend``, which will +use/require `micromamba `_ +instead of conda. Parametrization --------------- diff --git a/docs/usage.rst b/docs/usage.rst index 887b4d15..069f123a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -126,7 +126,7 @@ Then running ``nox --session tests`` will actually run all parametrized versions Changing the sessions default backend ------------------------------------- -By default Nox uses ``virtualenv`` as the virtual environment backend for the sessions, but it also supports ``uv``, ``conda``, ``mamba``, and ``venv`` as well as no backend (passthrough to whatever python environment Nox is running on). You can change the default behaviour by using ``-db `` or ``--default-venv-backend ``. Supported names are ``('none', 'uv', 'virtualenv', 'conda', 'mamba', 'venv')``. +By default Nox uses ``virtualenv`` as the virtual environment backend for the sessions, but it also supports ``uv``, ``conda``, ``mamba``, ``micromamba``, and ``venv`` as well as no backend (passthrough to whatever python environment Nox is running on). You can change the default behaviour by using ``-db `` or ``--default-venv-backend ``. Supported names are ``('none', 'uv', 'virtualenv', 'conda', 'mamba', 'venv')``. .. tabs:: @@ -142,9 +142,9 @@ By default Nox uses ``virtualenv`` as the virtual environment backend for the se .. note:: - The ``uv``, ``conda``, and ``mamba`` backends require their respective - programs be pre-installed. ``uv`` is distributed as a Python package - and can be installed with the ``nox[uv]`` extra. + The ``uv``, ``conda``, ``mamba``, and ``micromamba`` backends require their + respective programs be pre-installed. ``uv`` is distributed as a Python + package and can be installed with the ``nox[uv]`` extra. You can also set this option with the ``NOX_DEFAULT_VENV_BACKEND`` environment variable, or in the Noxfile with ``nox.options.default_venv_backend``. In case more than one is provided, the command line argument overrides the environment variable, which in turn overrides the Noxfile configuration. @@ -156,7 +156,7 @@ Note that using this option does not change the backend for sessions where ``ven as ``uv pip`` is used to install programs instead. If you need to manually interact with pip, you should install it with ``session.install("pip")``. -Backends that could be missing (``uv``, ``conda``, and ``mamba``) can have a fallback using ``|``, such as ``uv|virtualenv`` or ``mamba|conda``. This will use the first item that is available on the users system. +Backends that could be missing (``uv``, ``conda``, ``mamba``, and ``micromamba``) can have a fallback using ``|``, such as ``uv|virtualenv`` or ``micromamba|mamba|conda``. This will use the first item that is available on the users system. If you need to check to see which backend was selected, you can access it via ``session.venv_backend`` in your noxfile. @@ -166,7 +166,7 @@ If you need to check to see which backend was selected, you can access it via Forcing the sessions backend ---------------------------- -You might work in a different environment than a project's default continuous integration settings, and might wish to get a quick way to execute the same tasks but on a different venv backend. For this purpose, you can temporarily force the backend used by **all** sessions in the current Nox execution by using ``-fb `` or ``--force-venv-backend ``. No exceptions are made, the backend will be forced for all sessions run whatever the other options values and Noxfile configuration. Supported names are ``('none', 'uv', 'virtualenv', 'conda', 'mamba', 'venv')``. +You might work in a different environment than a project's default continuous integration settings, and might wish to get a quick way to execute the same tasks but on a different venv backend. For this purpose, you can temporarily force the backend used by **all** sessions in the current Nox execution by using ``-fb `` or ``--force-venv-backend ``. No exceptions are made, the backend will be forced for all sessions run whatever the other options values and Noxfile configuration. Supported names are ``('none', 'uv', 'virtualenv', 'conda', 'mamba', 'micromamba', 'venv')``. .. code-block:: console diff --git a/nox/sessions.py b/nox/sessions.py index 57cc7bd0..1544c3f4 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -554,7 +554,9 @@ def conda_install( You can specify a conda channel using `channel=`; a falsey value will not change the current channels. You can specify a list of channels if - needed. + needed. It is highly recommended to specify this; micromamba does not + set default channels, and default channels vary for conda. Note that + "defaults" is also not permissivly licenced like "conda-forge" is. Additional keyword args are the same as for :meth:`run`. diff --git a/nox/virtualenv.py b/nox/virtualenv.py index b5610540..bf93f9f9 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -236,7 +236,7 @@ class CondaEnv(ProcessEnv): """ is_sandboxed = True - allowed_globals = ("conda", "mamba") + allowed_globals = ("conda", "mamba", "micromamba") def __init__( self, @@ -305,6 +305,12 @@ def create(self) -> bool: return False cmd = [self.conda_cmd, "create", "--yes", "--prefix", self.location] + if self.conda_cmd == "micromamba" and not any( + v.startswith(("--channel=", "-c")) or v == "--channel" + for v in self.venv_params + ): + # Micromamba doesn't have any default channels + cmd.append("--channel=conda-forge") cmd.extend(self.venv_params) @@ -314,7 +320,9 @@ def create(self) -> bool: python_dep = f"python={self.interpreter}" if self.interpreter else "python" cmd.append(python_dep) - logger.info(f"Creating conda env in {self.location_name} with {python_dep}") + logger.info( + f"Creating {self.conda_cmd} env in {self.location_name} with {python_dep}" + ) nox.command.run(cmd, silent=True, log=nox.options.verbose or False) return True @@ -589,6 +597,7 @@ def venv_backend(self) -> str: ALL_VENVS: dict[str, Callable[..., ProcessEnv]] = { "conda": functools.partial(CondaEnv, conda_cmd="conda"), "mamba": functools.partial(CondaEnv, conda_cmd="mamba"), + "micromamba": functools.partial(CondaEnv, conda_cmd="micromamba"), "virtualenv": functools.partial(VirtualEnv, venv_backend="virtualenv"), "venv": functools.partial(VirtualEnv, venv_backend="venv"), "uv": functools.partial(VirtualEnv, venv_backend="uv"), @@ -601,5 +610,6 @@ def venv_backend(self) -> str: OPTIONAL_VENVS = { "conda": shutil.which("conda") is not None, "mamba": shutil.which("mamba") is not None, + "micromamba": shutil.which("micromamba") is not None, "uv": HAS_UV, } diff --git a/noxfile.py b/noxfile.py index a151e5b1..7f3e4bb1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -34,6 +34,10 @@ nox.options.sessions = ["tests", "cover", "lint", "docs"] if shutil.which("conda"): nox.options.sessions.append("conda_tests") +if shutil.which("mamba"): + nox.options.sessions.append("mamba_tests") +if shutil.which("micromamba"): + nox.options.sessions.append("micromamba_tests") # Because there is a dependency conflict between argcomplete and the latest tox @@ -79,12 +83,31 @@ def tests(session: nox.Session, tox_version: str) -> None: con.execute("DELETE FROM file WHERE SUBSTR(path, 2, 1) == ':'") -@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"], venv_backend="conda") +@nox.session(venv_backend="conda") def conda_tests(session: nox.Session) -> None: - """Run test suite with pytest.""" - session.create_tmp() # Fixes permission errors on Windows + """Run test suite set up with conda.""" + session.conda_install( + "--file", "requirements-conda-test.txt", channel="conda-forge" + ) + session.install("-e", ".", "--no-deps") + session.run("pytest", *session.posargs) + + +@nox.session(venv_backend="mamba") +def mamba_tests(session: nox.Session) -> None: + """Run test suite set up with mamba.""" + session.conda_install( + "--file", "requirements-conda-test.txt", channel="conda-forge" + ) + session.install("-e", ".", "--no-deps") + session.run("pytest", *session.posargs) + + +@nox.session(venv_backend="micromamba") +def micromamba_tests(session: nox.Session) -> None: + """Run test suite set up with micromamba.""" session.conda_install( - "--file", "requirements-conda-test.txt", "--channel", "conda-forge" + "--file", "requirements-conda-test.txt", channel="conda-forge" ) session.install("-e", ".", "--no-deps") session.run("pytest", *session.posargs) diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 340c2f98..f1dfb82d 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -483,6 +483,51 @@ def test_reuse_conda_environment(make_one): assert reused +# This mocks micromamba so that it doesn't need to be installed. +@has_conda +def test_micromamba_environment(make_one, monkeypatch): + conda_path = shutil.which("conda") + which = shutil.which + monkeypatch.setattr( + shutil, "which", lambda x: conda_path if x == "micromamba" else which(x) + ) + venv, _ = make_one(reuse_existing=True, venv_backend="micromamba") + run = mock.Mock() + monkeypatch.setattr(nox.command, "run", run) + venv.create() + run.assert_called_once() + # .args requires Python 3.8+ + ((args,), _) = run.call_args + assert args[0] == "micromamba" + assert "--channel=conda-forge" in args + + +# This mocks micromamba so that it doesn't need to be installed. +@pytest.mark.parametrize( + "params", + [["--channel=default"], ["-cdefault"], ["-c", "default"], ["--channel", "default"]], +) +@has_conda +def test_micromamba_channel_environment(make_one, monkeypatch, params): + conda_path = shutil.which("conda") + which = shutil.which + monkeypatch.setattr( + shutil, "which", lambda x: conda_path if x == "micromamba" else which(x) + ) + venv, _ = make_one(reuse_existing=True, venv_backend="micromamba") + run = mock.Mock() + monkeypatch.setattr(nox.command, "run", run) + venv.venv_params = params + venv.create() + run.assert_called_once() + # .args requires Python 3.8+ + ((args,), _) = run.call_args + assert args[0] == "micromamba" + for p in params: + assert p in args + assert "--channel=conda-forge" not in args + + @pytest.mark.parametrize( ("frm", "to", "result"), [