Repository: tox-dev/tox

Issue:
using py2 and py3 in factors causes a "conflicting basepython version" warning
I've narrowed it down to this line
https://github.com/tox-dev/tox/blob/d6cd8d42fa06cc02d747ebe92b3179bc677f0591/src/tox/config/__init__.py#L574

Discussion:
demosdemon: tests pass! 🎉 

Patch:
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index 5d7eae23..f5447f88 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -24,6 +24,7 @@ Eugene Yunak
 Fernando L. Pereira
 Florian Preinstorfer
 Florian Schulze
+Gonéri Le Bouder
 Hazal Ozturk
 Henk-Jaap Wagenaar
 Ian Stapleton Cordasco
@@ -32,6 +33,7 @@ Ionel Maries Cristian
 Itxaka Serrano
 Jake Windle
 Jannis Leidel
+Joachim Brandon LeBlanc
 Johannes Christ
 Jon Dufresne
 Josh Smeaton
diff --git a/MANIFEST.in b/MANIFEST.in
index 0a9d6ae9..ed0088a1 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,7 +4,7 @@ include CONTRIBUTORS
 include LICENSE
 include setup.py
 include tox.ini
-graft doc
+graft docs
 graft tests
 
 global-exclude __pycache__
diff --git a/docs/changelog/1153.bugfix.rst b/docs/changelog/1153.bugfix.rst
new file mode 100644
index 00000000..bfe6c356
--- /dev/null
+++ b/docs/changelog/1153.bugfix.rst
@@ -0,0 +1,1 @@
+Using ``py2`` and ``py3`` with a specific ``basepython`` will no longer raise a warning unless the major version conflicts - by :user:`demosdemon`.
diff --git a/docs/changelog/1155.bugix.rst b/docs/changelog/1155.bugix.rst
new file mode 100644
index 00000000..c9f208d7
--- /dev/null
+++ b/docs/changelog/1155.bugix.rst
@@ -0,0 +1,1 @@
+The ``-eALL`` command line argument now expands the ``envlist`` key and includes all its environment.
diff --git a/src/tox/config/__init__.py b/src/tox/config/__init__.py
index bf4252e3..c50c713b 100644
--- a/src/tox/config/__init__.py
+++ b/src/tox/config/__init__.py
@@ -571,7 +571,8 @@ def tox_addoption(parser):
                 proposed_version = "".join(
                     str(i) for i in python_info_for_proposed.version_info[0:2]
                 )
-                if implied_version != proposed_version:
+                # '27'.startswith('2') or '27'.startswith('27')
+                if not proposed_version.startswith(implied_version):
                     # TODO(stephenfin): Raise an exception here in tox 4.0
                     warnings.warn(
                         "conflicting basepython version (set {}, should be {}) for env '{}';"
@@ -1155,39 +1156,54 @@ class ParseIni(object):
                 reader.addsubstitutions(**{env_attr.name: res})
         return tc
 
-    def _getenvdata(self, reader, config):
-        candidates = (
-            os.environ.get(PARALLEL_ENV_VAR_KEY),
-            self.config.option.env,
-            os.environ.get("TOXENV"),
-            reader.getstring("envlist", replace=False),
-        )
-        env_str = next((i for i in candidates if i), [])
+    def _getallenvs(self, reader, extra_env_list=None):
+        extra_env_list = extra_env_list or []
+        env_str = reader.getstring("envlist", replace=False)
         env_list = _split_env(env_str)
+        for env in extra_env_list:
+            if env not in env_list:
+                env_list.append(env)
 
-        # collect section envs
         all_envs = OrderedDict((i, None) for i in env_list)
-        if "ALL" in all_envs:
-            all_envs.pop("ALL")
         for section in self._cfg:
             if section.name.startswith(testenvprefix):
                 all_envs[section.name[len(testenvprefix) :]] = None
         if not all_envs:
             all_envs["python"] = None
+        return list(all_envs.keys())
+
+    def _getenvdata(self, reader, config):
+        from_option = self.config.option.env
+        from_environ = os.environ.get("TOXENV")
+        from_config = reader.getstring("envlist", replace=False)
+
+        env_list = []
+        if (from_option and "ALL" in from_option) or (
+            not from_option and from_environ and "ALL" in from_environ.split(",")
+        ):
+            all_envs = self._getallenvs(reader)
+        else:
+            candidates = (
+                os.environ.get(PARALLEL_ENV_VAR_KEY),
+                from_option,
+                from_environ,
+                from_config,
+            )
+            env_str = next((i for i in candidates if i), [])
+            env_list = _split_env(env_str)
+            all_envs = self._getallenvs(reader, env_list)
+
+        if not env_list:
+            env_list = all_envs
 
         package_env = config.isolated_build_env
         if config.isolated_build is True and package_env in all_envs:
-            all_envs.pop(package_env)
-
-        if not env_list or "ALL" in env_list:
-            env_list = list(all_envs.keys())
+            all_envs.remove(package_env)
 
         if config.isolated_build is True and package_env in env_list:
             msg = "isolated_build_env {} cannot be part of envlist".format(package_env)
             raise tox.exception.ConfigError(msg)
-
-        all_env_list = list(all_envs.keys())
-        return env_list, all_env_list
+        return env_list, all_envs
 
 
 def _split_env(env):


Test patch:
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
index 00524bc4..33969985 100644
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -1910,6 +1910,46 @@ class TestConfigTestEnv:
         assert env_config.basepython == "python{}.{}".format(major, minor - 1)
         assert not recwarn.list, "\n".join(repr(i.message) for i in recwarn.list)
 
+    def test_default_single_digit_factors(self, newconfig, monkeypatch):
+        from tox.interpreters import Interpreters
+
+        def get_executable(self, envconfig):
+            return sys.executable
+
+        monkeypatch.setattr(Interpreters, "get_executable", get_executable)
+
+        major, minor = sys.version_info[0:2]
+
+        with pytest.warns(None) as lying:
+            config = newconfig(
+                """
+                [testenv:py{0}]
+                basepython=python{0}.{1}
+                commands = python --version
+                """.format(
+                    major, minor - 1
+                )
+            )
+
+        env_config = config.envconfigs["py{}".format(major)]
+        assert env_config.basepython == "python{}.{}".format(major, minor - 1)
+        assert len(lying) == 0, "\n".join(repr(r.message) for r in lying)
+
+        with pytest.warns(None) as truthful:
+            config = newconfig(
+                """
+                [testenv:py{0}]
+                basepython=python{0}.{1}
+                commands = python --version
+                """.format(
+                    major, minor
+                )
+            )
+
+        env_config = config.envconfigs["py{}".format(major)]
+        assert env_config.basepython == "python{}.{}".format(major, minor)
+        assert len(truthful) == 0, "\n".join(repr(r.message) for r in truthful)
+
     def test_default_factors_conflict_ignore(self, newconfig, capsys):
         with pytest.warns(None) as record:
             config = newconfig(
@@ -2065,7 +2105,7 @@ class TestGlobalOptions:
         config = newconfig([], "")
         assert not config.sdistsrc
 
-    def test_env_selection(self, newconfig, monkeypatch):
+    def test_env_selection_with_section_name(self, newconfig, monkeypatch):
         inisource = """
             [tox]
             envlist = py36
@@ -2091,6 +2131,18 @@ class TestGlobalOptions:
         config = newconfig(["-espam"], inisource)
         assert config.envlist == ["spam"]
 
+    def test_env_selection_expanded_envlist(self, newconfig, monkeypatch):
+        inisource = """
+            [tox]
+            envlist = py{36,35,27}
+            [testenv:py36]
+            basepython=python3.6
+        """
+        config = newconfig([], inisource)
+        assert config.envlist == ["py36", "py35", "py27"]
+        config = newconfig(["-eALL"], inisource)
+        assert config.envlist == ["py36", "py35", "py27"]
+
     def test_py_venv(self, newconfig):
         config = newconfig(["-epy"], "")
         env = config.envconfigs["py"]