Repository: theacodes/nox
Pull request #128 (2018-08-28 16:51:15)

Adding `Session.__slots__`.
This way old code being migrated will get failures when trying to set previously supported attributes such as `session.interpreter` and `session.virtualenv_dirname`.

----

Note that `TestSession.test_install` is currently broken because `Session.run` cannot be monkey-patched in the presence of `__slots__`. I have three workarounds for this in mind but would like to discuss:

1. Define the behavior of `Session.run` in a function `_run` defined in the `nox.sessions` module and the monkey-patch that function
1. Instantiate a do-nothing subclass of `Session` that does not have `__slots__`, then just monkey-patch `run` on that instance
1. Ditch `__slots__` and use `__(get|set|del)attr__` to give helpful error messages for non-existent attributes

theacodes: (2) sounds pretty reasonable.
dhermes: @theacodes PTAL, I went with (2) as commanded
theacodes: > as commanded

don't give me that much power.

Patch:
diff --git a/nox/sessions.py b/nox/sessions.py
index db3213c..db6c518 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -74,9 +74,21 @@ class Session:
     your Nox session.
     """
 
+    __slots__ = ("_runner",)
+
     def __init__(self, runner):
         self._runner = runner
 
+    @property
+    def __dict__(self):
+        """Attribute dictionary for object inspection.
+
+        This is needed because ``__slots__`` turns of ``__dict__`` by
+        default. Unlike a typical object, modifying the result of this
+        dictionary won't allow modification of the instance.
+        """
+        return {"_runner": self._runner}
+
     @property
     def env(self):
         """A dictionary of environment variables to pass into all commands."""


Test patch:
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 2af0b57..095dd1f 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -146,7 +146,20 @@ class TestSession:
             session.install()
 
     def test_install(self):
-        session, runner = self.make_session_and_runner()
+        runner = nox.sessions.SessionRunner(
+            name="test",
+            signature="test",
+            func=mock.sentinel.func,
+            global_config=argparse.Namespace(posargs=mock.sentinel.posargs),
+            manifest=mock.create_autospec(nox.manifest.Manifest),
+        )
+        runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
+        runner.venv.env = {}
+
+        class SessionNoSlots(nox.sessions.Session):
+            pass
+
+        session = SessionNoSlots(runner=runner)
 
         with mock.patch.object(session, "run", autospec=True) as run:
             session.install("requests", "urllib3")
@@ -190,6 +203,18 @@ class TestSession:
         with pytest.raises(nox.sessions._SessionSkip):
             session.skip()
 
+    def test___slots__(self):
+        session, _ = self.make_session_and_runner()
+        with pytest.raises(AttributeError):
+            session.foo = "bar"
+        with pytest.raises(AttributeError):
+            session.quux
+
+    def test___dict__(self):
+        session, _ = self.make_session_and_runner()
+        expected = {name: getattr(session, name) for name in session.__slots__}
+        assert session.__dict__ == expected
+
 
 class TestSessionRunner:
     def make_runner(self):