diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 24e855a80..713ada507 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -1,14 +1,14 @@ +import csv import logging import os -from email.parser import FeedParser from optparse import Values -from typing import Dict, Iterator, List +from typing import Iterator, List, NamedTuple, Optional -from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS +from pip._internal.metadata import BaseDistribution, get_default_environment from pip._internal.utils.misc import write_output logger = logging.getLogger(__name__) @@ -50,18 +50,37 @@ class ShowCommand(Command): return SUCCESS -def search_packages_info(query): - # type: (List[str]) -> Iterator[Dict[str, str]] +class _PackageInfo(NamedTuple): + name: str + version: str + location: str + requires: List[str] + required_by: List[str] + installer: str + metadata_version: str + classifiers: List[str] + summary: str + homepage: str + author: str + author_email: str + license: str + entry_points: List[str] + files: Optional[List[str]] + + +def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]: """ Gather details from installed distributions. Print distribution name, version, location, and installed files. Installed files requires a pip generated 'installed-files.txt' in the distributions '.egg-info' directory. """ - installed = {} - for p in pkg_resources.working_set: - installed[canonicalize_name(p.project_name)] = p + env = get_default_environment() + installed = { + dist.canonical_name: dist + for dist in env.iter_distributions() + } query_names = [canonicalize_name(name) for name in query] missing = sorted( [name for name, pkg in zip(query, query_names) if pkg not in installed] @@ -69,79 +88,73 @@ def search_packages_info(query): if missing: logger.warning('Package(s) not found: %s', ', '.join(missing)) - def get_requiring_packages(package_name): - # type: (str) -> List[str] - canonical_name = canonicalize_name(package_name) + def _get_requiring_packages(current_dist: BaseDistribution) -> List[str]: return [ - pkg.project_name for pkg in pkg_resources.working_set - if canonical_name in - [canonicalize_name(required.name) for required in - pkg.requires()] + dist.canonical_name + for dist in env.iter_distributions() + if current_dist.canonical_name in { + canonicalize_name(d.name) for d in dist.iter_dependencies() + } ] - for dist in [installed[pkg] for pkg in query_names if pkg in installed]: - package = { - 'name': dist.project_name, - 'version': dist.version, - 'location': dist.location, - 'requires': [dep.project_name for dep in dist.requires()], - 'required_by': get_requiring_packages(dist.project_name) - } - file_list = None - metadata = '' - if isinstance(dist, pkg_resources.DistInfoDistribution): - # RECORDs should be part of .dist-info metadatas - if dist.has_metadata('RECORD'): - lines = dist.get_metadata_lines('RECORD') - paths = [line.split(',')[0] for line in lines] - paths = [os.path.join(dist.location, p) for p in paths] - file_list = [os.path.relpath(p, dist.location) for p in paths] + def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]: + try: + text = dist.read_text('RECORD') + except FileNotFoundError: + return None + return (row[0] for row in csv.reader(text.splitlines())) - if dist.has_metadata('METADATA'): - metadata = dist.get_metadata('METADATA') + def _files_from_installed_files(dist: BaseDistribution) -> Optional[Iterator[str]]: + try: + text = dist.read_text('installed-files.txt') + except FileNotFoundError: + return None + return (p for p in text.splitlines(keepends=False) if p) + + for query_name in query_names: + try: + dist = installed[query_name] + except KeyError: + continue + + try: + entry_points_text = dist.read_text('entry_points.txt') + entry_points = entry_points_text.splitlines(keepends=False) + except FileNotFoundError: + entry_points = [] + + files_iter = _files_from_record(dist) or _files_from_installed_files(dist) + if files_iter is None: + files: Optional[List[str]] = None else: - # Otherwise use pip's log for .egg-info's - if dist.has_metadata('installed-files.txt'): - paths = dist.get_metadata_lines('installed-files.txt') - paths = [os.path.join(dist.egg_info, p) for p in paths] - file_list = [os.path.relpath(p, dist.location) for p in paths] + files = sorted(os.path.relpath(p, dist.location) for p in files_iter) - if dist.has_metadata('PKG-INFO'): - metadata = dist.get_metadata('PKG-INFO') + metadata = dist.metadata - if dist.has_metadata('entry_points.txt'): - entry_points = dist.get_metadata_lines('entry_points.txt') - package['entry_points'] = entry_points - - if dist.has_metadata('INSTALLER'): - for line in dist.get_metadata_lines('INSTALLER'): - if line.strip(): - package['installer'] = line.strip() - break - - # @todo: Should pkg_resources.Distribution have a - # `get_pkg_info` method? - feed_parser = FeedParser() - feed_parser.feed(metadata) - pkg_info_dict = feed_parser.close() - for key in ('metadata-version', 'summary', - 'home-page', 'author', 'author-email', 'license'): - package[key] = pkg_info_dict.get(key) - - # It looks like FeedParser cannot deal with repeated headers - classifiers = [] - for line in metadata.splitlines(): - if line.startswith('Classifier: '): - classifiers.append(line[len('Classifier: '):]) - package['classifiers'] = classifiers - - if file_list: - package['files'] = sorted(file_list) - yield package + yield _PackageInfo( + name=dist.canonical_name, + version=str(dist.version), + location=dist.location or "", + requires=[req.name for req in dist.iter_dependencies()], + required_by=_get_requiring_packages(dist), + installer=dist.installer, + metadata_version=dist.metadata_version or "", + classifiers=metadata.get_all("Classifier", []), + summary=metadata.get("Summary", ""), + homepage=metadata.get("Home-page", ""), + author=metadata.get("Author", ""), + author_email=metadata.get("Author-email", ""), + license=metadata.get("License", ""), + entry_points=entry_points, + files=files, + ) -def print_results(distributions, list_files=False, verbose=False): - # type: (Iterator[Dict[str, str]], bool, bool) -> bool +def print_results( + distributions: Iterator[_PackageInfo], + list_files: bool, + verbose: bool, +) -> bool: """ Print the information from installed distributions found. """ @@ -151,31 +164,31 @@ def print_results(distributions, list_files=False, verbose=False): if i > 0: write_output("---") - write_output("Name: %s", dist.get('name', '')) - write_output("Version: %s", dist.get('version', '')) - write_output("Summary: %s", dist.get('summary', '')) - write_output("Home-page: %s", dist.get('home-page', '')) - write_output("Author: %s", dist.get('author', '')) - write_output("Author-email: %s", dist.get('author-email', '')) - write_output("License: %s", dist.get('license', '')) - write_output("Location: %s", dist.get('location', '')) - write_output("Requires: %s", ', '.join(dist.get('requires', []))) - write_output("Required-by: %s", ', '.join(dist.get('required_by', []))) + write_output("Name: %s", dist.name) + write_output("Version: %s", dist.version) + write_output("Summary: %s", dist.summary) + write_output("Home-page: %s", dist.homepage) + write_output("Author: %s", dist.author) + write_output("Author-email: %s", dist.author_email) + write_output("License: %s", dist.license) + write_output("Location: %s", dist.location) + write_output("Requires: %s", ', '.join(dist.requires)) + write_output("Required-by: %s", ', '.join(dist.required_by)) if verbose: - write_output("Metadata-Version: %s", - dist.get('metadata-version', '')) - write_output("Installer: %s", dist.get('installer', '')) + write_output("Metadata-Version: %s", dist.metadata_version) + write_output("Installer: %s", dist.installer) write_output("Classifiers:") - for classifier in dist.get('classifiers', []): + for classifier in dist.classifiers: write_output(" %s", classifier) write_output("Entry-points:") - for entry in dist.get('entry_points', []): + for entry in dist.entry_points: write_output(" %s", entry.strip()) if list_files: write_output("Files:") - for line in dist.get('files', []): - write_output(" %s", line.strip()) - if "files" not in dist: - write_output("Cannot locate installed-files.txt") + if dist.files is None: + write_output("Cannot locate RECORD or installed-files.txt") + else: + for line in dist.files: + write_output(" %s", line.strip()) return results_printed diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index bab64fa37..179743479 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -68,6 +68,8 @@ class Distribution(BaseDistribution): return misc.dist_in_usersite(self._dist) def read_text(self, name: str) -> str: + if not self._dist.has_metadata(name): + raise FileNotFoundError(name) return self._dist.get_metadata(name) def iter_entry_points(self) -> Iterable[BaseEntryPoint]: diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index 7047aa63a..d3e99353c 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -115,7 +115,7 @@ def test_search_any_case(): """ result = list(search_packages_info(['PIP'])) assert len(result) == 1 - assert result[0]['name'] == 'pip' + assert result[0].name == 'pip' def test_more_than_one_package():