| import csv |
| import logging |
| import pathlib |
| from optparse import Values |
| from typing import Iterator, List, NamedTuple, Optional, Tuple |
| |
| 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__) |
| |
| |
| class ShowCommand(Command): |
| """ |
| Show information about one or more installed packages. |
| |
| The output is in RFC-compliant mail header format. |
| """ |
| |
| usage = """ |
| %prog [options] <package> ...""" |
| ignore_require_venv = True |
| |
| def add_options(self) -> None: |
| self.cmd_opts.add_option( |
| "-f", |
| "--files", |
| dest="files", |
| action="store_true", |
| default=False, |
| help="Show the full list of installed files for each package.", |
| ) |
| |
| self.parser.insert_option_group(0, self.cmd_opts) |
| |
| def run(self, options: Values, args: List[str]) -> int: |
| if not args: |
| logger.warning("ERROR: Please provide a package name or names.") |
| return ERROR |
| query = args |
| |
| results = search_packages_info(query) |
| if not print_results( |
| results, list_files=options.files, verbose=options.verbose |
| ): |
| return ERROR |
| return SUCCESS |
| |
| |
| 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 _convert_legacy_entry(entry: Tuple[str, ...], info: Tuple[str, ...]) -> str: |
| """Convert a legacy installed-files.txt path into modern RECORD path. |
| |
| The legacy format stores paths relative to the info directory, while the |
| modern format stores paths relative to the package root, e.g. the |
| site-packages directory. |
| |
| :param entry: Path parts of the installed-files.txt entry. |
| :param info: Path parts of the egg-info directory relative to package root. |
| :returns: The converted entry. |
| |
| For best compatibility with symlinks, this does not use ``abspath()`` or |
| ``Path.resolve()``, but tries to work with path parts: |
| |
| 1. While ``entry`` starts with ``..``, remove the equal amounts of parts |
| from ``info``; if ``info`` is empty, start appending ``..`` instead. |
| 2. Join the two directly. |
| """ |
| while entry and entry[0] == "..": |
| if not info or info[-1] == "..": |
| info += ("..",) |
| else: |
| info = info[:-1] |
| entry = entry[1:] |
| return str(pathlib.Path(*info, *entry)) |
| |
| |
| 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. |
| """ |
| 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] |
| ) |
| if missing: |
| logger.warning("Package(s) not found: %s", ", ".join(missing)) |
| |
| def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[str]: |
| return ( |
| dist.metadata["Name"] or "UNKNOWN" |
| for dist in installed.values() |
| if current_dist.canonical_name |
| in {canonicalize_name(d.name) for d in dist.iter_dependencies()} |
| ) |
| |
| def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]: |
| try: |
| text = dist.read_text("RECORD") |
| except FileNotFoundError: |
| return None |
| # This extra Path-str cast normalizes entries. |
| return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines())) |
| |
| def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]: |
| try: |
| text = dist.read_text("installed-files.txt") |
| except FileNotFoundError: |
| return None |
| paths = (p for p in text.splitlines(keepends=False) if p) |
| root = dist.location |
| info = dist.info_directory |
| if root is None or info is None: |
| return paths |
| try: |
| info_rel = pathlib.Path(info).relative_to(root) |
| except ValueError: # info is not relative to root. |
| return paths |
| if not info_rel.parts: # info *is* root. |
| return paths |
| return ( |
| _convert_legacy_entry(pathlib.Path(p).parts, info_rel.parts) for p in paths |
| ) |
| |
| for query_name in query_names: |
| try: |
| dist = installed[query_name] |
| except KeyError: |
| continue |
| |
| requires = sorted((req.name for req in dist.iter_dependencies()), key=str.lower) |
| required_by = sorted(_get_requiring_packages(dist), key=str.lower) |
| |
| 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_legacy(dist) |
| if files_iter is None: |
| files: Optional[List[str]] = None |
| else: |
| files = sorted(files_iter) |
| |
| metadata = dist.metadata |
| |
| yield _PackageInfo( |
| name=dist.raw_name, |
| version=str(dist.version), |
| location=dist.location or "", |
| requires=requires, |
| required_by=required_by, |
| 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: Iterator[_PackageInfo], |
| list_files: bool, |
| verbose: bool, |
| ) -> bool: |
| """ |
| Print the information from installed distributions found. |
| """ |
| results_printed = False |
| for i, dist in enumerate(distributions): |
| results_printed = True |
| if i > 0: |
| write_output("---") |
| |
| 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.metadata_version) |
| write_output("Installer: %s", dist.installer) |
| write_output("Classifiers:") |
| for classifier in dist.classifiers: |
| write_output(" %s", classifier) |
| write_output("Entry-points:") |
| for entry in dist.entry_points: |
| write_output(" %s", entry.strip()) |
| if list_files: |
| write_output("Files:") |
| 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 |