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