图片解析应用
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.

255 lines
9.6 KiB

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