Repository: wntrblm/nox

Issue:
Add option to display  reason for skipped sessions in the summary
### How would this feature be useful?

In large projects, there can be multiple reasons why tests are skipped. For example, some tests might be skipped on Python 3.13 due to compatibility issues, while others could be skipped due to missing dependencies, platform-specific constraints, etc

Without a clear summary, developers often need to scroll through long test logs to find out why certain tests were skipped.

By adding a flag to display skip reasons in the final summary, developers can quickly understand why tests were skipped without digging through nox output. 

### Describe the solution you'd like

nox > Ran multiple sessions:
nox > * fail: failed
nox > * skip_3_13: skipped (Skip tests on python 3.13 due to compatibility issues)

### Describe alternatives you've considered

_No response_

### Anything else?

_No response_

Discussion:
@chirizxc: I'm not sure about flag, but I think something like: `--show-skip-reason-summary` , `--skip-summary-details` @chirizxc: @DiddiLeija what do you think?

Solution patch:
diff --git a/.github/action_helper.py b/.github/action_helper.py
index d2a6fce..e068547 100644
--- a/.github/action_helper.py
+++ b/.github/action_helper.py
@@ -33,7 +33,7 @@ def filter_version(version: str) -> str:
     return ".".join(version_parts[:2])
 
 
-def setup_action(input_: str) -> None:
+def setup_action(input_: str, *, self_version: str = "3.12") -> None:
     versions = [version.strip() for version in input_.split(",") if version.strip()]
 
     pypy_versions = [version for version in versions if version.startswith("pypy")]
@@ -60,15 +60,15 @@ def setup_action(input_: str) -> None:
     # other interpreters also define pythonX.Y symlinks.
     versions = pypy_versions + cpython_versions
 
-    # we want to install python 3.11 last to ease nox set-up
-    if "3.11" in cpython_versions_filtered:
-        index = cpython_versions_filtered.index("3.11")
+    # we want to install our own self version last to ease nox set-up
+    if self_version in cpython_versions_filtered:
+        index = cpython_versions_filtered.index(self_version)
         index = versions.index(cpython_versions[index])
         cpython_nox = versions.pop(index)
         versions.append(cpython_nox)
     else:
         # add this to install nox
-        versions.append("3.11")
+        versions.append(self_version)
 
     print(f"interpreters={json.dumps(versions)}")
 
diff --git a/action.yml b/action.yml
index e26e1fa..d1eb0c0 100644
--- a/action.yml
+++ b/action.yml
@@ -4,7 +4,7 @@ inputs:
   python-versions:
     description: "comma-separated list of python versions to install"
     required: false
-    default: "3.8, 3.9, 3.10, 3.11, 3.12, 3.13"
+    default: "3.9, 3.10, 3.11, 3.12, 3.13"
 branding:
   icon: package
   color: blue
@@ -15,7 +15,7 @@ runs:
     - uses: actions/setup-python@v5
       id: localpython
       with:
-        python-version: "3.8 - 3.12"
+        python-version: "3.9 - 3.13"
         update-environment: false
 
     - name: "Validate input"
diff --git a/docs/config.rst b/docs/config.rst
index 593c111..520160f 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -442,12 +442,10 @@ More sophisticated tag assignment can be performed by passing a generator to the
                     tags.append("quick")
                 if dependency == "2.0" or database == "sqlite":
                     tags.append("standard")
-                yield nox.param((dependency, database), tags)
+                yield nox.param(dependency, database, tags=tags)
 
     @nox.session
-    @nox.parametrize(
-        ["dependency", "database"], generate_params(),
-    )
+    @nox.parametrize(["dependency", "database"], generate_params())
     def tests(session, dependency, database):
         ...
 
diff --git a/docs/usage.rst b/docs/usage.rst
index ab9daea..18fc0f7 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -156,11 +156,6 @@ Note that using this option does not change the backend for sessions where ``ven
    as ``uv pip`` is used to install programs instead. If you need to manually
    interact with pip, you should install it with ``session.install("pip")``.
 
-   Also, uv tries very hard to cache, so you might want
-   ``--reinstall-package=<package>`` in your pip install command for the local
-   package, or preferably set ``tool.uv.reinstall-package`` in your
-   pyproject.toml/uv.toml.
-
 Backends that could be missing (``uv``, ``conda``, ``mamba``, and ``micromamba``) can have a fallback using ``|``, such as ``uv|virtualenv`` or ``micromamba|mamba|conda``. This will use the first item that is available on the users system.
 
 If you need to check to see which backend was selected, you can access it via
diff --git a/nox/tasks.py b/nox/tasks.py
index 02762fe..1b9d499 100644
--- a/nox/tasks.py
+++ b/nox/tasks.py
@@ -29,7 +29,7 @@ from nox._resolver import CycleError
 from nox._version import InvalidVersionSpecifier, VersionCheckFailed, check_nox_version
 from nox.logger import logger
 from nox.manifest import WARN_PYTHONS_IGNORED, Manifest
-from nox.sessions import Result
+from nox.sessions import Result, Status
 
 if TYPE_CHECKING:
     import types
@@ -411,7 +411,10 @@ def print_summary(
     for result in results:
         name = result.session.friendly_name
         status = result.status.name.lower()
-        result.log(f"* {name}: {status}")
+        if result.status is Status.SKIPPED and result.reason:
+            result.log(f"* {name}: {status} ({result.reason})")
+        else:
+            result.log(f"* {name}: {status}")
 
     # Return the results that were sent to this function.
     return results
diff --git a/noxfile.py b/noxfile.py
index 81dda9e..6dc05cf 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -177,7 +177,7 @@ def _check_python_version(session: nox.Session) -> None:
 )
 def github_actions_default_tests(session: nox.Session) -> None:
     """Check default versions installed by the nox GHA Action"""
-    assert sys.version_info[:2] == (3, 11)
+    assert sys.version_info[:2] == (3, 12)
     _check_python_version(session)
 
 
diff --git a/requirements-conda-test.txt b/requirements-conda-test.txt
index 8392016..50123a9 100644
--- a/requirements-conda-test.txt
+++ b/requirements-conda-test.txt
@@ -1,5 +1,7 @@
 argcomplete >=1.9.4,<3.0
+attrs >=23.1
 colorlog >=2.6.1,<7.0.0
+dependency-groups >=1.1
 jinja2
 pytest
 tox>=4.0.0


Test patch:
diff --git a/tests/test_action_helper.py b/tests/test_action_helper.py
index 9995e9f..7237798 100644
--- a/tests/test_action_helper.py
+++ b/tests/test_action_helper.py
@@ -48,17 +48,18 @@ VALID_VERSION_LISTS = {
         "3.9",
         "3.10",
         "3.11",
+        "3.12",
     ],
-    "": ["3.11"],
-    "~3.12.0-0": ["~3.12.0-0", "3.11"],
-    "3.11.4": ["3.11.4"],
-    "3.9-dev,pypy3.9-nightly": ["pypy3.9-nightly", "3.9-dev", "3.11"],
-    "3.11, 3.10, 3.9, 3.8": ["3.10", "3.9", "3.8", "3.11"],
+    "": ["3.12"],
+    "~3.12.0-0": ["~3.12.0-0"],
+    "3.11.4": ["3.11.4", "3.12"],
+    "3.9-dev,pypy3.9-nightly": ["pypy3.9-nightly", "3.9-dev", "3.12"],
+    "3.12, 3.11, 3.10, 3.9": ["3.11", "3.10", "3.9", "3.12"],
     ",".join(f"3.{minor}" for minor in range(20)): [
         f"3.{minor}"
-        for i, minor in enumerate(minor_ for minor_ in range(20) if minor_ != 11)
+        for i, minor in enumerate(minor_ for minor_ in range(20) if minor_ != 12)
     ]
-    + ["3.11"],
+    + ["3.12"],
 }
 
 
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index 5d25db1..b223d15 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -695,23 +695,48 @@ def test_print_summary_one_result() -> None:
 
 
 def test_print_summary() -> None:
-    results = [
-        sessions.Result(
-            session=typing.cast(
-                sessions.SessionRunner, argparse.Namespace(friendly_name="foo")
+    with mock.patch.object(sessions.Result, "log") as mock_log:
+        results = [
+            sessions.Result(
+                session=typing.cast(
+                    sessions.SessionRunner,
+                    argparse.Namespace(friendly_name="foo"),
+                ),
+                status=sessions.Status.SUCCESS,
             ),
-            status=sessions.Status.SUCCESS,
-        ),
-        sessions.Result(
-            session=typing.cast(
-                sessions.SessionRunner, argparse.Namespace(friendly_name="bar")
+            sessions.Result(
+                session=typing.cast(
+                    sessions.SessionRunner,
+                    argparse.Namespace(friendly_name="bar"),
+                ),
+                status=sessions.Status.FAILED,
             ),
-            status=sessions.Status.FAILED,
-        ),
-    ]
-    with mock.patch.object(sessions.Result, "log", autospec=True) as log:
+            sessions.Result(
+                session=typing.cast(
+                    sessions.SessionRunner,
+                    argparse.Namespace(friendly_name="baz"),
+                ),
+                status=sessions.Status.SKIPPED,
+            ),
+            sessions.Result(
+                session=typing.cast(
+                    sessions.SessionRunner,
+                    argparse.Namespace(friendly_name="qux"),
+                ),
+                status=sessions.Status.SKIPPED,
+                reason="something reason",
+            ),
+        ]
+
         answer = tasks.print_summary(results, argparse.Namespace())
-        assert log.call_count == 2
+
+        assert mock_log.call_count == 4
+        calls = mock_log.call_args_list
+        assert calls[0][0][0] == "* foo: success"
+        assert calls[1][0][0] == "* bar: failed"
+        assert calls[2][0][0] == "* baz: skipped"
+        assert calls[3][0][0] == "* qux: skipped (something reason)"
+
     assert answer is results