Repository: theacodes/nox
Pull request #196 (2019-05-18 20:23:42)

Add interactive property to session.
I took a stab at adding the interactive attribute to session. The tests are present, but I think it'll need some testing in a non-interactive pipeline to confirm it works. The unit tests in test_sessions feel a bit clunky with the mocks, so suggestions on making that cleaner would be great.

I need to sort out how to update the docs as well to include the new attribute.

If this looks reasonable to folks, I can take a look at adding the '--non-interactive' flag.

theacodes: This is wonderful, @AGeekInside. I'm going to merge #187 first and add a `--non-interactive` option to negate this before merging. Thank you so much!

Patch:
diff --git a/.travis.yml b/.travis.yml
index 162957b..3417c3f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,7 +16,7 @@ matrix:
 install:
   - pip install --upgrade pip setuptools
   - pip install .
-script: nox --session "$NOXSESSION"
+script: nox --non-interactive --session "$NOXSESSION"
 deploy:
   provider: pypi
   user: theacodes
diff --git a/docs/usage.rst b/docs/usage.rst
index 04aa0db..042f662 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -209,6 +209,32 @@ Would run both ``install`` commands, but skip the ``run`` command:
     nox > Session tests was successful.
 
 
+Forcing non-interactive behavior
+--------------------------------
+
+:attr:`session.interactive <nox.sessions.Session.interactive>` can be used to tell if Nox is being run from an interactive terminal (such as an actual human running it on their computer) vs run in a non-interactive terminal (such as a continuous integration system).
+
+.. code-block:: python
+
+    @nox.session
+    def docs(session):
+        ...
+
+        if session.interactive:
+            nox.run("sphinx-autobuild", ...)
+        else:
+            nox.run("sphinx-build", ...)
+
+Sometimes it's useful to force Nox to see the session as non-interactive. You can use the ``--non-interactive`` argument to do this:
+
+.. code-block:: bash
+
+    nox --non-interactive
+
+
+This will cause ``session.interactive`` to always return ``False``.
+
+
 Controlling color output
 ------------------------
 
diff --git a/nox/_options.py b/nox/_options.py
index 4dcadda..d421d90 100644
--- a/nox/_options.py
+++ b/nox/_options.py
@@ -94,7 +94,7 @@ def _color_finalizer(value, args):
     if args.nocolor is True:
         return False
 
-    return sys.stderr.isatty()
+    return sys.stdin.isatty()
 
 
 def _posargs_finalizer(value, unused_args):
@@ -216,6 +216,13 @@ options.add_options(
         noxfile=True,
         help="Output a report of all sessions to the given filename.",
     ),
+    _option_set.Option(
+        "non_interactive",
+        "--non-interactive",
+        group="secondary",
+        action="store_true",
+        help="Force session.interactive to always be False, even in interactive sessions.",
+    ),
     _option_set.Option(
         "nocolor",
         "--nocolor",
diff --git a/nox/sessions.py b/nox/sessions.py
index 40d67b3..ceb708f 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -16,6 +16,7 @@ import enum
 import hashlib
 import os
 import re
+import sys
 import unicodedata
 
 import py
@@ -115,6 +116,11 @@ class Session:
         """The bin directory for the virtualenv."""
         return self._runner.venv.bin
 
+    @property
+    def interactive(self):
+        """Returns True if Nox is being run in an interactive session or False otherwise."""
+        return not self._runner.global_config.non_interactive and sys.stdin.isatty()
+
     def chdir(self, dir):
         """Change the current working directory."""
         self.log("cd {}".format(dir))
diff --git a/noxfile.py b/noxfile.py
index b535358..146c91e 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -68,7 +68,7 @@ def docs(session):
     session.cd("docs")
     sphinx_args = ["-b", "html", "-W", "-d", "_build/doctrees", ".", "_build/html"]
 
-    if "serve" not in session.posargs:
+    if not session.interactive:
         sphinx_cmd = "sphinx-build"
     else:
         sphinx_cmd = "sphinx-autobuild"


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index d091fcc..07486ec 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -373,7 +373,7 @@ def test_main_color_from_isatty(monkeypatch, isatty_value, expected):
     monkeypatch.setattr(sys, "argv", [sys.executable])
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
-        with mock.patch("sys.stderr.isatty") as isatty:
+        with mock.patch("sys.stdin.isatty") as isatty:
             isatty.return_value = isatty_value
 
             # Call the main function.
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 7814448..aee7d0d 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -84,6 +84,27 @@ class TestSession:
         assert session.bin is runner.venv.bin
         assert session.python is runner.func.python
 
+    def test_interactive(self):
+        session, runner = self.make_session_and_runner()
+
+        with mock.patch("nox.sessions.sys.stdin.isatty") as m_isatty:
+            m_isatty.return_value = True
+
+            assert session.interactive is True
+
+            m_isatty.return_value = False
+
+            assert session.interactive is False
+
+    def test_explicit_non_interactive(self):
+        session, runner = self.make_session_and_runner()
+
+        with mock.patch("nox.sessions.sys.stdin.isatty") as m_isatty:
+            m_isatty.return_value = True
+            runner.global_config.non_interactive = True
+
+            assert session.interactive is False
+
     def test_chdir(self, tmpdir):
         cdto = str(tmpdir.join("cdbby").ensure(dir=True))
         current_cwd = os.getcwd()