Repository: theacodes/nox
Pull request #397 (2021-03-07 10:05:05)

set local session posargs via notify
Closes: https://github.com/theacodes/nox/issues/391

Once the basic API has been reviewed and accepted I'll add tests for this feature.

rmorshea: @cjolowicz assuming tests pass I think this should be good now.
rmorshea: @cjolowicz I think this is good to merge.
rmorshea: ping: @cjolowicz - I think this is good to merge

Patch:
diff --git a/nox/_parametrize.py b/nox/_parametrize.py
index 9f2cd63..be226e4 100644
--- a/nox/_parametrize.py
+++ b/nox/_parametrize.py
@@ -51,8 +51,7 @@ class Param:
             return self.id
         else:
             call_spec = self.call_spec
-            keys = sorted(call_spec.keys(), key=str)
-            args = ["{}={}".format(k, repr(call_spec[k])) for k in keys]
+            args = ["{}={}".format(k, repr(call_spec[k])) for k in call_spec.keys()]
             return ", ".join(args)
 
     __repr__ = __str__
diff --git a/nox/manifest.py b/nox/manifest.py
index 616de13..1334fa9 100644
--- a/nox/manifest.py
+++ b/nox/manifest.py
@@ -16,7 +16,18 @@ import argparse
 import collections.abc
 import itertools
 from collections import OrderedDict
-from typing import Any, Iterable, Iterator, List, Mapping, Sequence, Set, Tuple, Union
+from typing import (
+    Any,
+    Iterable,
+    Iterator,
+    List,
+    Mapping,
+    Optional,
+    Sequence,
+    Set,
+    Tuple,
+    Union,
+)
 
 from nox._decorators import Call, Func
 from nox.sessions import Session, SessionRunner
@@ -257,7 +268,9 @@ class Manifest:
     def next(self) -> SessionRunner:
         return self.__next__()
 
-    def notify(self, session: Union[str, SessionRunner]) -> bool:
+    def notify(
+        self, session: Union[str, SessionRunner], posargs: Optional[List[str]] = None
+    ) -> bool:
         """Enqueue the specified session in the queue.
 
         If the session is already in the queue, or has been run already,
@@ -266,6 +279,10 @@ class Manifest:
         Args:
             session (Union[str, ~nox.session.Session]): The session to be
                 enqueued.
+            posargs (Optional[List[str]]): If given, sets the positional
+                arguments *only* for the queued session. Otherwise, the
+                standard globally available positional arguments will be
+                used instead.
 
         Returns:
             bool: Whether the session was added to the queue.
@@ -282,6 +299,8 @@ class Manifest:
         # the end of the queue.
         for s in self._all_sessions:
             if s == session or s.name == session or session in s.signatures:
+                if posargs is not None:
+                    s.posargs = posargs
                 self._queue.append(s)
                 return True
 
diff --git a/nox/sessions.py b/nox/sessions.py
index 78cdcbb..52f647a 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -149,9 +149,8 @@ class Session:
 
     @property
     def posargs(self) -> List[str]:
-        """This is set to any extra arguments
-        passed to ``nox`` on the commandline."""
-        return self._runner.global_config.posargs
+        """Any extra arguments from the ``nox`` commandline or :class:`Session.notify`."""
+        return self._runner.posargs
 
     @property
     def virtualenv(self) -> ProcessEnv:
@@ -432,7 +431,11 @@ class Session:
 
         self._run("python", "-m", "pip", "install", *args, external="error", **kwargs)
 
-    def notify(self, target: "Union[str, SessionRunner]") -> None:
+    def notify(
+        self,
+        target: "Union[str, SessionRunner]",
+        posargs: Optional[Iterable[str]] = None,
+    ) -> None:
         """Place the given session at the end of the queue.
 
         This method is idempotent; multiple notifications to the same session
@@ -442,8 +445,14 @@ class Session:
             target (Union[str, Callable]): The session to be notified. This
                 may be specified as the appropriate string (same as used for
                 ``nox -s``) or using the function object.
+            posargs (Optional[Iterable[str]]): If given, sets the positional
+                arguments *only* for the queued session. Otherwise, the
+                standard globally available positional arguments will be
+                used instead.
         """
-        self._runner.manifest.notify(target)
+        if posargs is not None:
+            posargs = list(posargs)
+        self._runner.manifest.notify(target, posargs)
 
     def log(self, *args: Any, **kwargs: Any) -> None:
         """Outputs a log during the session."""
@@ -472,7 +481,8 @@ class SessionRunner:
         self.func = func
         self.global_config = global_config
         self.manifest = manifest
-        self.venv = None  # type: Optional[ProcessEnv]
+        self.venv: Optional[ProcessEnv] = None
+        self.posargs: List[str] = global_config.posargs
 
     @property
     def description(self) -> Optional[str]:
diff --git a/noxfile.py b/noxfile.py
index 24e41a7..802f6dc 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -90,6 +90,7 @@ def lint(session):
     session.install("flake8==3.7.8", "black==19.3b0", "isort==4.3.21", "mypy==0.720")
     session.run(
         "mypy",
+        "--config-file=",
         "--disallow-untyped-defs",
         "--warn-unused-ignores",
         "--ignore-missing-imports",


Test patch:
diff --git a/tests/test__parametrize.py b/tests/test__parametrize.py
index 2a7f69b..d130174 100644
--- a/tests/test__parametrize.py
+++ b/tests/test__parametrize.py
@@ -200,19 +200,19 @@ def test_generate_calls_multiple_args():
     f = mock.Mock()
     f.__name__ = "f"
 
-    arg_names = ("abc", "foo")
+    arg_names = ("foo", "abc")
     call_specs = [
-        _parametrize.Param(1, "a", arg_names=arg_names),
-        _parametrize.Param(2, "b", arg_names=arg_names),
-        _parametrize.Param(3, "c", arg_names=arg_names),
+        _parametrize.Param("a", 1, arg_names=arg_names),
+        _parametrize.Param("b", 2, arg_names=arg_names),
+        _parametrize.Param("c", 3, arg_names=arg_names),
     ]
 
     calls = _decorators.Call.generate_calls(f, call_specs)
 
     assert len(calls) == 3
-    assert calls[0].session_signature == "(abc=1, foo='a')"
-    assert calls[1].session_signature == "(abc=2, foo='b')"
-    assert calls[2].session_signature == "(abc=3, foo='c')"
+    assert calls[0].session_signature == "(foo='a', abc=1)"
+    assert calls[1].session_signature == "(foo='b', abc=2)"
+    assert calls[2].session_signature == "(foo='c', abc=3)"
 
     calls[0]()
     f.assert_called_with(abc=1, foo="a")
diff --git a/tests/test_manifest.py b/tests/test_manifest.py
index a363151..35eb775 100644
--- a/tests/test_manifest.py
+++ b/tests/test_manifest.py
@@ -34,10 +34,11 @@ def create_mock_sessions():
 
 
 def create_mock_config():
-    cfg = mock.sentinel.CONFIG
+    cfg = mock.sentinel.MOCKED_CONFIG
     cfg.force_venv_backend = None
     cfg.default_venv_backend = None
     cfg.extra_pythons = None
+    cfg.posargs = []
     return cfg
 
 
@@ -223,9 +224,7 @@ def test_add_session_multiple_pythons():
     ],
 )
 def test_extra_pythons(python, extra_pythons, expected):
-    cfg = mock.sentinel.CONFIG
-    cfg.force_venv_backend = None
-    cfg.default_venv_backend = None
+    cfg = create_mock_config()
     cfg.extra_pythons = extra_pythons
 
     manifest = Manifest({}, cfg)
@@ -345,6 +344,21 @@ def test_notify_noop():
     assert len(manifest) == 1
 
 
+def test_notify_with_posargs():
+    cfg = create_mock_config()
+    manifest = Manifest({}, cfg)
+
+    session = manifest.make_session("my_session", Func(lambda session: None))[0]
+    manifest.add_session(session)
+
+    # delete my_session from the queue
+    manifest.filter_by_name(())
+
+    assert session.posargs is cfg.posargs
+    assert manifest.notify("my_session", posargs=["--an-arg"])
+    assert session.posargs == ["--an-arg"]
+
+
 def test_notify_error():
     manifest = Manifest({}, create_mock_config())
     with pytest.raises(ValueError):
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 8bb9b78..150c15e 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -505,7 +505,11 @@ class TestSession:
 
         session.notify("other")
 
-        runner.manifest.notify.assert_called_once_with("other")
+        runner.manifest.notify.assert_called_once_with("other", None)
+
+        session.notify("other", posargs=["--an-arg"])
+
+        runner.manifest.notify.assert_called_with("other", ["--an-arg"])
 
     def test_log(self, caplog):
         caplog.set_level(logging.INFO)