freeze.py 9.8 KB

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