|
import collections |
|
import logging |
|
import os |
|
from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set |
|
|
|
from pip._vendor.packaging.utils import canonicalize_name |
|
from pip._vendor.packaging.version import Version |
|
|
|
from pip._internal.exceptions import BadCommand, InstallationError |
|
from pip._internal.metadata import BaseDistribution, get_environment |
|
from pip._internal.req.constructors import ( |
|
install_req_from_editable, |
|
install_req_from_line, |
|
) |
|
from pip._internal.req.req_file import COMMENT_RE |
|
from pip._internal.utils.direct_url_helpers import direct_url_as_pep440_direct_reference |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
class _EditableInfo(NamedTuple): |
|
requirement: str |
|
comments: List[str] |
|
|
|
|
|
def freeze( |
|
requirement: Optional[List[str]] = None, |
|
local_only: bool = False, |
|
user_only: bool = False, |
|
paths: Optional[List[str]] = None, |
|
isolated: bool = False, |
|
exclude_editable: bool = False, |
|
skip: Container[str] = (), |
|
) -> Generator[str, None, None]: |
|
installations: Dict[str, FrozenRequirement] = {} |
|
|
|
dists = get_environment(paths).iter_installed_distributions( |
|
local_only=local_only, |
|
skip=(), |
|
user_only=user_only, |
|
) |
|
for dist in dists: |
|
req = FrozenRequirement.from_dist(dist) |
|
if exclude_editable and req.editable: |
|
continue |
|
installations[req.canonical_name] = req |
|
|
|
if requirement: |
|
|
|
|
|
|
|
|
|
emitted_options: Set[str] = set() |
|
|
|
|
|
req_files: Dict[str, List[str]] = collections.defaultdict(list) |
|
for req_file_path in requirement: |
|
with open(req_file_path) as req_file: |
|
for line in req_file: |
|
if ( |
|
not line.strip() |
|
or line.strip().startswith("#") |
|
or line.startswith( |
|
( |
|
"-r", |
|
"--requirement", |
|
"-f", |
|
"--find-links", |
|
"-i", |
|
"--index-url", |
|
"--pre", |
|
"--trusted-host", |
|
"--process-dependency-links", |
|
"--extra-index-url", |
|
"--use-feature", |
|
) |
|
) |
|
): |
|
line = line.rstrip() |
|
if line not in emitted_options: |
|
emitted_options.add(line) |
|
yield line |
|
continue |
|
|
|
if line.startswith("-e") or line.startswith("--editable"): |
|
if line.startswith("-e"): |
|
line = line[2:].strip() |
|
else: |
|
line = line[len("--editable") :].strip().lstrip("=") |
|
line_req = install_req_from_editable( |
|
line, |
|
isolated=isolated, |
|
) |
|
else: |
|
line_req = install_req_from_line( |
|
COMMENT_RE.sub("", line).strip(), |
|
isolated=isolated, |
|
) |
|
|
|
if not line_req.name: |
|
logger.info( |
|
"Skipping line in requirement file [%s] because " |
|
"it's not clear what it would install: %s", |
|
req_file_path, |
|
line.strip(), |
|
) |
|
logger.info( |
|
" (add #egg=PackageName to the URL to avoid" |
|
" this warning)" |
|
) |
|
else: |
|
line_req_canonical_name = canonicalize_name(line_req.name) |
|
if line_req_canonical_name not in installations: |
|
|
|
|
|
if not req_files[line_req.name]: |
|
logger.warning( |
|
"Requirement file [%s] contains %s, but " |
|
"package %r is not installed", |
|
req_file_path, |
|
COMMENT_RE.sub("", line).strip(), |
|
line_req.name, |
|
) |
|
else: |
|
req_files[line_req.name].append(req_file_path) |
|
else: |
|
yield str(installations[line_req_canonical_name]).rstrip() |
|
del installations[line_req_canonical_name] |
|
req_files[line_req.name].append(req_file_path) |
|
|
|
|
|
|
|
for name, files in req_files.items(): |
|
if len(files) > 1: |
|
logger.warning( |
|
"Requirement %s included multiple times [%s]", |
|
name, |
|
", ".join(sorted(set(files))), |
|
) |
|
|
|
yield ("## The following requirements were added by pip freeze:") |
|
for installation in sorted(installations.values(), key=lambda x: x.name.lower()): |
|
if installation.canonical_name not in skip: |
|
yield str(installation).rstrip() |
|
|
|
|
|
def _format_as_name_version(dist: BaseDistribution) -> str: |
|
if isinstance(dist.version, Version): |
|
return f"{dist.raw_name}=={dist.version}" |
|
return f"{dist.raw_name}==={dist.version}" |
|
|
|
|
|
def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: |
|
""" |
|
Compute and return values (req, comments) for use in |
|
FrozenRequirement.from_dist(). |
|
""" |
|
editable_project_location = dist.editable_project_location |
|
assert editable_project_location |
|
location = os.path.normcase(os.path.abspath(editable_project_location)) |
|
|
|
from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs |
|
|
|
vcs_backend = vcs.get_backend_for_dir(location) |
|
|
|
if vcs_backend is None: |
|
display = _format_as_name_version(dist) |
|
logger.debug( |
|
'No VCS found for editable requirement "%s" in: %r', |
|
display, |
|
location, |
|
) |
|
return _EditableInfo( |
|
requirement=location, |
|
comments=[f"# Editable install with no version control ({display})"], |
|
) |
|
|
|
vcs_name = type(vcs_backend).__name__ |
|
|
|
try: |
|
req = vcs_backend.get_src_requirement(location, dist.raw_name) |
|
except RemoteNotFoundError: |
|
display = _format_as_name_version(dist) |
|
return _EditableInfo( |
|
requirement=location, |
|
comments=[f"# Editable {vcs_name} install with no remote ({display})"], |
|
) |
|
except RemoteNotValidError as ex: |
|
display = _format_as_name_version(dist) |
|
return _EditableInfo( |
|
requirement=location, |
|
comments=[ |
|
f"# Editable {vcs_name} install ({display}) with either a deleted " |
|
f"local remote or invalid URI:", |
|
f"# '{ex.url}'", |
|
], |
|
) |
|
except BadCommand: |
|
logger.warning( |
|
"cannot determine version of editable source in %s " |
|
"(%s command not found in path)", |
|
location, |
|
vcs_backend.name, |
|
) |
|
return _EditableInfo(requirement=location, comments=[]) |
|
except InstallationError as exc: |
|
logger.warning("Error when trying to get requirement for VCS system %s", exc) |
|
else: |
|
return _EditableInfo(requirement=req, comments=[]) |
|
|
|
logger.warning("Could not determine repository location of %s", location) |
|
|
|
return _EditableInfo( |
|
requirement=location, |
|
comments=["## !! Could not determine repository location"], |
|
) |
|
|
|
|
|
class FrozenRequirement: |
|
def __init__( |
|
self, |
|
name: str, |
|
req: str, |
|
editable: bool, |
|
comments: Iterable[str] = (), |
|
) -> None: |
|
self.name = name |
|
self.canonical_name = canonicalize_name(name) |
|
self.req = req |
|
self.editable = editable |
|
self.comments = comments |
|
|
|
@classmethod |
|
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement": |
|
editable = dist.editable |
|
if editable: |
|
req, comments = _get_editable_info(dist) |
|
else: |
|
comments = [] |
|
direct_url = dist.direct_url |
|
if direct_url: |
|
|
|
req = direct_url_as_pep440_direct_reference(direct_url, dist.raw_name) |
|
else: |
|
|
|
req = _format_as_name_version(dist) |
|
|
|
return cls(dist.raw_name, req, editable, comments=comments) |
|
|
|
def __str__(self) -> str: |
|
req = self.req |
|
if self.editable: |
|
req = f"-e {req}" |
|
return "\n".join(list(self.comments) + [str(req)]) + "\n" |
|
|