Repository: wntrblm/nox (python)

Issue:
Support a `NOXPYTHON` environment variable similar to `NOXSESSION`
### How would this feature be useful?

It'd save a few keystrokes and would work as a default for `--pythons`.

### Describe the solution you'd like

Check for `NOXPYTHON` environment variable:

```shell
NOXPYTHON=3.11 nox -s tests
NOXPYTHON=3.10,3.11 nox -s tests
```

While we're at it, it might make sense to also add env vars for `--extra-pythons` and `--force-pythons`.

### Describe alternatives you've considered

Continue using `--pythons`, or include the Python version in `NOXSESSION`, i.e.

```dotenv
NOXSESSION=tests-3.10
```

but this couples the default session with a Python version

### Anything else?

_No response_

Pull request description:
Add support for `NOXPYTHON`, `NOXEXTRAPYTHON` and `NOXFORCEPYTHON`
Closes #672 

Patch:
diff --git a/docs/conf.py b/docs/conf.py
index 50318d1..7982f61 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,6 +42,7 @@ extensions = [
     "sphinx.ext.autodoc",
     "sphinx.ext.napoleon",
     "myst_parser",
+    "sphinx_tabs.tabs",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -316,3 +317,6 @@ texinfo_documents = [
 
 # If true, do not generate a @detailmenu in the "Top" node's menu.
 # texinfo_no_detailmenu = False
+
+# Disable tab closing by selecting the open tab
+sphinx_tabs_disable_tab_closing = True
diff --git a/docs/usage.rst b/docs/usage.rst
index 2a77b28..6bcb0a0 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -52,27 +52,35 @@ Specifying one or more sessions
 
 By default Nox will run all sessions defined in the Noxfile. However, you can choose to run a particular set of them using ``--session``, ``-s``, or ``-e``:
 
-.. code-block:: console
+.. tabs::
 
-    nox --session tests
-    nox -s lint tests
-    nox -e lint
+   .. code-tab:: console CLI options
 
-You can also use the ``NOXSESSION`` environment variable:
+         nox --session tests
+         nox -s lint tests
+         nox -e lint
 
-.. code-block:: console
+   .. code-tab:: console Environment variables
 
-    NOXSESSION=lint nox
-    NOXSESSION=lint,tests nox
+         NOXSESSION=tests nox
+         NOXSESSION=lint nox
+         NOXSESSION=lint,tests nox
 
 Nox will run these sessions in the same order they are specified.
 
 If you have a :ref:`configured session's virtualenv <virtualenv config>`, you can choose to run only sessions with given Python versions:
 
-.. code-block:: console
+.. tabs::
+
+   .. code-tab:: console CLI options
+
+         nox --python 3.8
+         nox -p 3.7 3.8
+
+   .. code-tab:: console Environment variables
 
-    nox --python 3.8
-    nox -p 3.7 3.8
+         NOXPYTHON=3.8 nox
+         NOXPYTHON=3.7,3.8 nox
 
 You can also use `pytest-style keywords`_ using ``-k`` or ``--keywords``, and
 tags using ``-t`` or ``--tags`` to filter test sessions:
@@ -184,25 +192,55 @@ Running additional Python versions
 
 In addition to Nox supporting executing single sessions, it also supports running Python versions that aren't specified using ``--extra-pythons``.
 
-.. code-block:: console
+.. tabs::
+
+   .. code-tab:: console CLI options
+
+         nox --extra-pythons 3.8 3.9 3.10
+
+   .. code-tab:: console Environment variables
+
+         NOXEXTRAPYTHON=3.8,3.9,3.10 nox
 
-    nox --extra-pythons 3.8 3.9 3.10
 
 This will, in addition to specified Python versions in the Noxfile, also create sessions for the specified versions.
 
-This option can be combined with ``--python`` to replace, instead of appending, the Python interpreter for a given session::
+This option can be combined with ``--python`` to replace, instead of appending, the Python interpreter for a given session:
 
-    nox --python 3.11 --extra-python 3.11 -s lint
+.. tabs::
 
-Instead of passing both options, you can use the ``--force-python`` shorthand::
+   .. code-tab:: console CLI options
 
-    nox --force-python 3.11 -s lint
+         nox --python 3.11 --extra-python 3.11 -s lint
+
+   .. code-tab:: console Environment variables
+
+         NOXPYTHON=3.11 NOXEXTRAPYTHON=3.11 NOXSESSION=lint nox
+
+Instead of passing both options, you can use the ``--force-python`` shorthand:
+
+.. tabs::
+
+   .. code-tab:: console CLI options
+
+         nox --force-python 3.11 -s lint
+
+   .. code-tab:: console Environment variables
+
+         NOXFORCEPYTHON=3.11 NOXSESSION=lint nox
 
 Also, you can specify ``python`` in place of a specific version. This will run the session
-using the ``python`` specified for the current ``PATH``::
+using the ``python`` specified for the current ``PATH``:
+
+.. tabs::
+
+   .. code-tab:: console CLI options
+
+         nox --force-python python -s lint
 
-    nox --force-python python -s lint
+   .. code-tab:: console Environment variables
 
+         NOXFORCEPYTHON=python NOXSESSION=lint nox
 
 .. _opt-stop-on-first-error:
 
diff --git a/nox/_options.py b/nox/_options.py
index ae4e08f..d6ad071 100644
--- a/nox/_options.py
+++ b/nox/_options.py
@@ -18,7 +18,7 @@ import argparse
 import functools
 import os
 import sys
-from typing import Any, Sequence
+from typing import Any, Callable, Sequence
 
 from nox import _option_set
 from nox.tasks import discover_manifest, filter_manifest, load_nox_module
@@ -142,11 +142,21 @@ def _envdir_merge_func(
     return command_args.envdir or noxfile_args.envdir or ".nox"
 
 
-def _sessions_default() -> list[str] | None:
-    """Looks at the NOXSESSION env var to set the default value for sessions."""
-    nox_env = os.environ.get("NOXSESSION")
-    env_sessions = nox_env.split(",") if nox_env else None
-    return env_sessions
+def default_env_var_list_factory(env_var: str) -> Callable[[], list[str] | None]:
+    """Looks at the env var to set the default value for a list of env vars.
+
+    Args:
+        env_var (str): The name of the environment variable to look up.
+
+    Returns:
+        A callback that retrieves a list from a comma-delimited environment variable.
+    """
+
+    def _default_list() -> list[str] | None:
+        env_value = os.environ.get(env_var)
+        return env_value.split(",") if env_value else None
+
+    return _default_list
 
 
 def _color_finalizer(value: bool, args: argparse.Namespace) -> bool:
@@ -264,7 +274,7 @@ options.add_options(
         noxfile=True,
         merge_func=functools.partial(_sessions_and_keywords_merge_func, "sessions"),
         nargs="*",
-        default=_sessions_default,
+        default=default_env_var_list_factory("NOXSESSION"),
         help="Which sessions to run. By default, all sessions will run.",
         completer=_session_completer,
     ),
@@ -276,6 +286,7 @@ options.add_options(
         group=options.groups["python"],
         noxfile=True,
         nargs="*",
+        default=default_env_var_list_factory("NOXPYTHON"),
         help="Only run sessions that use the given python interpreter versions.",
     ),
     _option_set.Option(
@@ -406,6 +417,7 @@ options.add_options(
         "--extra-python",
         group=options.groups["python"],
         nargs="*",
+        default=default_env_var_list_factory("NOXEXTRAPYTHON"),
         help="Additionally, run sessions using the given python interpreter versions.",
     ),
     _option_set.Option(
@@ -414,6 +426,7 @@ options.add_options(
         "--force-python",
         group=options.groups["python"],
         nargs="*",
+        default=default_env_var_list_factory("NOXFORCEPYTHON"),
         help=(
             "Run sessions with the given interpreters instead of those listed in the"
             " Noxfile. This is a shorthand for ``--python=X.Y --extra-python=X.Y``."


Test patch:
diff --git a/requirements-test.txt b/requirements-test.txt
index 7cc7a3b..c4c2ba5 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -4,4 +4,5 @@ pytest>=6.0
 pytest-cov
 sphinx>=3.0
 sphinx-autobuild
+sphinx-tabs
 witchhazel
diff --git a/tests/test_main.py b/tests/test_main.py
index c1ebc13..d91d6b5 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -217,10 +217,30 @@ def test_main_explicit_sessions_with_spaces_in_names(monkeypatch):
 
 
 @pytest.mark.parametrize(
-    "env,sessions", [("foo", ["foo"]), ("foo,bar", ["foo", "bar"])]
+    "var,option,env,values",
+    [
+        ("NOXSESSION", "sessions", "foo", ["foo"]),
+        ("NOXSESSION", "sessions", "foo,bar", ["foo", "bar"]),
+        ("NOXPYTHON", "pythons", "3.9", ["3.9"]),
+        ("NOXPYTHON", "pythons", "3.9,3.10", ["3.9", "3.10"]),
+        ("NOXEXTRAPYTHON", "extra_pythons", "3.9", ["3.9"]),
+        ("NOXEXTRAPYTHON", "extra_pythons", "3.9,3.10", ["3.9", "3.10"]),
+        ("NOXFORCEPYTHON", "force_pythons", "3.9", ["3.9"]),
+        ("NOXFORCEPYTHON", "force_pythons", "3.9,3.10", ["3.9", "3.10"]),
+    ],
+    ids=[
+        "single_session",
+        "multiple_sessions",
+        "single_python",
+        "multiple_pythons",
+        "single_extra_python",
+        "multiple_extra_pythons",
+        "single_force_python",
+        "multiple_force_pythons",
+    ],
 )
-def test_main_session_from_nox_env_var(monkeypatch, env, sessions):
-    monkeypatch.setenv("NOXSESSION", env)
+def test_main_list_option_from_nox_env_var(monkeypatch, var, option, env, values):
+    monkeypatch.setenv(var, env)
     monkeypatch.setattr(sys, "argv", [sys.executable])
 
     with mock.patch("nox.workflow.execute") as execute:
@@ -234,9 +254,10 @@ def test_main_session_from_nox_env_var(monkeypatch, env, sessions):
 
         # Verify that the sessions from the env var are listed in the config.
         config = execute.call_args[1]["global_config"]
-        assert len(config.sessions) == len(sessions)
-        for session in sessions:
-            assert session in config.sessions
+        config_values = getattr(config, option)
+        assert len(config_values) == len(values)
+        for value in values:
+            assert value in config_values
 
 
 def test_main_positional_args(capsys, monkeypatch):