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