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

feat: added venv_location option to specify virtualenv location #785

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
12 changes: 11 additions & 1 deletion docs/config.rst
Expand Up @@ -175,14 +175,24 @@ You are not limited to virtualenv, there is a selection of backends you can choo
def tests(session):
pass

Finally, custom backend parameters are supported:
Custom backend parameters are supported:

.. code-block:: python

@nox.session(venv_params=['--no-download'])
def tests(session):
pass

Finally, you can specify the exact location of an environment:

.. code-block:: python

@nox.session(venv_location=".venv")
def dev(session):
pass

This places the environment in the folder ``./.venv`` instead of the default ``./.nox/dev``.


Passing arguments into sessions
-------------------------------
Expand Down
29 changes: 4 additions & 25 deletions docs/cookbook.rst
Expand Up @@ -47,33 +47,12 @@ Enter the ``dev`` nox session:
# so it's not run twice accidentally
nox.options.sessions = [...] # Sessions other than 'dev'

# this VENV_DIR constant specifies the name of the dir that the `dev`
# session will create, containing the virtualenv;
# the `resolve()` makes it portable
VENV_DIR = pathlib.Path('./.venv').resolve()
VENV_DIR = "./.venv"

@nox.session
@nox.session(venv_location=VENV_DIR)
def dev(session: nox.Session) -> None:
"""
Sets up a python development environment for the project.

This session will:
- Create a python virtualenv for the session
- Install the `virtualenv` cli tool into this environment
- Use `virtualenv` to create a global project virtual environment
- Invoke the python interpreter from the global project environment to install
the project and all it's development dependencies.
"""

session.install("virtualenv")
# the VENV_DIR constant is explained above
session.run("virtualenv", os.fsdecode(VENV_DIR), silent=True)

python = os.fsdecode(VENV_DIR.joinpath("bin/python"))

# Use the venv's interpreter to install the project along with
# all it's dev dependencies, this ensures it's installed in the right way
session.run(python, "-m", "pip", "install", "-e", ".[dev]", external=True)
"""Sets up a python development environment for the project."""
session.install("-e", ".[dev]")

With this, a user can simply run ``nox -s dev`` and have their entire environment set up automatically!

Expand Down
4 changes: 4 additions & 0 deletions nox/_decorators.py
Expand Up @@ -67,6 +67,7 @@ def __init__(
name: str | None = None,
venv_backend: Any = None,
venv_params: Any = None,
venv_location: str | None = None,
should_warn: Mapping[str, Any] | None = None,
tags: Sequence[str] | None = None,
) -> None:
Expand All @@ -76,6 +77,7 @@ def __init__(
self.name = name
self.venv_backend = venv_backend
self.venv_params = venv_params
self.venv_location = venv_location
self.should_warn = dict(should_warn or {})
self.tags = list(tags or [])

Expand All @@ -92,6 +94,7 @@ def copy(self, name: str | None = None) -> Func:
name,
self.venv_backend,
self.venv_params,
self.venv_location,
self.should_warn,
self.tags,
)
Expand Down Expand Up @@ -123,6 +126,7 @@ def __init__(self, func: Func, param_spec: Param) -> None:
None,
func.venv_backend,
func.venv_params,
func.venv_location,
func.should_warn,
func.tags,
)
Expand Down
12 changes: 11 additions & 1 deletion nox/registry.py
Expand Up @@ -42,6 +42,7 @@ def session_decorator(
name: str | None = ...,
venv_backend: Any | None = ...,
venv_params: Any | None = ...,
venv_location: str | None = ...,
tags: Sequence[str] | None = ...,
) -> Callable[[F], F]:
...
Expand All @@ -55,6 +56,7 @@ def session_decorator(
name: str | None = None,
venv_backend: Any | None = None,
venv_params: Any | None = None,
venv_location: str | None = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should support Path too, I think.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree. Will update to os.PathLike

tags: Sequence[str] | None = None,
) -> F | Callable[[F], F]:
"""Designate the decorated function as a session."""
Expand All @@ -74,6 +76,7 @@ def session_decorator(
name=name,
venv_backend=venv_backend,
venv_params=venv_params,
venv_location=venv_location,
tags=tags,
)

Expand All @@ -88,7 +91,14 @@ def session_decorator(

final_name = name or func.__name__
fn = Func(
func, python, reuse_venv, final_name, venv_backend, venv_params, tags=tags
func,
python,
reuse_venv,
final_name,
venv_backend,
venv_params,
venv_location=venv_location,
tags=tags,
)
_REGISTRY[final_name] = fn
return fn
Expand Down
7 changes: 6 additions & 1 deletion nox/sessions.py
Expand Up @@ -754,7 +754,12 @@ def tags(self) -> list[str]:

@property
def envdir(self) -> str:
return _normalize_path(self.global_config.envdir, self.friendly_name)
if self.func.venv_location:
return os.path.expanduser(self.func.venv_location)
return _normalize_path(
self.global_config.envdir,
self.friendly_name,
)

def _create_venv(self) -> None:
backend = (
Expand Down
6 changes: 6 additions & 0 deletions noxfile.py
Expand Up @@ -192,3 +192,9 @@ def github_actions_default_tests(session: nox.Session) -> None:
def github_actions_all_tests(session: nox.Session) -> None:
"""Check all versions installed by the nox GHA Action"""
_check_python_version(session)


@nox.session(venv_location=".venv")
def dev(session: nox.Session) -> None:
"""Create development environment `./.venv` using `nox -s dev`"""
session.install("-r", "requirements-dev.txt")
81 changes: 78 additions & 3 deletions tests/test_sessions.py
Expand Up @@ -34,6 +34,15 @@
from nox.logger import logger


@pytest.fixture()
def change_to_tmp_path(tmp_path):
old_cwd = Path.cwd()
os.chdir(tmp_path)
yield tmp_path
# Cleanup?
os.chdir(old_cwd)


def test__normalize_path():
envdir = "envdir"
normalize = nox.sessions._normalize_path
Expand Down Expand Up @@ -62,8 +71,8 @@ def test__normalize_path_give_up():


class TestSession:
def make_session_and_runner(self):
func = mock.Mock(spec=["python"], python="3.7")
def make_session_and_runner(self, venv_location=None):
func = mock.Mock(spec=["python"], python="3.7", venv_location=venv_location)
runner = nox.sessions.SessionRunner(
name="test",
signatures=["test"],
Expand Down Expand Up @@ -100,6 +109,55 @@ def test_create_tmp_twice(self):
assert session.env["TMPDIR"] == os.path.abspath(tmpdir)
assert tmpdir.startswith(root)

@pytest.mark.parametrize("pre_run", [0, 1])
def test_create_tmp_with_venv_location(self, pre_run):
# for testing, also set envdir
with tempfile.TemporaryDirectory() as new_location:
session, runner = self.make_session_and_runner(
venv_location=os.path.join(new_location, "my-location")
)
# for testing, also set envdir
with tempfile.TemporaryDirectory() as root:
runner.global_config.envdir = root
for _ in range(pre_run):
session.create_tmp()

tmpdir = session.create_tmp()

assert tmpdir == os.path.join(new_location, "my-location", "tmp")
assert os.path.abspath(tmpdir) == os.path.join(
new_location, "my-location", "tmp"
)
assert session.env["TMPDIR"] == os.path.abspath(tmpdir)

@pytest.mark.parametrize("pre_run", [0, 1])
def test_create_tmp_with_venv_location2(self, change_to_tmp_path, pre_run):
session, runner = self.make_session_and_runner(venv_location="my-location")
# for testing, also set envdir
with tempfile.TemporaryDirectory() as root:
runner.global_config.envdir = root
for _ in range(pre_run):
session.create_tmp()

tmpdir = session.create_tmp()

assert tmpdir == os.path.join("my-location", "tmp")
assert os.path.abspath(tmpdir) == os.path.join(
change_to_tmp_path, "my-location", "tmp"
)
assert session.env["TMPDIR"] == os.path.abspath(tmpdir)

@pytest.mark.parametrize(
("venv_location", "envdir"),
[
("my-location", "my-location"),
("~/my-location", os.path.expanduser("~/my-location")),
],
)
def test_envdir(self, venv_location, envdir):
session, runner = self.make_session_and_runner(venv_location=venv_location)
assert runner.envdir == envdir

def test_properties(self):
session, runner = self.make_session_and_runner()
with tempfile.TemporaryDirectory() as root:
Expand Down Expand Up @@ -849,6 +907,7 @@ def make_runner(self):
func.python = None
func.venv_backend = None
func.reuse_venv = False
func.venv_location = None
runner = nox.sessions.SessionRunner(
name="test",
signatures=["test(1, 2)"],
Expand Down Expand Up @@ -930,6 +989,7 @@ def test__create_venv(self, create):
assert runner.venv.interpreter is None
assert runner.venv.reuse_existing is False

@pytest.mark.parametrize("venv_location", [None, "my-location"])
@pytest.mark.parametrize(
"create_method,venv_backend,expected_backend",
[
Expand All @@ -943,11 +1003,14 @@ def test__create_venv(self, create):
("nox.virtualenv.CondaEnv.create", "conda", nox.virtualenv.CondaEnv),
],
)
def test__create_venv_options(self, create_method, venv_backend, expected_backend):
def test__create_venv_options(
self, venv_location, create_method, venv_backend, expected_backend
):
runner = self.make_runner()
runner.func.python = "coolpython"
runner.func.reuse_venv = True
runner.func.venv_backend = venv_backend
runner.func.venv_location = venv_location

with mock.patch(create_method, autospec=True) as create:
runner._create_venv()
Expand All @@ -957,6 +1020,18 @@ def test__create_venv_options(self, create_method, venv_backend, expected_backen
assert runner.venv.interpreter == "coolpython"
assert runner.venv.reuse_existing is True

location_name = (
os.path.expanduser(venv_location)
if venv_location
else nox.sessions._normalize_path(
runner.global_config.envdir, runner.friendly_name
)
)

assert runner.venv.location_name == location_name
assert runner.venv.location == os.path.abspath(location_name)
assert runner.envdir == location_name

def test__create_venv_unexpected_venv_backend(self):
runner = self.make_runner()
runner.func.venv_backend = "somenewenvtool"
Expand Down