1from __future__ import absolute_import 2 3import logging 4import os 5from email.parser import FeedParser 6 7from pip._vendor import pkg_resources 8from pip._vendor.packaging.utils import canonicalize_name 9 10from pip._internal.cli.base_command import Command 11from pip._internal.cli.status_codes import ERROR, SUCCESS 12from pip._internal.utils.misc import write_output 13from pip._internal.utils.typing import MYPY_CHECK_RUNNING 14 15if MYPY_CHECK_RUNNING: 16 from optparse import Values 17 from typing import Dict, Iterator, List 18 19logger = logging.getLogger(__name__) 20 21 22class ShowCommand(Command): 23 """ 24 Show information about one or more installed packages. 25 26 The output is in RFC-compliant mail header format. 27 """ 28 29 usage = """ 30 %prog [options] <package> ...""" 31 ignore_require_venv = True 32 33 def add_options(self): 34 # type: () -> None 35 self.cmd_opts.add_option( 36 '-f', '--files', 37 dest='files', 38 action='store_true', 39 default=False, 40 help='Show the full list of installed files for each package.') 41 42 self.parser.insert_option_group(0, self.cmd_opts) 43 44 def run(self, options, args): 45 # type: (Values, List[str]) -> int 46 if not args: 47 logger.warning('ERROR: Please provide a package name or names.') 48 return ERROR 49 query = args 50 51 results = search_packages_info(query) 52 if not print_results( 53 results, list_files=options.files, verbose=options.verbose): 54 return ERROR 55 return SUCCESS 56 57 58def search_packages_info(query): 59 # type: (List[str]) -> Iterator[Dict[str, str]] 60 """ 61 Gather details from installed distributions. Print distribution name, 62 version, location, and installed files. Installed files requires a 63 pip generated 'installed-files.txt' in the distributions '.egg-info' 64 directory. 65 """ 66 installed = {} 67 for p in pkg_resources.working_set: 68 installed[canonicalize_name(p.project_name)] = p 69 70 query_names = [canonicalize_name(name) for name in query] 71 missing = sorted( 72 [name for name, pkg in zip(query, query_names) if pkg not in installed] 73 ) 74 if missing: 75 logger.warning('Package(s) not found: %s', ', '.join(missing)) 76 77 def get_requiring_packages(package_name): 78 # type: (str) -> List[str] 79 canonical_name = canonicalize_name(package_name) 80 return [ 81 pkg.project_name for pkg in pkg_resources.working_set 82 if canonical_name in 83 [canonicalize_name(required.name) for required in 84 pkg.requires()] 85 ] 86 87 for dist in [installed[pkg] for pkg in query_names if pkg in installed]: 88 package = { 89 'name': dist.project_name, 90 'version': dist.version, 91 'location': dist.location, 92 'requires': [dep.project_name for dep in dist.requires()], 93 'required_by': get_requiring_packages(dist.project_name) 94 } 95 file_list = None 96 metadata = '' 97 if isinstance(dist, pkg_resources.DistInfoDistribution): 98 # RECORDs should be part of .dist-info metadatas 99 if dist.has_metadata('RECORD'): 100 lines = dist.get_metadata_lines('RECORD') 101 paths = [line.split(',')[0] for line in lines] 102 paths = [os.path.join(dist.location, p) for p in paths] 103 file_list = [os.path.relpath(p, dist.location) for p in paths] 104 105 if dist.has_metadata('METADATA'): 106 metadata = dist.get_metadata('METADATA') 107 else: 108 # Otherwise use pip's log for .egg-info's 109 if dist.has_metadata('installed-files.txt'): 110 paths = dist.get_metadata_lines('installed-files.txt') 111 paths = [os.path.join(dist.egg_info, p) for p in paths] 112 file_list = [os.path.relpath(p, dist.location) for p in paths] 113 114 if dist.has_metadata('PKG-INFO'): 115 metadata = dist.get_metadata('PKG-INFO') 116 117 if dist.has_metadata('entry_points.txt'): 118 entry_points = dist.get_metadata_lines('entry_points.txt') 119 package['entry_points'] = entry_points 120 121 if dist.has_metadata('INSTALLER'): 122 for line in dist.get_metadata_lines('INSTALLER'): 123 if line.strip(): 124 package['installer'] = line.strip() 125 break 126 127 # @todo: Should pkg_resources.Distribution have a 128 # `get_pkg_info` method? 129 feed_parser = FeedParser() 130 feed_parser.feed(metadata) 131 pkg_info_dict = feed_parser.close() 132 for key in ('metadata-version', 'summary', 133 'home-page', 'author', 'author-email', 'license'): 134 package[key] = pkg_info_dict.get(key) 135 136 # It looks like FeedParser cannot deal with repeated headers 137 classifiers = [] 138 for line in metadata.splitlines(): 139 if line.startswith('Classifier: '): 140 classifiers.append(line[len('Classifier: '):]) 141 package['classifiers'] = classifiers 142 143 if file_list: 144 package['files'] = sorted(file_list) 145 yield package 146 147 148def print_results(distributions, list_files=False, verbose=False): 149 # type: (Iterator[Dict[str, str]], bool, bool) -> bool 150 """ 151 Print the information from installed distributions found. 152 """ 153 results_printed = False 154 for i, dist in enumerate(distributions): 155 results_printed = True 156 if i > 0: 157 write_output("---") 158 159 write_output("Name: %s", dist.get('name', '')) 160 write_output("Version: %s", dist.get('version', '')) 161 write_output("Summary: %s", dist.get('summary', '')) 162 write_output("Home-page: %s", dist.get('home-page', '')) 163 write_output("Author: %s", dist.get('author', '')) 164 write_output("Author-email: %s", dist.get('author-email', '')) 165 write_output("License: %s", dist.get('license', '')) 166 write_output("Location: %s", dist.get('location', '')) 167 write_output("Requires: %s", ', '.join(dist.get('requires', []))) 168 write_output("Required-by: %s", ', '.join(dist.get('required_by', []))) 169 170 if verbose: 171 write_output("Metadata-Version: %s", 172 dist.get('metadata-version', '')) 173 write_output("Installer: %s", dist.get('installer', '')) 174 write_output("Classifiers:") 175 for classifier in dist.get('classifiers', []): 176 write_output(" %s", classifier) 177 write_output("Entry-points:") 178 for entry in dist.get('entry_points', []): 179 write_output(" %s", entry.strip()) 180 if list_files: 181 write_output("Files:") 182 for line in dist.get('files', []): 183 write_output(" %s", line.strip()) 184 if "files" not in dist: 185 write_output("Cannot locate installed-files.txt") 186 return results_printed 187