Repository: pypa/pip

Issue:
Include vendored library versions in pip debug
It might actually be useful [snip] to include the vendored libraries' version in `pip debug` output to help diagnose _tweaked_ installation.

_Originally posted by @xavfernandez in https://github.com/pypa/pip/pull/7705#discussion_r384462481_


Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -67,6 +67,7 @@ def get_version(rel_path):
         exclude=["contrib", "docs", "tests*", "tasks"],
     ),
     package_data={
+        "pip._vendor": ["vendor.txt"],
         "pip._vendor.certifi": ["*.pem"],
         "pip._vendor.requests": ["*.pem"],
         "pip._vendor.distlib._backport": ["sysconfig.cfg"],
diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py
--- a/src/pip/_internal/commands/debug.py
+++ b/src/pip/_internal/commands/debug.py
@@ -8,8 +8,11 @@
 import os
 import sys
 
+import pip._vendor
+from pip._vendor import pkg_resources
 from pip._vendor.certifi import where
 
+from pip import __file__ as pip_location
 from pip._internal.cli import cmdoptions
 from pip._internal.cli.base_command import Command
 from pip._internal.cli.cmdoptions import make_target_python
@@ -19,7 +22,8 @@
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
 if MYPY_CHECK_RUNNING:
-    from typing import Any, List, Optional
+    from types import ModuleType
+    from typing import Any, List, Optional, Dict
     from optparse import Values
 
 logger = logging.getLogger(__name__)
@@ -43,6 +47,93 @@ def show_sys_implementation():
         show_value('name', implementation_name)
 
 
+def create_vendor_txt_map():
+    # type: () -> Dict[str, str]
+    vendor_txt_path = os.path.join(
+        os.path.dirname(pip_location),
+        '_vendor',
+        'vendor.txt'
+    )
+
+    with open(vendor_txt_path) as f:
+        # Purge non version specifying lines.
+        # Also, remove any space prefix or suffixes (including comments).
+        lines = [line.strip().split(' ', 1)[0]
+                 for line in f.readlines() if '==' in line]
+
+    # Transform into "module" -> version dict.
+    return dict(line.split('==', 1) for line in lines)  # type: ignore
+
+
+def get_module_from_module_name(module_name):
+    # type: (str) -> ModuleType
+
+    # Module name can be uppercase in vendor.txt for some reason...
+    module_name = module_name.lower()
+    # PATCH: setuptools is actually only pkg_resources.
+    if module_name == 'setuptools':
+        module_name = 'pkg_resources'
+
+    __import__(
+        'pip._vendor.{}'.format(module_name),
+        globals(),
+        locals(),
+        level=0
+    )
+    return getattr(pip._vendor, module_name)
+
+
+def get_vendor_version_from_module(module_name):
+    # type: (str) -> str
+
+    module = get_module_from_module_name(module_name)
+    version = getattr(module, '__version__', None)
+
+    if not version:
+        # Try to find version in debundled module info
+        pkg_set = pkg_resources.WorkingSet(
+            [os.path.dirname(getattr(module, '__file__'))]
+        )
+        package = pkg_set.find(pkg_resources.Requirement.parse(module_name))
+        version = getattr(package, 'version', None)
+
+    return version
+
+
+def show_actual_vendor_versions(vendor_txt_versions):
+    # type: (Dict[str, str]) -> None
+    # Logs the actual version and print extra info
+    # if there is a conflict or if the actual version could not be imported.
+
+    for module_name, expected_version in vendor_txt_versions.items():
+        extra_message = ''
+        actual_version = get_vendor_version_from_module(module_name)
+        if not actual_version:
+            extra_message = ' (Unable to locate actual module version, using'\
+                            ' vendor.txt specified version)'
+            actual_version = expected_version
+        elif actual_version != expected_version:
+            extra_message = ' (CONFLICT: vendor.txt suggests version should'\
+                            ' be {})'.format(expected_version)
+
+        logger.info(
+            '{name}=={actual}{extra}'.format(
+                name=module_name,
+                actual=actual_version,
+                extra=extra_message
+            )
+        )
+
+
+def show_vendor_versions():
+    # type: () -> None
+    logger.info('vendored library versions:')
+
+    vendor_txt_versions = create_vendor_txt_map()
+    with indent_log():
+        show_actual_vendor_versions(vendor_txt_versions)
+
+
 def show_tags(options):
     # type: (Values) -> None
     tag_limit = 10
@@ -136,6 +227,9 @@ def run(self, options, args):
         show_value("REQUESTS_CA_BUNDLE", os.environ.get('REQUESTS_CA_BUNDLE'))
         show_value("CURL_CA_BUNDLE", os.environ.get('CURL_CA_BUNDLE'))
         show_value("pip._vendor.certifi.where()", where())
+        show_value("pip._vendor.DEBUNDLED", pip._vendor.DEBUNDLED)
+
+        show_vendor_versions()
 
         show_tags(options)
 


Test patch:
diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py
--- a/tests/functional/test_debug.py
+++ b/tests/functional/test_debug.py
@@ -1,5 +1,6 @@
 import pytest
 
+from pip._internal.commands.debug import create_vendor_txt_map
 from pip._internal.utils import compatibility_tags
 
 
@@ -14,6 +15,8 @@
     'REQUESTS_CA_BUNDLE: ',
     'CURL_CA_BUNDLE: ',
     'pip._vendor.certifi.where(): ',
+    'pip._vendor.DEBUNDLED: ',
+    'vendored library versions:',
 
 ])
 def test_debug(script, expected_text):
@@ -27,6 +30,18 @@ def test_debug(script, expected_text):
     assert expected_text in stdout
 
 
+def test_debug__library_versions(script):
+    """
+    Check the library versions normal output.
+    """
+    args = ['debug']
+    result = script.pip(*args, allow_stderr_warning=True)
+    stdout = result.stdout
+    vendored_versions = create_vendor_txt_map()
+    for name, value in vendored_versions.items():
+        assert '{}=={}'.format(name, value) in stdout
+
+
 @pytest.mark.parametrize(
     'args',
     [