You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
7.8 KiB
234 lines
7.8 KiB
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 _covert_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) -> List[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 ( |
|
_covert_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 |
|
|
|
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=[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: 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
|
|
|