Repository: tox-dev/tox

Issue:
skip missing interpreters should be a toggle-able flag
It makes sense to set skip missing interpreters inside the tox file to ensure that users locally don't get failures for interpreters they don't have. In this cases, it should be still possible to override that and force not skipping missing interpreters in the CI environment for example. 

Discussion:

Patch:
diff --git a/changelog/903.feature.rst b/changelog/903.feature.rst
new file mode 100644
index 00000000..bb700ffb
--- /dev/null
+++ b/changelog/903.feature.rst
@@ -0,0 +1,1 @@
+skip missing interpreters value from the config file can now be overridden via the ``--skip-missing-interpreters`` cli flag - by :user:`gaborbernat`
diff --git a/doc/config.rst b/doc/config.rst
index e56fe8de..cb47a68f 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -54,18 +54,18 @@ and will first lookup global tox settings in this section:
     commands = ...  # override [tox] settings for the jenkins context
     # note: for jenkins distshare defaults to ``{toxworkdir}/distshare`` (DEPRECATED)
 
-.. confval:: skip_missing_interpreters=BOOL
+.. confval:: skip_missing_interpreters=config|true|false
 
     .. versionadded:: 1.7.2
 
-    Setting this to ``True`` is equivalent of passing the
-    ``--skip-missing-interpreters`` command line option, and will force ``tox`` to
-    return success even if some of the specified environments were missing. This is
-    useful for some CI systems or running on a developer box, where you might only
-    have a subset of all your supported interpreters installed but don't want to
-    mark the build as failed because of it. As expected, the command line switch
-    always overrides this setting if passed on the invokation.
-    **Default:** ``False``
+    When skip missing interpreters is ``true`` will force ``tox`` to return success even
+    if some of the specified environments were missing. This is useful for some CI
+    systems or running on a developer box, where you might only have a subset of
+    all your supported interpreters installed but don't want to mark the build as
+    failed because of it. As expected, the command line switch always overrides
+    this setting if passed on the invocation. Setting it to ``config``
+    means that the value is read from the config file (default is ``false``).
+    **Default:** ``config``
 
 .. confval:: envlist=CSV
 
diff --git a/src/tox/config.py b/src/tox/config.py
index a7e62d52..12ac98c5 100755
--- a/src/tox/config.py
+++ b/src/tox/config.py
@@ -493,11 +493,8 @@ def tox_addoption(parser):
     parser.add_argument(
         "--alwayscopy", action="store_true", help="override alwayscopy setting to True in all envs"
     )
-    parser.add_argument(
-        "--skip-missing-interpreters",
-        action="store_true",
-        help="don't fail tests for missing interpreters",
-    )
+
+    cli_skip_missing_interpreter(parser)
     parser.add_argument(
         "--workdir",
         action="store",
@@ -780,6 +777,24 @@ def tox_addoption(parser):
     )
 
 
+def cli_skip_missing_interpreter(parser):
+    class SkipMissingInterpreterAction(argparse.Action):
+        def __call__(self, parser, namespace, values, option_string=None):
+            value = "true" if values is None else values
+            if value not in ("config", "true", "false"):
+                raise argparse.ArgumentTypeError("value must be config, true or false")
+            setattr(namespace, self.dest, value)
+
+    parser.add_argument(
+        "--skip-missing-interpreters",
+        default="config",
+        metavar="val",
+        nargs="?",
+        action=SkipMissingInterpreterAction,
+        help="don't fail tests for missing interpreters: {config,true,false} choice",
+    )
+
+
 class Config(object):
     """Global Tox config object."""
 
@@ -947,10 +962,9 @@ class ParseIni(object):
         else:
             config.toxworkdir = config.toxinidir.join(config.option.workdir, abs=True)
 
-        if not config.option.skip_missing_interpreters:
-            config.option.skip_missing_interpreters = reader.getbool(
-                "skip_missing_interpreters", False
-            )
+        if config.option.skip_missing_interpreters == "config":
+            val = reader.getbool("skip_missing_interpreters", False)
+            config.option.skip_missing_interpreters = "true" if val else "false"
 
         config.ignore_basepython_conflict = reader.getbool("ignore_basepython_conflict", False)
 
diff --git a/src/tox/session.py b/src/tox/session.py
index 97e5da19..07e99708 100644
--- a/src/tox/session.py
+++ b/src/tox/session.py
@@ -470,7 +470,7 @@ class Session:
                 )
             except tox.exception.InterpreterNotFound as e:
                 status = e
-                if self.config.option.skip_missing_interpreters:
+                if self.config.option.skip_missing_interpreters == "true":
                     default_ret_code = 0
             if status:
                 str_status = str(status)
@@ -573,7 +573,7 @@ class Session:
             status = venv.status
             if isinstance(status, tox.exception.InterpreterNotFound):
                 msg = " {}: {}".format(venv.envconfig.envname, str(status))
-                if self.config.option.skip_missing_interpreters:
+                if self.config.option.skip_missing_interpreters == "true":
                     self.report.skip(msg)
                 else:
                     retcode = 1


Test patch:
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
index 8b5d2285..3b53bad2 100644
--- a/tests/unit/test_config.py
+++ b/tests/unit/test_config.py
@@ -1902,20 +1902,48 @@ class TestGlobalOptions:
             newconfig([], inisource)
 
     def test_skip_missing_interpreters_true(self, newconfig):
-        inisource = """
+        ini_source = """
             [tox]
             skip_missing_interpreters = True
         """
-        config = newconfig([], inisource)
-        assert config.option.skip_missing_interpreters
+        config = newconfig([], ini_source)
+        assert config.option.skip_missing_interpreters == "true"
 
     def test_skip_missing_interpreters_false(self, newconfig):
-        inisource = """
+        ini_source = """
             [tox]
             skip_missing_interpreters = False
         """
-        config = newconfig([], inisource)
-        assert not config.option.skip_missing_interpreters
+        config = newconfig([], ini_source)
+        assert config.option.skip_missing_interpreters == "false"
+
+    def test_skip_missing_interpreters_cli_no_arg(self, newconfig):
+        ini_source = """
+            [tox]
+            skip_missing_interpreters = False
+        """
+        config = newconfig(["--skip-missing-interpreters"], ini_source)
+        assert config.option.skip_missing_interpreters == "true"
+
+    def test_skip_missing_interpreters_cli_not_specified(self, newconfig):
+        config = newconfig([], "")
+        assert config.option.skip_missing_interpreters == "false"
+
+    def test_skip_missing_interpreters_cli_overrides_true(self, newconfig):
+        ini_source = """
+                    [tox]
+                    skip_missing_interpreters = False
+                """
+        config = newconfig(["--skip-missing-interpreters", "true"], ini_source)
+        assert config.option.skip_missing_interpreters == "true"
+
+    def test_skip_missing_interpreters_cli_overrides_false(self, newconfig):
+        ini_source = """
+                    [tox]
+                    skip_missing_interpreters = True
+                """
+        config = newconfig(["--skip-missing-interpreters", "false"], ini_source)
+        assert config.option.skip_missing_interpreters == "false"
 
     def test_defaultenv_commandline(self, newconfig):
         config = newconfig(["-epy27"], "")