1# The following comment should be removed at some point in the future. 2# mypy: disallow-untyped-defs=False 3 4from __future__ import absolute_import 5 6import json 7import logging 8 9from pipenv.patched.notpip._vendor import six 10from pipenv.patched.notpip._vendor.six.moves import zip_longest 11 12from pipenv.patched.notpip._internal.cli import cmdoptions 13from pipenv.patched.notpip._internal.cli.req_command import IndexGroupCommand 14from pipenv.patched.notpip._internal.exceptions import CommandError 15from pipenv.patched.notpip._internal.index.package_finder import PackageFinder 16from pipenv.patched.notpip._internal.models.selection_prefs import SelectionPreferences 17from pipenv.patched.notpip._internal.self_outdated_check import make_link_collector 18from pipenv.patched.notpip._internal.utils.misc import ( 19 dist_is_editable, 20 get_installed_distributions, 21 write_output, 22) 23from pipenv.patched.notpip._internal.utils.packaging import get_installer 24 25logger = logging.getLogger(__name__) 26 27 28class ListCommand(IndexGroupCommand): 29 """ 30 List installed packages, including editables. 31 32 Packages are listed in a case-insensitive sorted order. 33 """ 34 35 usage = """ 36 %prog [options]""" 37 38 def __init__(self, *args, **kw): 39 super(ListCommand, self).__init__(*args, **kw) 40 41 cmd_opts = self.cmd_opts 42 43 cmd_opts.add_option( 44 '-o', '--outdated', 45 action='store_true', 46 default=False, 47 help='List outdated packages') 48 cmd_opts.add_option( 49 '-u', '--uptodate', 50 action='store_true', 51 default=False, 52 help='List uptodate packages') 53 cmd_opts.add_option( 54 '-e', '--editable', 55 action='store_true', 56 default=False, 57 help='List editable projects.') 58 cmd_opts.add_option( 59 '-l', '--local', 60 action='store_true', 61 default=False, 62 help=('If in a virtualenv that has global access, do not list ' 63 'globally-installed packages.'), 64 ) 65 self.cmd_opts.add_option( 66 '--user', 67 dest='user', 68 action='store_true', 69 default=False, 70 help='Only output packages installed in user-site.') 71 cmd_opts.add_option(cmdoptions.list_path()) 72 cmd_opts.add_option( 73 '--pre', 74 action='store_true', 75 default=False, 76 help=("Include pre-release and development versions. By default, " 77 "pip only finds stable versions."), 78 ) 79 80 cmd_opts.add_option( 81 '--format', 82 action='store', 83 dest='list_format', 84 default="columns", 85 choices=('columns', 'freeze', 'json'), 86 help="Select the output format among: columns (default), freeze, " 87 "or json", 88 ) 89 90 cmd_opts.add_option( 91 '--not-required', 92 action='store_true', 93 dest='not_required', 94 help="List packages that are not dependencies of " 95 "installed packages.", 96 ) 97 98 cmd_opts.add_option( 99 '--exclude-editable', 100 action='store_false', 101 dest='include_editable', 102 help='Exclude editable package from output.', 103 ) 104 cmd_opts.add_option( 105 '--include-editable', 106 action='store_true', 107 dest='include_editable', 108 help='Include editable package from output.', 109 default=True, 110 ) 111 index_opts = cmdoptions.make_option_group( 112 cmdoptions.index_group, self.parser 113 ) 114 115 self.parser.insert_option_group(0, index_opts) 116 self.parser.insert_option_group(0, cmd_opts) 117 118 def _build_package_finder(self, options, session): 119 """ 120 Create a package finder appropriate to this list command. 121 """ 122 link_collector = make_link_collector(session, options=options) 123 124 # Pass allow_yanked=False to ignore yanked versions. 125 selection_prefs = SelectionPreferences( 126 allow_yanked=False, 127 allow_all_prereleases=options.pre, 128 ) 129 130 return PackageFinder.create( 131 link_collector=link_collector, 132 selection_prefs=selection_prefs, 133 ) 134 135 def run(self, options, args): 136 if options.outdated and options.uptodate: 137 raise CommandError( 138 "Options --outdated and --uptodate cannot be combined.") 139 140 cmdoptions.check_list_path_option(options) 141 142 packages = get_installed_distributions( 143 local_only=options.local, 144 user_only=options.user, 145 editables_only=options.editable, 146 include_editables=options.include_editable, 147 paths=options.path, 148 ) 149 150 # get_not_required must be called firstly in order to find and 151 # filter out all dependencies correctly. Otherwise a package 152 # can't be identified as requirement because some parent packages 153 # could be filtered out before. 154 if options.not_required: 155 packages = self.get_not_required(packages, options) 156 157 if options.outdated: 158 packages = self.get_outdated(packages, options) 159 elif options.uptodate: 160 packages = self.get_uptodate(packages, options) 161 162 self.output_package_listing(packages, options) 163 164 def get_outdated(self, packages, options): 165 return [ 166 dist for dist in self.iter_packages_latest_infos(packages, options) 167 if dist.latest_version > dist.parsed_version 168 ] 169 170 def get_uptodate(self, packages, options): 171 return [ 172 dist for dist in self.iter_packages_latest_infos(packages, options) 173 if dist.latest_version == dist.parsed_version 174 ] 175 176 def get_not_required(self, packages, options): 177 dep_keys = set() 178 for dist in packages: 179 dep_keys.update(requirement.key for requirement in dist.requires()) 180 return {pkg for pkg in packages if pkg.key not in dep_keys} 181 182 def iter_packages_latest_infos(self, packages, options): 183 with self._build_session(options) as session: 184 finder = self._build_package_finder(options, session) 185 186 for dist in packages: 187 typ = 'unknown' 188 all_candidates = finder.find_all_candidates(dist.key) 189 if not options.pre: 190 # Remove prereleases 191 all_candidates = [candidate for candidate in all_candidates 192 if not candidate.version.is_prerelease] 193 194 evaluator = finder.make_candidate_evaluator( 195 project_name=dist.project_name, 196 ) 197 best_candidate = evaluator.sort_best_candidate(all_candidates) 198 if best_candidate is None: 199 continue 200 201 remote_version = best_candidate.version 202 if best_candidate.link.is_wheel: 203 typ = 'wheel' 204 else: 205 typ = 'sdist' 206 # This is dirty but makes the rest of the code much cleaner 207 dist.latest_version = remote_version 208 dist.latest_filetype = typ 209 yield dist 210 211 def output_package_listing(self, packages, options): 212 packages = sorted( 213 packages, 214 key=lambda dist: dist.project_name.lower(), 215 ) 216 if options.list_format == 'columns' and packages: 217 data, header = format_for_columns(packages, options) 218 self.output_package_listing_columns(data, header) 219 elif options.list_format == 'freeze': 220 for dist in packages: 221 if options.verbose >= 1: 222 write_output("%s==%s (%s)", dist.project_name, 223 dist.version, dist.location) 224 else: 225 write_output("%s==%s", dist.project_name, dist.version) 226 elif options.list_format == 'json': 227 write_output(format_for_json(packages, options)) 228 229 def output_package_listing_columns(self, data, header): 230 # insert the header first: we need to know the size of column names 231 if len(data) > 0: 232 data.insert(0, header) 233 234 pkg_strings, sizes = tabulate(data) 235 236 # Create and add a separator. 237 if len(data) > 0: 238 pkg_strings.insert(1, " ".join(map(lambda x: '-' * x, sizes))) 239 240 for val in pkg_strings: 241 write_output(val) 242 243 244def tabulate(vals): 245 # From pfmoore on GitHub: 246 # https://github.com/pypa/pip/issues/3651#issuecomment-216932564 247 assert len(vals) > 0 248 249 sizes = [0] * max(len(x) for x in vals) 250 for row in vals: 251 sizes = [max(s, len(str(c))) for s, c in zip_longest(sizes, row)] 252 253 result = [] 254 for row in vals: 255 display = " ".join([str(c).ljust(s) if c is not None else '' 256 for s, c in zip_longest(sizes, row)]) 257 result.append(display) 258 259 return result, sizes 260 261 262def format_for_columns(pkgs, options): 263 """ 264 Convert the package data into something usable 265 by output_package_listing_columns. 266 """ 267 running_outdated = options.outdated 268 # Adjust the header for the `pip list --outdated` case. 269 if running_outdated: 270 header = ["Package", "Version", "Latest", "Type"] 271 else: 272 header = ["Package", "Version"] 273 274 data = [] 275 if options.verbose >= 1 or any(dist_is_editable(x) for x in pkgs): 276 header.append("Location") 277 if options.verbose >= 1: 278 header.append("Installer") 279 280 for proj in pkgs: 281 # if we're working on the 'outdated' list, separate out the 282 # latest_version and type 283 row = [proj.project_name, proj.version] 284 285 if running_outdated: 286 row.append(proj.latest_version) 287 row.append(proj.latest_filetype) 288 289 if options.verbose >= 1 or dist_is_editable(proj): 290 row.append(proj.location) 291 if options.verbose >= 1: 292 row.append(get_installer(proj)) 293 294 data.append(row) 295 296 return data, header 297 298 299def format_for_json(packages, options): 300 data = [] 301 for dist in packages: 302 info = { 303 'name': dist.project_name, 304 'version': six.text_type(dist.version), 305 } 306 if options.verbose >= 1: 307 info['location'] = dist.location 308 info['installer'] = get_installer(dist) 309 if options.outdated: 310 info['latest_version'] = six.text_type(dist.latest_version) 311 info['latest_filetype'] = dist.latest_filetype 312 data.append(info) 313 return json.dumps(data) 314