freeze.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. from __future__ import absolute_import
  2. import collections
  3. import logging
  4. import os
  5. import re
  6. import warnings
  7. from pip._vendor import pkg_resources, six
  8. from pip._vendor.packaging.utils import canonicalize_name
  9. from pip._vendor.pkg_resources import RequirementParseError
  10. from pip._internal.exceptions import InstallationError
  11. from pip._internal.req import InstallRequirement
  12. from pip._internal.req.req_file import COMMENT_RE
  13. from pip._internal.utils.deprecation import RemovedInPip11Warning
  14. from pip._internal.utils.misc import (
  15. dist_is_editable, get_installed_distributions,
  16. )
  17. logger = logging.getLogger(__name__)
  18. def freeze(
  19. requirement=None,
  20. find_links=None, local_only=None, user_only=None, skip_regex=None,
  21. isolated=False,
  22. wheel_cache=None,
  23. exclude_editable=False,
  24. skip=()):
  25. find_links = find_links or []
  26. skip_match = None
  27. if skip_regex:
  28. skip_match = re.compile(skip_regex).search
  29. dependency_links = []
  30. for dist in pkg_resources.working_set:
  31. if dist.has_metadata('dependency_links.txt'):
  32. dependency_links.extend(
  33. dist.get_metadata_lines('dependency_links.txt')
  34. )
  35. for link in find_links:
  36. if '#egg=' in link:
  37. dependency_links.append(link)
  38. for link in find_links:
  39. yield '-f %s' % link
  40. installations = {}
  41. for dist in get_installed_distributions(local_only=local_only,
  42. skip=(),
  43. user_only=user_only):
  44. try:
  45. req = FrozenRequirement.from_dist(
  46. dist,
  47. dependency_links
  48. )
  49. except RequirementParseError:
  50. logger.warning(
  51. "Could not parse requirement: %s",
  52. dist.project_name
  53. )
  54. continue
  55. if exclude_editable and req.editable:
  56. continue
  57. installations[req.name] = req
  58. if requirement:
  59. # the options that don't get turned into an InstallRequirement
  60. # should only be emitted once, even if the same option is in multiple
  61. # requirements files, so we need to keep track of what has been emitted
  62. # so that we don't emit it again if it's seen again
  63. emitted_options = set()
  64. # keep track of which files a requirement is in so that we can
  65. # give an accurate warning if a requirement appears multiple times.
  66. req_files = collections.defaultdict(list)
  67. for req_file_path in requirement:
  68. with open(req_file_path) as req_file:
  69. for line in req_file:
  70. if (not line.strip() or
  71. line.strip().startswith('#') or
  72. (skip_match and skip_match(line)) or
  73. line.startswith((
  74. '-r', '--requirement',
  75. '-Z', '--always-unzip',
  76. '-f', '--find-links',
  77. '-i', '--index-url',
  78. '--pre',
  79. '--trusted-host',
  80. '--process-dependency-links',
  81. '--extra-index-url'))):
  82. line = line.rstrip()
  83. if line not in emitted_options:
  84. emitted_options.add(line)
  85. yield line
  86. continue
  87. if line.startswith('-e') or line.startswith('--editable'):
  88. if line.startswith('-e'):
  89. line = line[2:].strip()
  90. else:
  91. line = line[len('--editable'):].strip().lstrip('=')
  92. line_req = InstallRequirement.from_editable(
  93. line,
  94. isolated=isolated,
  95. wheel_cache=wheel_cache,
  96. )
  97. else:
  98. line_req = InstallRequirement.from_line(
  99. COMMENT_RE.sub('', line).strip(),
  100. isolated=isolated,
  101. wheel_cache=wheel_cache,
  102. )
  103. if not line_req.name:
  104. logger.info(
  105. "Skipping line in requirement file [%s] because "
  106. "it's not clear what it would install: %s",
  107. req_file_path, line.strip(),
  108. )
  109. logger.info(
  110. " (add #egg=PackageName to the URL to avoid"
  111. " this warning)"
  112. )
  113. elif line_req.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 that "
  119. "package is not installed",
  120. req_file_path,
  121. COMMENT_RE.sub('', line).strip(),
  122. )
  123. else:
  124. req_files[line_req.name].append(req_file_path)
  125. else:
  126. yield str(installations[line_req.name]).rstrip()
  127. del installations[line_req.name]
  128. req_files[line_req.name].append(req_file_path)
  129. # Warn about requirements that were included multiple times (in a
  130. # single requirements file or in different requirements files).
  131. for name, files in six.iteritems(req_files):
  132. if len(files) > 1:
  133. logger.warning("Requirement %s included multiple times [%s]",
  134. name, ', '.join(sorted(set(files))))
  135. yield(
  136. '## The following requirements were added by '
  137. 'pip freeze:'
  138. )
  139. for installation in sorted(
  140. installations.values(), key=lambda x: x.name.lower()):
  141. if canonicalize_name(installation.name) not in skip:
  142. yield str(installation).rstrip()
  143. class FrozenRequirement(object):
  144. def __init__(self, name, req, editable, comments=()):
  145. self.name = name
  146. self.req = req
  147. self.editable = editable
  148. self.comments = comments
  149. _rev_re = re.compile(r'-r(\d+)$')
  150. _date_re = re.compile(r'-(20\d\d\d\d\d\d)$')
  151. @classmethod
  152. def from_dist(cls, dist, dependency_links):
  153. location = os.path.normcase(os.path.abspath(dist.location))
  154. comments = []
  155. from pip._internal.vcs import vcs, get_src_requirement
  156. if dist_is_editable(dist) and vcs.get_backend_name(location):
  157. editable = True
  158. try:
  159. req = get_src_requirement(dist, location)
  160. except InstallationError as exc:
  161. logger.warning(
  162. "Error when trying to get requirement for VCS system %s, "
  163. "falling back to uneditable format", exc
  164. )
  165. req = None
  166. if req is None:
  167. logger.warning(
  168. 'Could not determine repository location of %s', location
  169. )
  170. comments.append(
  171. '## !! Could not determine repository location'
  172. )
  173. req = dist.as_requirement()
  174. editable = False
  175. else:
  176. editable = False
  177. req = dist.as_requirement()
  178. specs = req.specs
  179. assert len(specs) == 1 and specs[0][0] in ["==", "==="], \
  180. 'Expected 1 spec with == or ===; specs = %r; dist = %r' % \
  181. (specs, dist)
  182. version = specs[0][1]
  183. ver_match = cls._rev_re.search(version)
  184. date_match = cls._date_re.search(version)
  185. if ver_match or date_match:
  186. svn_backend = vcs.get_backend('svn')
  187. if svn_backend:
  188. svn_location = svn_backend().get_location(
  189. dist,
  190. dependency_links,
  191. )
  192. if not svn_location:
  193. logger.warning(
  194. 'Warning: cannot find svn location for %s', req,
  195. )
  196. comments.append(
  197. '## FIXME: could not find svn URL in dependency_links '
  198. 'for this package:'
  199. )
  200. else:
  201. warnings.warn(
  202. "SVN editable detection based on dependency links "
  203. "will be dropped in the future.",
  204. RemovedInPip11Warning,
  205. )
  206. comments.append(
  207. '# Installing as editable to satisfy requirement %s:' %
  208. req
  209. )
  210. if ver_match:
  211. rev = ver_match.group(1)
  212. else:
  213. rev = '{%s}' % date_match.group(1)
  214. editable = True
  215. req = '%s@%s#egg=%s' % (
  216. svn_location,
  217. rev,
  218. cls.egg_name(dist)
  219. )
  220. return cls(dist.project_name, req, editable, comments)
  221. @staticmethod
  222. def egg_name(dist):
  223. name = dist.egg_name()
  224. match = re.search(r'-py\d\.\d$', name)
  225. if match:
  226. name = name[:match.start()]
  227. return name
  228. def __str__(self):
  229. req = self.req
  230. if self.editable:
  231. req = '-e %s' % req
  232. return '\n'.join(list(self.comments) + [str(req)]) + '\n'