Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow passing callable venv_backend for creation of session virtualenv #753

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
160 changes: 160 additions & 0 deletions docs/cookbook.rst
Expand Up @@ -78,6 +78,166 @@ Enter the ``dev`` nox session:
With this, a user can simply run ``nox -s dev`` and have their entire environment set up automatically!


Instant Dev Environment using callable venv_backend
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

As an alternative to the above, you can instead invoke a custom callable for ``venv_backend`` to create a development nox session.

.. code-block:: python

from nox.sessions import SessionRunner
from nox.virtualenv import VirtualEnv


def create_venv_override_location(
location: str,
interpreter: str | None,
reuse_existing: bool,
venv_params: Any,
runner: SessionRunner,
) -> VirtualEnv:
"""
Override location of virtualenv

To set the location, pass `venv_params = {"location": path/to/.venv, "venv_params": ...}`
where `venv_params[venv_params]` will be passed to `VirtualEnv` creation.
"""

if not isinstance(venv_params, dict) or "location" not in venv_params:
raise ValueError("must supply `venv_backend = {'location': path, ...}")

# Override the virtual environment location
location = venv_params["location"]
assert isinstance(location, str)

venv = VirtualEnv(
location=location,
interpreter=interpreter,
reuse_existing=reuse_existing,
venv_params=venv_params.get("venv_params"),
)

venv.create()
return venv


@nox.session(
python="3.11",
venv_backend=create_venv_override_location,
venv_params={"location": ".venv"},
)
def dev(session: nox.Session) -> None:
"""Easy way to create a development environment

This will place the development environment in the `.venv` directory
"""
session.install("-e", ".[dev]")




Create environment with ``conda env create``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It's common to create conda environments directly from an ``environment.yaml``
file with ``conda env create``. There are also cases where using ``micromamba`` is advantageous.
To do this with nox, you can use a callable ``venv_backend``. For example:

.. code-block:: python

import os
from nox.sessions import SessionRunner
from nox.virtualenv import CondaEnv
from nox.logger import logger


def factory_conda_env(backend: str):
"""Create a callable backend with specified conda backend."""
# Override CondaEnv backend
CondaEnv.allowed_globals = ("conda", "mamba", "micromamba") # type: ignore[assignment]

if backend not in CondaEnv.allowed_globals:
msg = f"{backend=} must be in {CondaEnv.allowed_globals}"
raise ValueError(msg)


def create_conda_env(
location: str,
interpreter: str | None,
reuse_existing: bool,
venv_params: Any,
runner: SessionRunner,
) -> CondaEnv:
"""
Custom venv_backend to create conda environment from `environment.yaml` file

This particular callable infers the file from the interpreter. For example,
if `interpreter = "3.8"`, then the environment file will be `environment/py3.8-conda-test.yaml`
"""
if not interpreter:
raise ValueError("must supply interpreter for this backend")

venv = CondaEnv(
location=location,
interpreter=interpreter,
reuse_existing=reuse_existing,
venv_params=venv_params,
conda_cmd=backend,
)

env_file = f"environment/py{interpreter}-conda-test.yaml"

assert os.path.exists(env_file)
# Custom creating (based on CondaEnv.create)
if not venv._clean_location():
logger.debug(f"Re-using existing conda env at {venv.location_name}.")
venv._reused = True

else:
cmd = (
[venv.conda_cmd]
+ (["--yes"] if venv.conda_cmd == "micromamba" else ["env"])
+ ["create", "--prefix", venv.location, "-f", env_file]
)
# cmd = ["conda", "env", "create", "--prefix", venv.location, "-f", env_file]

logger.info(
f"Creating conda env in {venv.location_name} with env file {env_file}"
)
logger.info(f"{cmd}")
nox.command.run(cmd, silent=False, log=nox.options.verbose or False)

return venv

return create_conda_env



@nox.session(python=["3.8"], venv_backend=factory_conda_env("conda"))
def conda_tests(session: nox.Session) -> None:
"""Run test suite in conda environment."""
# Note that all extra dependencies are assumed to
# be installed during environment creation
session.install("-e", ".", "--no-deps")
session.run("pytest", *session.posargs)


@nox.session(python=["3.8"], venv_backend=factory_conda_env("micromamba"))
def micromamba_tests(session: nox.Session) -> None:
"""Run test suite in micromamba environment."""
# Note that all extra dependencies are assumed to
# be installed during environment creation
session.install("-e", ".", "--no-deps")
session.run("pytest", *session.posargs)

Note that this scheme can be extended to use, for example, `conda-lock
<https://github.com/conda/conda-lock>`_ to install locked environments.






The Auto-Release
^^^^^^^^^^^^^^^^

Expand Down
7 changes: 7 additions & 0 deletions docs/tutorial.rst
Expand Up @@ -410,6 +410,13 @@ incompatible versions of packages already installed with conda.
use/require `mamba <https://github.com/mamba-org/mamba>`_ instead of conda.


Customized virtual environment creation
---------------------------------------

If the builtin methods for creating virtual environments don't fit you exact
needs, you can instead pass a callable for ``venv_backend``. See :doc:`cookbook` for examples.


Parametrization
---------------

Expand Down
72 changes: 72 additions & 0 deletions environment/create_env_files.py
@@ -0,0 +1,72 @@
import logging
import os
import sys
from pathlib import Path

PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11"]
LOCK_VERSIONS = ["3.11"]


logger = logging.getLogger(__name__)


HERE = Path(__file__).resolve().parent


def create_yaml(*deps: str) -> str:
x = ["dependencies:"]
x.extend([f" - {dep}" for dep in deps])
return "\n".join(x) + "\n"


def create_yaml_files():
os.chdir(HERE)
logging.info(f"cwd: {Path.cwd()}")

# load requirements:
requirements = [
x.replace(" ", "")
for x in Path("../requirements-conda-test.txt").read_text().strip().split("\n")
]

# create environment files
for py in PYTHON_VERSIONS:
yaml = Path(f"py{py}-conda-test.yaml")

# make sure these have python and pip
s = create_yaml(f"python={py}", *requirements, "pip")
with yaml.open("w") as f:
f.write(s)


def create_lock_files():
import subprocess

os.chdir(HERE)
for py in LOCK_VERSIONS:
subprocess.run(
[
"conda-lock",
"lock",
"-c",
"conda-forge",
f"--file=py{py}-conda-test.yaml",
f"--lockfile=py{py}-conda-test-conda-lock.yml",
],
stdout=sys.stdout,
stderr=sys.stderr,
)


if __name__ == "__main__":
args = sys.argv[1:]

if not args:
print("pass yaml (to create yaml files) or lock (to create lock files)")

else:
for arg in args:
if arg == "yaml":
create_yaml_files()
elif arg == "lock":
create_lock_files()
9 changes: 9 additions & 0 deletions environment/py3.10-conda-test.yaml
@@ -0,0 +1,9 @@
dependencies:
- python=3.10
- argcomplete>=1.9.4,<3.0
- colorlog>=2.6.1,<7.0.0
- jinja2
- pytest
- tox<4.0.0
- virtualenv>=14.0.0
- pip