1from __future__ import absolute_import
2
3import locale
4import logging
5import os
6import sys
7
8import pip._vendor
9from pip._vendor import pkg_resources
10from pip._vendor.certifi import where
11
12from pip import __file__ as pip_location
13from pip._internal.cli import cmdoptions
14from pip._internal.cli.base_command import Command
15from pip._internal.cli.cmdoptions import make_target_python
16from pip._internal.cli.status_codes import SUCCESS
17from pip._internal.utils.logging import indent_log
18from pip._internal.utils.misc import get_pip_version
19from pip._internal.utils.typing import MYPY_CHECK_RUNNING
20
21if MYPY_CHECK_RUNNING:
22    from optparse import Values
23    from types import ModuleType
24    from typing import Dict, List, Optional
25
26    from pip._internal.configuration import Configuration
27
28logger = logging.getLogger(__name__)
29
30
31def show_value(name, value):
32    # type: (str, Optional[str]) -> None
33    logger.info('%s: %s', name, value)
34
35
36def show_sys_implementation():
37    # type: () -> None
38    logger.info('sys.implementation:')
39    if hasattr(sys, 'implementation'):
40        implementation = sys.implementation  # type: ignore
41        implementation_name = implementation.name
42    else:
43        implementation_name = ''
44
45    with indent_log():
46        show_value('name', implementation_name)
47
48
49def create_vendor_txt_map():
50    # type: () -> Dict[str, str]
51    vendor_txt_path = os.path.join(
52        os.path.dirname(pip_location),
53        '_vendor',
54        'vendor.txt'
55    )
56
57    with open(vendor_txt_path) as f:
58        # Purge non version specifying lines.
59        # Also, remove any space prefix or suffixes (including comments).
60        lines = [line.strip().split(' ', 1)[0]
61                 for line in f.readlines() if '==' in line]
62
63    # Transform into "module" -> version dict.
64    return dict(line.split('==', 1) for line in lines)  # type: ignore
65
66
67def get_module_from_module_name(module_name):
68    # type: (str) -> ModuleType
69    # Module name can be uppercase in vendor.txt for some reason...
70    module_name = module_name.lower()
71    # PATCH: setuptools is actually only pkg_resources.
72    if module_name == 'setuptools':
73        module_name = 'pkg_resources'
74
75    __import__(
76        'pip._vendor.{}'.format(module_name),
77        globals(),
78        locals(),
79        level=0
80    )
81    return getattr(pip._vendor, module_name)
82
83
84def get_vendor_version_from_module(module_name):
85    # type: (str) -> Optional[str]
86    module = get_module_from_module_name(module_name)
87    version = getattr(module, '__version__', None)
88
89    if not version:
90        # Try to find version in debundled module info
91        # The type for module.__file__ is Optional[str] in
92        # Python 2, and str in Python 3. The type: ignore is
93        # added to account for Python 2, instead of a cast
94        # and should be removed once we drop Python 2 support
95        pkg_set = pkg_resources.WorkingSet(
96            [os.path.dirname(module.__file__)]  # type: ignore
97        )
98        package = pkg_set.find(pkg_resources.Requirement.parse(module_name))
99        version = getattr(package, 'version', None)
100
101    return version
102
103
104def show_actual_vendor_versions(vendor_txt_versions):
105    # type: (Dict[str, str]) -> None
106    """Log the actual version and print extra info if there is
107    a conflict or if the actual version could not be imported.
108    """
109    for module_name, expected_version in vendor_txt_versions.items():
110        extra_message = ''
111        actual_version = get_vendor_version_from_module(module_name)
112        if not actual_version:
113            extra_message = ' (Unable to locate actual module version, using'\
114                            ' vendor.txt specified version)'
115            actual_version = expected_version
116        elif actual_version != expected_version:
117            extra_message = ' (CONFLICT: vendor.txt suggests version should'\
118                            ' be {})'.format(expected_version)
119        logger.info('%s==%s%s', module_name, actual_version, extra_message)
120
121
122def show_vendor_versions():
123    # type: () -> None
124    logger.info('vendored library versions:')
125
126    vendor_txt_versions = create_vendor_txt_map()
127    with indent_log():
128        show_actual_vendor_versions(vendor_txt_versions)
129
130
131def show_tags(options):
132    # type: (Values) -> None
133    tag_limit = 10
134
135    target_python = make_target_python(options)
136    tags = target_python.get_tags()
137
138    # Display the target options that were explicitly provided.
139    formatted_target = target_python.format_given()
140    suffix = ''
141    if formatted_target:
142        suffix = ' (target: {})'.format(formatted_target)
143
144    msg = 'Compatible tags: {}{}'.format(len(tags), suffix)
145    logger.info(msg)
146
147    if options.verbose < 1 and len(tags) > tag_limit:
148        tags_limited = True
149        tags = tags[:tag_limit]
150    else:
151        tags_limited = False
152
153    with indent_log():
154        for tag in tags:
155            logger.info(str(tag))
156
157        if tags_limited:
158            msg = (
159                '...\n'
160                '[First {tag_limit} tags shown. Pass --verbose to show all.]'
161            ).format(tag_limit=tag_limit)
162            logger.info(msg)
163
164
165def ca_bundle_info(config):
166    # type: (Configuration) -> str
167    levels = set()
168    for key, _ in config.items():
169        levels.add(key.split('.')[0])
170
171    if not levels:
172        return "Not specified"
173
174    levels_that_override_global = ['install', 'wheel', 'download']
175    global_overriding_level = [
176        level for level in levels if level in levels_that_override_global
177    ]
178    if not global_overriding_level:
179        return 'global'
180
181    if 'global' in levels:
182        levels.remove('global')
183    return ", ".join(levels)
184
185
186class DebugCommand(Command):
187    """
188    Display debug information.
189    """
190
191    usage = """
192      %prog <options>"""
193    ignore_require_venv = True
194
195    def add_options(self):
196        # type: () -> None
197        cmdoptions.add_target_python_options(self.cmd_opts)
198        self.parser.insert_option_group(0, self.cmd_opts)
199        self.parser.config.load()
200
201    def run(self, options, args):
202        # type: (Values, List[str]) -> int
203        logger.warning(
204            "This command is only meant for debugging. "
205            "Do not use this with automation for parsing and getting these "
206            "details, since the output and options of this command may "
207            "change without notice."
208        )
209        show_value('pip version', get_pip_version())
210        show_value('sys.version', sys.version)
211        show_value('sys.executable', sys.executable)
212        show_value('sys.getdefaultencoding', sys.getdefaultencoding())
213        show_value('sys.getfilesystemencoding', sys.getfilesystemencoding())
214        show_value(
215            'locale.getpreferredencoding', locale.getpreferredencoding(),
216        )
217        show_value('sys.platform', sys.platform)
218        show_sys_implementation()
219
220        show_value("'cert' config value", ca_bundle_info(self.parser.config))
221        show_value("REQUESTS_CA_BUNDLE", os.environ.get('REQUESTS_CA_BUNDLE'))
222        show_value("CURL_CA_BUNDLE", os.environ.get('CURL_CA_BUNDLE'))
223        show_value("pip._vendor.certifi.where()", where())
224        show_value("pip._vendor.DEBUNDLED", pip._vendor.DEBUNDLED)
225
226        show_vendor_versions()
227
228        show_tags(options)
229
230        return SUCCESS
231