Repository: tox-dev/tox (python)

Issue:
tox --version should display loaded plugins
It seems that currently tox is very secretive about which plugins it did load and that's not giving the best user experience.

Based on the implementations from other tools, usually they display plugin information alongside with their versions, because that results describes the best what you really run.

One nice example is ``pytest --version``:
```
pytest --version                                                                                                                                                              [15:24:20]
This is pytest version 3.0.7, imported from /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/pytest.pyc
setuptools registered plugins:
  pytest-xdist-1.15.0 at /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/xdist/boxed.py
  pytest-xdist-1.15.0 at /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/xdist/looponfail.py
  pytest-xdist-1.15.0 at /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/xdist/plugin.py
  pytest-pep8-1.0.6 at /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/pytest_pep8.py
  pytest-mock-1.6.0 at /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/pytest_mock.py
  pytest-instafail-0.3.0 at /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/pytest_instafail.py
  pytest-cov-2.4.0 at /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/pytest_cov/plugin.py
  flaky-3.3.0 at /Users/ssbarnea/.venv/rh/lib/python2.7/site-packages/flaky/flaky_pytest_plugin.py
```

Pull request description:
tox --version now shows version info of registered plugins
Closes #544 

output from my box, with installed plugins:

    tox --version
    2.8.2.dev27 imported from /home/oliver/work/tox/tox/tox/__init__.py
    registered plugins:
        tox-virtualenv-no-download-1.0.0 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_virtualenv_no_download.py
        tox-travis-0.8 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_travis/hooks.py
        tox-run-command-0.4 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_run_command.py
        tox-pyenv-1.1.0 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_pyenv.py
        tox-py-backwards-0.1 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_py_backwards.py
        tox-pip-extensions-1.1.0 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_pip_extensions.py
        tox-globinterpreter-0.3 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_globinterpreter.py
        tox-docker-1.0.0 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_docker.py
        tox-bitbucket-status-1.0 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/tox_bitbucket_status.py
        tox-battery-0.5 at /home/oliver/.virtualenvs/tmp/lib/python3.6/site-packages/toxbat/requirements.py


Patch:
diff --git a/newsfragments/544.feature b/newsfragments/544.feature
new file mode 100644
index 00000000..a39afa87
--- /dev/null
+++ b/newsfragments/544.feature
@@ -0,0 +1,1 @@
+``tox --version`` now shows information about all registered plugins - by :user:`obestwalter`
diff --git a/tox/config.py b/tox/config.py
index 546f7401..de13eccd 100755
--- a/tox/config.py
+++ b/tox/config.py
@@ -217,7 +217,9 @@ def parseconfig(args=None, plugins=()):
     config = Config(pluginmanager=pm, option=option, interpreters=interpreters)
     config._parser = parser
     config._testenv_attr = parser._testenv_attr
-
+    if config.option.version:
+        print(get_version_info(pm))
+        raise SystemExit(0)
     # parse ini file
     basename = config.option.configfile
     if os.path.isfile(basename):
@@ -258,11 +260,16 @@ def feedback(msg, sysexit=False):
         raise SystemExit(1)
 
 
-class VersionAction(argparse.Action):
-    def __call__(self, argparser, *args, **kwargs):
-        version = tox.__version__
-        print("%s imported from %s" % (version, tox.__file__))
-        raise SystemExit(0)
+def get_version_info(pm):
+    out = ["%s imported from %s" % (tox.__version__, tox.__file__)]
+    plugin_dist_info = pm.list_plugin_distinfo()
+    if plugin_dist_info:
+        out.append('registered plugins:')
+        for mod, egg_info in plugin_dist_info:
+            source = getattr(mod, '__file__', repr(mod))
+            out.append("    %s-%s at %s" % (
+                egg_info.project_name, egg_info.version, source))
+    return '\n'.join(out)
 
 
 class SetenvDict(object):
@@ -312,8 +319,7 @@ class SetenvDict(object):
 @hookimpl
 def tox_addoption(parser):
     # formatter_class=argparse.ArgumentDefaultsHelpFormatter)
-    parser.add_argument("--version", nargs=0, action=VersionAction,
-                        dest="version",
+    parser.add_argument("--version", action="store_true", dest="version",
                         help="report version information to stdout.")
     parser.add_argument("-h", "--help", action="store_true", dest="help",
                         help="show help about options")
diff --git a/tox/interpreters.py b/tox/interpreters.py
index c2f5943c..f3fd63f1 100644
--- a/tox/interpreters.py
+++ b/tox/interpreters.py
@@ -1,5 +1,7 @@
+import distutils.util
 import inspect
 import re
+import subprocess
 import sys
 
 import py
@@ -164,16 +166,14 @@ else:
     def locate_via_py(v_maj, v_min):
         ver = "-%s.%s" % (v_maj, v_min)
         script = "import sys; print(sys.executable)"
-        py_exe = py.path.local.sysfind('py')
+        py_exe = distutils.spawn.find_executable('py')
         if py_exe:
-            try:
-                exe = py_exe.sysexec(ver, '-c', script).strip()
-            except py.process.cmdexec.Error:
-                exe = None
-            if exe:
-                exe = py.path.local(exe)
-                if exe.check():
-                    return exe
+            proc = subprocess.Popen(
+                (py_exe, ver, '-c', script), stdout=subprocess.PIPE,
+            )
+            out, _ = proc.communicate()
+            if not proc.returncode:
+                return out.decode('UTF-8').strip()
 
 
 def pyinfo():
diff --git a/tox/session.py b/tox/session.py
index 3e352549..be8af838 100644
--- a/tox/session.py
+++ b/tox/session.py
@@ -12,7 +12,6 @@ import shutil
 import subprocess
 import sys
 import time
-from subprocess import STDOUT
 
 import py
 
@@ -138,7 +137,7 @@ class Action(object):
             cwd = py.path.local()
         try:
             popen = self._popen(args, cwd, env=env,
-                                stdout=stdout, stderr=STDOUT)
+                                stdout=stdout, stderr=subprocess.STDOUT)
         except OSError as e:
             self.report.error("invocation failed (errno %d), args: %s, cwd: %s" %
                               (e.errno, args, cwd))
@@ -673,12 +672,12 @@ class Session:
 
     def info_versions(self):
         versions = ['tox-%s' % tox.__version__]
-        try:
-            version = py.process.cmdexec("virtualenv --version")
-        except py.process.cmdexec.Error:
-            versions.append("virtualenv-1.9.1 (vendored)")
-        else:
-            versions.append("virtualenv-%s" % version.strip())
+        proc = subprocess.Popen(
+            (sys.executable, '-m', 'virtualenv', '--version'),
+            stdout=subprocess.PIPE,
+        )
+        out, _ = proc.communicate()
+        versions.append('virtualenv-{0}'.format(out.decode('UTF-8').strip()))
         self.report.keyvalue("tool-versions:", " ".join(versions))
 
     def _resolve_pkg(self, pkgspec):


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index dd3009aa..6e6f03ff 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -4,12 +4,14 @@ from textwrap import dedent
 
 import py
 import pytest
+from pluggy import PluginManager
 
 import tox
 import tox.config
 from tox.config import CommandParser
 from tox.config import DepOption
 from tox.config import get_homedir
+from tox.config import get_version_info
 from tox.config import getcontextname
 from tox.config import is_section_substitution
 from tox.config import parseconfig
@@ -2024,13 +2026,58 @@ class TestCmdInvocation:
             "*help*",
         ])
 
-    def test_version(self, cmd):
+    def test_version_simple(self, cmd):
         result = cmd.run("tox", "--version")
         assert not result.ret
         stdout = result.stdout.str()
         assert tox.__version__ in stdout
         assert "imported from" in stdout
 
+    def test_version_no_plugins(self):
+        pm = PluginManager('fakeprject')
+        version_info = get_version_info(pm)
+        assert "imported from" in version_info
+        assert "registered plugins:" not in version_info
+
+    def test_version_with_normal_plugin(self, monkeypatch):
+        def fake_normal_plugin_distinfo():
+            class MockModule:
+                __file__ = 'some-file'
+
+            class MockEggInfo:
+                project_name = 'some-project'
+                version = '1.0'
+            return [(MockModule, MockEggInfo)]
+
+        pm = PluginManager('fakeproject')
+        monkeypatch.setattr(
+            pm, 'list_plugin_distinfo', fake_normal_plugin_distinfo)
+        version_info = get_version_info(pm)
+        assert "registered plugins:" in version_info
+        assert "some-file" in version_info
+        assert "some-project" in version_info
+        assert "1.0" in version_info
+
+    def test_version_with_fileless_module(self, monkeypatch):
+        def fake_no_file_plugin_distinfo():
+            class MockModule:
+                def __repr__(self):
+                    return "some-repr"
+
+            class MockEggInfo:
+                project_name = 'some-project'
+                version = '1.0'
+            return [(MockModule(), MockEggInfo)]
+
+        pm = PluginManager('fakeproject')
+        monkeypatch.setattr(
+            pm, 'list_plugin_distinfo', fake_no_file_plugin_distinfo)
+        version_info = get_version_info(pm)
+        assert "registered plugins:" in version_info
+        assert "some-project" in version_info
+        assert "some-repr" in version_info
+        assert "1.0" in version_info
+
     def test_listenvs(self, cmd, initproj):
         initproj('listenvs', filedefs={
             'tox.ini': '''
diff --git a/tests/test_interpreters.py b/tests/test_interpreters.py
index 06aa76f5..2992d807 100644
--- a/tests/test_interpreters.py
+++ b/tests/test_interpreters.py
@@ -1,3 +1,4 @@
+import distutils.spawn
 import os
 import subprocess
 import sys
@@ -19,20 +20,26 @@ def interpreters():
 
 @pytest.mark.skipif("sys.platform != 'win32'")
 def test_locate_via_py(monkeypatch):
-    class PseudoPy:
-        def sysexec(self, *args):
-            assert args[0] == '-3.2'
-            assert args[1] == '-c'
-            # Return value needs to actually exist!
-            return sys.executable
-
-    @staticmethod
-    def ret_pseudopy(name):
-        assert name == 'py'
-        return PseudoPy()
-    # Monkeypatch py.path.local.sysfind to return PseudoPy
-    monkeypatch.setattr(py.path.local, 'sysfind', ret_pseudopy)
     from tox.interpreters import locate_via_py
+
+    def fake_find_exe(exe):
+        assert exe == 'py'
+        return 'py'
+
+    def fake_popen(cmd, stdout):
+        assert cmd[:3] == ('py', '-3.2', '-c')
+
+        class proc:
+            returncode = 0
+
+            @staticmethod
+            def communicate():
+                return sys.executable.encode(), None
+        return proc
+
+    # Monkeypatch modules to return our faked value
+    monkeypatch.setattr(distutils.spawn, 'find_executable', fake_find_exe)
+    monkeypatch.setattr(subprocess, 'Popen', fake_popen)
     assert locate_via_py('3', '2') == sys.executable