python (3.12.0)
1 import collections
2 import logging
3 import os
4 from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set
5
6 from pip._vendor.packaging.utils import canonicalize_name
7 from pip._vendor.packaging.version import Version
8
9 from pip._internal.exceptions import BadCommand, InstallationError
10 from pip._internal.metadata import BaseDistribution, get_environment
11 from pip._internal.req.constructors import (
12 install_req_from_editable,
13 install_req_from_line,
14 )
15 from pip._internal.req.req_file import COMMENT_RE
16 from pip._internal.utils.direct_url_helpers import direct_url_as_pep440_direct_reference
17
18 logger = logging.getLogger(__name__)
19
20
21 class ESC[4;38;5;81m_EditableInfo(ESC[4;38;5;149mNamedTuple):
22 requirement: str
23 comments: List[str]
24
25
26 def freeze(
27 requirement: Optional[List[str]] = None,
28 local_only: bool = False,
29 user_only: bool = False,
30 paths: Optional[List[str]] = None,
31 isolated: bool = False,
32 exclude_editable: bool = False,
33 skip: Container[str] = (),
34 ) -> Generator[str, None, None]:
35 installations: Dict[str, FrozenRequirement] = {}
36
37 dists = get_environment(paths).iter_installed_distributions(
38 local_only=local_only,
39 skip=(),
40 user_only=user_only,
41 )
42 for dist in dists:
43 req = FrozenRequirement.from_dist(dist)
44 if exclude_editable and req.editable:
45 continue
46 installations[req.canonical_name] = req
47
48 if requirement:
49 # the options that don't get turned into an InstallRequirement
50 # should only be emitted once, even if the same option is in multiple
51 # requirements files, so we need to keep track of what has been emitted
52 # so that we don't emit it again if it's seen again
53 emitted_options: Set[str] = set()
54 # keep track of which files a requirement is in so that we can
55 # give an accurate warning if a requirement appears multiple times.
56 req_files: Dict[str, List[str]] = collections.defaultdict(list)
57 for req_file_path in requirement:
58 with open(req_file_path) as req_file:
59 for line in req_file:
60 if (
61 not line.strip()
62 or line.strip().startswith("#")
63 or line.startswith(
64 (
65 "-r",
66 "--requirement",
67 "-f",
68 "--find-links",
69 "-i",
70 "--index-url",
71 "--pre",
72 "--trusted-host",
73 "--process-dependency-links",
74 "--extra-index-url",
75 "--use-feature",
76 )
77 )
78 ):
79 line = line.rstrip()
80 if line not in emitted_options:
81 emitted_options.add(line)
82 yield line
83 continue
84
85 if line.startswith("-e") or line.startswith("--editable"):
86 if line.startswith("-e"):
87 line = line[2:].strip()
88 else:
89 line = line[len("--editable") :].strip().lstrip("=")
90 line_req = install_req_from_editable(
91 line,
92 isolated=isolated,
93 )
94 else:
95 line_req = install_req_from_line(
96 COMMENT_RE.sub("", line).strip(),
97 isolated=isolated,
98 )
99
100 if not line_req.name:
101 logger.info(
102 "Skipping line in requirement file [%s] because "
103 "it's not clear what it would install: %s",
104 req_file_path,
105 line.strip(),
106 )
107 logger.info(
108 " (add #egg=PackageName to the URL to avoid"
109 " this warning)"
110 )
111 else:
112 line_req_canonical_name = canonicalize_name(line_req.name)
113 if line_req_canonical_name not in installations:
114 # either it's not installed, or it is installed
115 # but has been processed already
116 if not req_files[line_req.name]:
117 logger.warning(
118 "Requirement file [%s] contains %s, but "
119 "package %r is not installed",
120 req_file_path,
121 COMMENT_RE.sub("", line).strip(),
122 line_req.name,
123 )
124 else:
125 req_files[line_req.name].append(req_file_path)
126 else:
127 yield str(installations[line_req_canonical_name]).rstrip()
128 del installations[line_req_canonical_name]
129 req_files[line_req.name].append(req_file_path)
130
131 # Warn about requirements that were included multiple times (in a
132 # single requirements file or in different requirements files).
133 for name, files in req_files.items():
134 if len(files) > 1:
135 logger.warning(
136 "Requirement %s included multiple times [%s]",
137 name,
138 ", ".join(sorted(set(files))),
139 )
140
141 yield ("## The following requirements were added by pip freeze:")
142 for installation in sorted(installations.values(), key=lambda x: x.name.lower()):
143 if installation.canonical_name not in skip:
144 yield str(installation).rstrip()
145
146
147 def _format_as_name_version(dist: BaseDistribution) -> str:
148 dist_version = dist.version
149 if isinstance(dist_version, Version):
150 return f"{dist.raw_name}=={dist_version}"
151 return f"{dist.raw_name}==={dist_version}"
152
153
154 def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
155 """
156 Compute and return values (req, comments) for use in
157 FrozenRequirement.from_dist().
158 """
159 editable_project_location = dist.editable_project_location
160 assert editable_project_location
161 location = os.path.normcase(os.path.abspath(editable_project_location))
162
163 from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs
164
165 vcs_backend = vcs.get_backend_for_dir(location)
166
167 if vcs_backend is None:
168 display = _format_as_name_version(dist)
169 logger.debug(
170 'No VCS found for editable requirement "%s" in: %r',
171 display,
172 location,
173 )
174 return _EditableInfo(
175 requirement=location,
176 comments=[f"# Editable install with no version control ({display})"],
177 )
178
179 vcs_name = type(vcs_backend).__name__
180
181 try:
182 req = vcs_backend.get_src_requirement(location, dist.raw_name)
183 except RemoteNotFoundError:
184 display = _format_as_name_version(dist)
185 return _EditableInfo(
186 requirement=location,
187 comments=[f"# Editable {vcs_name} install with no remote ({display})"],
188 )
189 except RemoteNotValidError as ex:
190 display = _format_as_name_version(dist)
191 return _EditableInfo(
192 requirement=location,
193 comments=[
194 f"# Editable {vcs_name} install ({display}) with either a deleted "
195 f"local remote or invalid URI:",
196 f"# '{ex.url}'",
197 ],
198 )
199 except BadCommand:
200 logger.warning(
201 "cannot determine version of editable source in %s "
202 "(%s command not found in path)",
203 location,
204 vcs_backend.name,
205 )
206 return _EditableInfo(requirement=location, comments=[])
207 except InstallationError as exc:
208 logger.warning("Error when trying to get requirement for VCS system %s", exc)
209 else:
210 return _EditableInfo(requirement=req, comments=[])
211
212 logger.warning("Could not determine repository location of %s", location)
213
214 return _EditableInfo(
215 requirement=location,
216 comments=["## !! Could not determine repository location"],
217 )
218
219
220 class ESC[4;38;5;81mFrozenRequirement:
221 def __init__(
222 self,
223 name: str,
224 req: str,
225 editable: bool,
226 comments: Iterable[str] = (),
227 ) -> None:
228 self.name = name
229 self.canonical_name = canonicalize_name(name)
230 self.req = req
231 self.editable = editable
232 self.comments = comments
233
234 @classmethod
235 def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
236 editable = dist.editable
237 if editable:
238 req, comments = _get_editable_info(dist)
239 else:
240 comments = []
241 direct_url = dist.direct_url
242 if direct_url:
243 # if PEP 610 metadata is present, use it
244 req = direct_url_as_pep440_direct_reference(direct_url, dist.raw_name)
245 else:
246 # name==version requirement
247 req = _format_as_name_version(dist)
248
249 return cls(dist.raw_name, req, editable, comments=comments)
250
251 def __str__(self) -> str:
252 req = self.req
253 if self.editable:
254 req = f"-e {req}"
255 return "\n".join(list(self.comments) + [str(req)]) + "\n"