Repository: theacodes/nox
Pull request #320 (2020-05-03 03:52:17)

Address #319: Give a create_tmp API
This doesn't have docs or tests! Hooray :)

It does however show one way to implement it (kinda hacky) and a working use case for further discussion.

moshez: Hi @theacodes ! This is a rough draft, but I want some feedback on how much in the wrong direction am I going. Is it reasonable to assume the bin directory is writable? Should we push this API to the specific virtualenv? Is it OK to fail if bin is None? Etc.
theacodes: This looks like a great start.

> Is it reasonable to assume the bin directory is writable? 

Yup.

> Should we push this API to the specific virtualenv?

Nope, I think even "virtualenvless" sessions will have use cases for the tmpdir.

> Is it OK to fail if bin is None?

I think this should work even if there's no virtualenv for the session.
moshez: Hi @theacodes ! I added tests and documentation. I'm not sure if I understood your response to the third question -- do you mean I should have a tmpdir even without bin? In that case, I'm not sure the location would still make sense.
theacodes: >  do you mean I should have a tmpdir even without bin? In that case, I'm not sure the location would still make sense.

Yup.

I think it makes sense for it to go in `.nox/{session name}/tmp`.
moshez: OK, let me know what you think now. It should always work now.
theacodes: Looks good. 👌

Patch:
diff --git a/nox/sessions.py b/nox/sessions.py
index 59a5cf4..7fa77ec 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -135,6 +135,13 @@ class Session:
         """The bin directory for the virtualenv."""
         return self.virtualenv.bin
 
+    def create_tmp(self) -> str:
+        """Create, and return, a temporary directory."""
+        tmpdir = os.path.join(self._runner.envdir, "tmp")
+        os.makedirs(tmpdir, exist_ok=True)
+        self.env["TMPDIR"] = tmpdir
+        return tmpdir
+
     @property
     def interactive(self) -> bool:
         """Returns True if Nox is being run in an interactive session or False otherwise."""
@@ -388,33 +395,36 @@ class SessionRunner:
     def friendly_name(self) -> str:
         return self.signatures[0] if self.signatures else self.name
 
+    @property
+    def envdir(self) -> str:
+        return _normalize_path(self.global_config.envdir, self.friendly_name)
+
     def _create_venv(self) -> None:
         if self.func.python is False:
             self.venv = ProcessEnv()
             return
 
-        path = _normalize_path(self.global_config.envdir, self.friendly_name)
         reuse_existing = (
             self.func.reuse_venv or self.global_config.reuse_existing_virtualenvs
         )
 
         if not self.func.venv_backend or self.func.venv_backend == "virtualenv":
             self.venv = VirtualEnv(
-                path,
+                self.envdir,
                 interpreter=self.func.python,  # type: ignore
                 reuse_existing=reuse_existing,
                 venv_params=self.func.venv_params,
             )
         elif self.func.venv_backend == "conda":
             self.venv = CondaEnv(
-                path,
+                self.envdir,
                 interpreter=self.func.python,  # type: ignore
                 reuse_existing=reuse_existing,
                 venv_params=self.func.venv_params,
             )
         elif self.func.venv_backend == "venv":
             self.venv = VirtualEnv(
-                path,
+                self.envdir,
                 interpreter=self.func.python,  # type: ignore
                 reuse_existing=reuse_existing,
                 venv=True,
diff --git a/noxfile.py b/noxfile.py
index e20e5c1..5bb786c 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import functools
 import os
 
 import nox
@@ -30,6 +31,7 @@ def is_python_version(session, version):
 @nox.session(python=["3.5", "3.6", "3.7", "3.8"])
 def tests(session):
     """Run test suite with pytest."""
+    session.create_tmp()
     session.install("-r", "requirements-test.txt")
     session.install("-e", ".[tox_to_nox]")
     tests = session.posargs or ["tests/"]
@@ -45,6 +47,7 @@ def tests(session):
 @nox.session(python=["3.5", "3.6", "3.7", "3.8"], venv_backend="conda")
 def conda_tests(session):
     """Run test suite with pytest."""
+    session.create_tmp()
     session.conda_install(
         "--file", "requirements-conda-test.txt", "--channel", "conda-forge"
     )
@@ -93,11 +96,15 @@ def lint(session):
 @nox.session(python="3.8")
 def docs(session):
     """Build the documentation."""
-    session.run("rm", "-rf", "docs/_build", external=True)
+    output_dir = os.path.join(session.create_tmp(), "output")
+    doctrees, html = map(
+        functools.partial(os.path.join, output_dir), ["doctrees", "html"]
+    )
+    session.run("rm", "-rf", output_dir, external=True)
     session.install("-r", "requirements-test.txt")
     session.install(".")
     session.cd("docs")
-    sphinx_args = ["-b", "html", "-W", "-d", "_build/doctrees", ".", "_build/html"]
+    sphinx_args = ["-b", "html", "-W", "-d", doctrees, ".", html]
 
     if not session.interactive:
         sphinx_cmd = "sphinx-build"


Test patch:
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 8af3f85..f4c457f 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -16,6 +16,7 @@ import argparse
 import logging
 import os
 import sys
+import tempfile
 from unittest import mock
 
 import nox.command
@@ -74,6 +75,24 @@ class TestSession:
         runner.venv.bin = "/no/bin/for/you"
         return nox.sessions.Session(runner=runner), runner
 
+    def test_create_tmp(self):
+        session, runner = self.make_session_and_runner()
+        with tempfile.TemporaryDirectory() as root:
+            runner.global_config.envdir = root
+            tmpdir = session.create_tmp()
+            assert session.env["TMPDIR"] == tmpdir
+            assert tmpdir.startswith(root)
+
+    def test_create_tmp_twice(self):
+        session, runner = self.make_session_and_runner()
+        with tempfile.TemporaryDirectory() as root:
+            runner.global_config.envdir = root
+            runner.venv.bin = bin
+            session.create_tmp()
+            tmpdir = session.create_tmp()
+            assert session.env["TMPDIR"] == tmpdir
+            assert tmpdir.startswith(root)
+
     def test_properties(self):
         session, runner = self.make_session_and_runner()