diff --git a/docs/config.rst b/docs/config.rst index a8da73db..d64d2f88 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -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 ------------------ diff --git a/nox/_decorators.py b/nox/_decorators.py index 03878deb..b04e48ab 100644 --- a/nox/_decorators.py +++ b/nox/_decorators.py @@ -1,5 +1,6 @@ import copy import functools +import inspect import types from typing import Any, Callable, Dict, Iterable, List, Optional, cast @@ -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 diff --git a/tests/test__parametrize.py b/tests/test__parametrize.py index d130174a..79fc2e5d 100644 --- a/tests/test__parametrize.py +++ b/tests/test__parametrize.py @@ -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( @@ -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")