Skip to content

Commit

Permalink
Allow nox.parametrize to select the session Python (#413)
Browse files Browse the repository at this point in the history
* Enable nox.parametrize to determine the session Python

* Add tests

* Add documentation
  • Loading branch information
cjolowicz authored Apr 24, 2021
1 parent a9bd872 commit 9cd5b78
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 5 deletions.
38 changes: 38 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,44 @@ Produces these sessions when running ``nox --list``:
* tests(mysql, new)
Parametrizing the session Python
--------------------------------

You can use parametrization to select the Python interpreter for a session.
These two examples are equivalent:

.. code-block:: python
@nox.session
@nox.parametrize("python", ["3.6", "3.7", "3.8"])
def tests(session):
...
@nox.session(python=["3.6", "3.7", "3.8"])
def tests(session):
...
The first form can be useful if you need to exclude some combinations of Python
versions with other parameters. For example, you may want to test against
multiple versions of a dependency, but the latest version doesn't run on older
Pythons:

.. code-block:: python
@nox.session
@nox.parametrize(
"python,dependency",
[
(python, dependency)
for python in ("3.6", "3.7", "3.8")
for dependency in ("1.0", "2.0")
if (python, dependency) != ("3.6", "2.0")
],
)
def tests(session, dependency):
...
The session object
------------------

Expand Down
24 changes: 20 additions & 4 deletions nox/_decorators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import copy
import functools
import inspect
import types
from typing import Any, Callable, Dict, Iterable, List, Optional, cast

Expand Down Expand Up @@ -66,20 +67,35 @@ def copy(self, name: str = None) -> "Func":

class Call(Func):
def __init__(self, func: Func, param_spec: "Param") -> None:
call_spec = param_spec.call_spec
session_signature = "({})".format(param_spec)

# Determine the Python interpreter for the session using either @session
# or @parametrize. For backwards compatibility, we only use a "python"
# parameter in @parametrize if the session function does not expect it
# as a normal argument, and if the @session decorator does not already
# specify `python`.

python = func.python
if python is None and "python" in call_spec:
signature = inspect.signature(func.func)
if "python" not in signature.parameters:
python = call_spec.pop("python")

super().__init__(
func,
func.python,
python,
func.reuse_venv,
None,
func.venv_backend,
func.venv_params,
func.should_warn,
)
self.param_spec = param_spec
self.session_signature = "({})".format(param_spec)
self.call_spec = call_spec
self.session_signature = session_signature

def __call__(self, *args: Any, **kwargs: Any) -> Any:
kwargs.update(self.param_spec.call_spec)
kwargs.update(self.call_spec)
return super().__call__(*args, **kwargs)

@classmethod
Expand Down
68 changes: 67 additions & 1 deletion tests/test__parametrize.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from unittest import mock

import pytest
from nox import _decorators, _parametrize
from nox import _decorators, _parametrize, parametrize, session


@pytest.mark.parametrize(
Expand Down Expand Up @@ -242,3 +242,69 @@ def test_generate_calls_ids():
f.assert_called_with(foo=1)
calls[1]()
f.assert_called_with(foo=2)


def test_generate_calls_session_python():
called_with = []

@session
@parametrize("python,dependency", [("3.8", "0.9"), ("3.9", "0.9"), ("3.9", "1.0")])
def f(session, dependency):
called_with.append((session, dependency))

calls = _decorators.Call.generate_calls(f, f.parametrize)

assert len(calls) == 3

assert calls[0].python == "3.8"
assert calls[1].python == "3.9"
assert calls[2].python == "3.9"

assert calls[0].session_signature == "(python='3.8', dependency='0.9')"
assert calls[1].session_signature == "(python='3.9', dependency='0.9')"
assert calls[2].session_signature == "(python='3.9', dependency='1.0')"

session_ = ()

calls[0](session_)
calls[1](session_)
calls[2](session_)

assert len(called_with) == 3

assert called_with[0] == (session_, "0.9")
assert called_with[1] == (session_, "0.9")
assert called_with[2] == (session_, "1.0")


def test_generate_calls_python_compatibility():
called_with = []

@session
@parametrize("python,dependency", [("3.8", "0.9"), ("3.9", "0.9"), ("3.9", "1.0")])
def f(session, python, dependency):
called_with.append((session, python, dependency))

calls = _decorators.Call.generate_calls(f, f.parametrize)

assert len(calls) == 3

assert calls[0].python is None
assert calls[1].python is None
assert calls[2].python is None

assert calls[0].session_signature == "(python='3.8', dependency='0.9')"
assert calls[1].session_signature == "(python='3.9', dependency='0.9')"
assert calls[2].session_signature == "(python='3.9', dependency='1.0')"

session_ = ()

calls[0](session_)
calls[1](session_)
calls[2](session_)

assert len(called_with) == 3

assert called_with[0] == (session_, "3.8", "0.9")
assert called_with[1] == (session_, "3.9", "0.9")
assert called_with[2] == (session_, "3.9", "1.0")

0 comments on commit 9cd5b78

Please sign in to comment.