Repository: wntrblm/nox (language: python)

Issue:
Add option to skip session by default
# How would this feature be useful?

Currently nox supports the possibility to exclude some sessions from running by default using positive list `nox.options.sessions = [session1, session3]`. In this case session2 would not be executed by running `nox` and would need to be specified explicitly, e.g. `nox -s session2`

# Describe the solution you'd like

Usually, this kind of tagging is expected to be found in negative lists. If someone forgets to add the session4 to the `nox.options.sessions` list, then it would run by default.

My proposal is to add an option to `nox.sessions` to skip the session by default
```
@nox.session(skip=True)
def session2(session):
   pass
```

# Describe alternatives you've considered
Writing my own decorator would be an option, but a built-in solution would be preferrable.

# Anything else?
No response
Add a `default=True` argument to nox.session
### How would this feature be useful?

I'd like to write

```python
@nox.session
def bar():
    pass

@nox.session(default=False)
def foo():
    pass

@nox.session
def baz():
    pass
```

as a DRYer alternative to:

```python
nox.options.sessions = ["bar", "baz"]

@nox.session
def bar():
    pass

@nox.session
def foo():
    pass

@nox.session
def baz():
    pass
```

i.e. to be able to specify whether a session is run by default or not when defining it, rather than hardcoding the sessions that should be run.

Has this been considered before perhaps?

### Describe the solution you'd like

Programmatically build up `nox.options.sessions` unless it's defined explicitly via the addition of a parameter (defaulting to True to match the current behavior) which defines whether a session is run by default or not.

### Describe alternatives you've considered

Obviously there could be some noodling on what to call the parameter.

### Anything else?

_No response_


Discussion / hints:
@slavong, perhaps this issue could be merged with a similar suggestion in #654?
@rnickle , yes, that seems to be a similar idea, but instead of using `skip=True` the request is to use `default=False`. From the discussion there I understand, that this feature seems to be not really desired. I still believe this would be useful, as using `nox.options.sessions` is quite cumbersome.
> as using `nox.options.sessions` is quite cumbersome.
I completely agree with this. I think excluding a single (or few) sessions from the default is far more useful, and less error prone (the serious error being test **not** run by mistake) than maintaining a positive list of sessions.
Sounds good to me! Using a parameter (instead of a variable) could be more human-readable, maybe?

Any thoughts from the maintainers?
It looks like this was brought up before and closed in #232.  It was also discussed in #40, which was closed in favor of #141. 

The _nox.options_ implementation of _nox.options.sessions_ was the solution adopted.
Fair enough, if there isn't a desire to add this obviously feel free to close -- I've basically started copy pasting [this](https://github.com/python-jsonschema/referencing/blob/c1a9ce8693d67b7b79a691e1bd0c4df1b73f617f/noxfile.py#L11-L20) into all my noxfiles which I'm happy to keep doing if others don't need this.
I would be very interested by this functionality. Just to give you a use case, we manage the translation of our Sphinx theme with Babel. update and compilation is managed by a nox session and a 2nd one is used to add extra languages. we would like to exclude the "init_language" from the default. 
I would love this for autopep8.

I have a flake8 session that I want to run by default. If I could, I would make an autopep8 session that was skipped by default, and then `nox -s autopep8` to fix most reported errors.

(Or `black --check` / `black`)

Another use would be a session to run `pip freeze > constraints.txt`.
I'd also really like something like this.

I'm currently experimenting with the following:
```py
import nox
skip = {"foobar"}
# Needs to go at the bottom of `noxfile.py`:
nox.options.sessions = [key for key in nox.registry.get() if key not in skip]
```
Which isn't perfect, but might do the trick in some cases.

I was also thinking about writing my own custom decorators, like an `@skip` and/or an `@only`, to throw above `@nox.session`, that'd manage including or excluding sessions by default.

Fix patch:
diff --git a/docs/config.rst b/docs/config.rst
index 53ab6931..ccc668d4 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -26,6 +26,10 @@ with ``@nox.session``. For example:
 
 You can also configure sessions to run against multiple Python versions as described in :ref:`virtualenv config` and  parametrize sessions as described in :ref:`parametrized sessions <parametrized>`.
 
+By default, all sessions will be run if no sessions are specified. You can
+remove sessions from this default list by passing ``default=False`` in the
+``@nox.session(...)`` decorator. You can also specify a list of sessions to run by
+default using the ``nox.options.sessions = [...]`` configuration option.
 
 Session description
 -------------------
@@ -421,7 +425,7 @@ Nox has various :doc:`command line arguments <usage>` that can be used to modify
     def tests(session):
         ...
 
-Or, if you wanted to provide a set of sessions that are run by default:
+Or, if you wanted to provide a set of sessions that are run by default (this overrides the ``default=`` argument to sessions):
 
 .. code-block:: python
 
diff --git a/nox/_decorators.py b/nox/_decorators.py
index aaef96a9..25f13de6 100644
--- a/nox/_decorators.py
+++ b/nox/_decorators.py
@@ -69,6 +69,8 @@ def __init__(
         venv_params: Any = None,
         should_warn: Mapping[str, Any] | None = None,
         tags: Sequence[str] | None = None,
+        *,
+        default: bool = True,
     ) -> None:
         self.func = func
         self.python = python
@@ -78,6 +80,7 @@ def __init__(
         self.venv_params = venv_params
         self.should_warn = dict(should_warn or {})
         self.tags = list(tags or [])
+        self.default = default
 
     def __call__(self, *args: Any, **kwargs: Any) -> Any:
         return self.func(*args, **kwargs)
@@ -94,6 +97,7 @@ def copy(self, name: str | None = None) -> Func:
             self.venv_params,
             self.should_warn,
             self.tags,
+            default=self.default,
         )
 
 
@@ -125,6 +129,7 @@ def __init__(self, func: Func, param_spec: Param) -> None:
             func.venv_params,
             func.should_warn,
             func.tags,
+            default=func.default,
         )
         self.call_spec = call_spec
         self.session_signature = session_signature
diff --git a/nox/manifest.py b/nox/manifest.py
index fa9f7978..e813f02b 100644
--- a/nox/manifest.py
+++ b/nox/manifest.py
@@ -155,6 +155,11 @@ def filter_by_name(self, specified_sessions: Iterable[str]) -> None:
         if missing_sessions:
             raise KeyError(f"Sessions not found: {', '.join(missing_sessions)}")
 
+    def filter_by_default(self) -> None:
+        """Filter sessions in the queue based on the default flag."""
+
+        self._queue = [x for x in self._queue if x.func.default]
+
     def filter_by_python_interpreter(self, specified_pythons: Sequence[str]) -> None:
         """Filter sessions in the queue based on the user-specified
         python interpreter versions.
diff --git a/nox/registry.py b/nox/registry.py
index 60765ae3..4e5c72aa 100644
--- a/nox/registry.py
+++ b/nox/registry.py
@@ -43,6 +43,8 @@ def session_decorator(
     venv_backend: Any | None = ...,
     venv_params: Any | None = ...,
     tags: Sequence[str] | None = ...,
+    *,
+    default: bool = ...,
 ) -> Callable[[F], F]:
     ...
 
@@ -56,6 +58,8 @@ def session_decorator(
     venv_backend: Any | None = None,
     venv_params: Any | None = None,
     tags: Sequence[str] | None = None,
+    *,
+    default: bool = True,
 ) -> F | Callable[[F], F]:
     """Designate the decorated function as a session."""
     # If `func` is provided, then this is the decorator call with the function
@@ -75,6 +79,7 @@ def session_decorator(
             venv_backend=venv_backend,
             venv_params=venv_params,
             tags=tags,
+            default=default,
         )
 
     if py is not None and python is not None:
@@ -88,7 +93,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,
+        tags=tags,
+        default=default,
     )
     _REGISTRY[final_name] = fn
     return fn
diff --git a/nox/tasks.py b/nox/tasks.py
index 595a4056..818dc935 100644
--- a/nox/tasks.py
+++ b/nox/tasks.py
@@ -173,7 +173,9 @@ def filter_manifest(manifest: Manifest, global_config: Namespace) -> Manifest |
     # This can raise KeyError if a specified session does not exist;
     # log this if it happens. The sessions does not come from the Noxfile
     # if keywords is not empty.
-    if global_config.sessions is not None:
+    if global_config.sessions is None:
+        manifest.filter_by_default()
+    else:
         try:
             manifest.filter_by_name(global_config.sessions)
         except KeyError as exc:


Test patch:
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index 947239ed..122cc0e8 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -40,6 +40,7 @@ def session_func():
 session_func.venv_backend = None
 session_func.should_warn = {}
 session_func.tags = []
+session_func.default = True
 
 
 def session_func_with_python():
@@ -48,6 +49,7 @@ def session_func_with_python():
 
 session_func_with_python.python = "3.8"
 session_func_with_python.venv_backend = None
+session_func_with_python.default = True
 
 
 def session_func_venv_pythons_warning():
@@ -346,6 +348,40 @@ def bar():
     assert len(manifest) == 2
 
 
+@pytest.mark.parametrize("selection", [None, ["qux"], ["quuz"], ["qux", "quuz"]])
+def test_default_false(selection):
+    @nox.session()
+    def qux():
+        pass
+
+    @nox.session()
+    def quux():
+        pass
+
+    @nox.session(default=False)
+    def quuz():
+        pass
+
+    @nox.session(default=False)
+    def corge():
+        pass
+
+    config = _options.options.namespace(sessions=selection, pythons=(), posargs=[])
+    manifest = Manifest(
+        {
+            "qux": qux,
+            "quux": quux,
+            "quuz": quuz,
+            "corge": corge,
+        },
+        config,
+    )
+    return_value = tasks.filter_manifest(manifest, config)
+    assert return_value is manifest
+    expected = 2 if selection is None else len(selection)
+    assert len(manifest) == expected
+
+
 def test_honor_list_request_noop():
     config = _options.options.namespace(list_sessions=False)
     manifest = {"thing": mock.sentinel.THING}