req_set.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. # The following comment should be removed at some point in the future.
  2. # mypy: strict-optional=False
  3. from __future__ import absolute_import
  4. import logging
  5. from collections import OrderedDict
  6. from pip._vendor.packaging.utils import canonicalize_name
  7. from pip._internal import pep425tags
  8. from pip._internal.exceptions import InstallationError
  9. from pip._internal.utils.logging import indent_log
  10. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  11. from pip._internal.wheel import Wheel
  12. if MYPY_CHECK_RUNNING:
  13. from typing import Dict, Iterable, List, Optional, Tuple
  14. from pip._internal.req.req_install import InstallRequirement
  15. logger = logging.getLogger(__name__)
  16. class RequirementSet(object):
  17. def __init__(self, require_hashes=False, check_supported_wheels=True):
  18. # type: (bool, bool) -> None
  19. """Create a RequirementSet.
  20. """
  21. self.requirements = OrderedDict() # type: Dict[str, InstallRequirement] # noqa: E501
  22. self.require_hashes = require_hashes
  23. self.check_supported_wheels = check_supported_wheels
  24. self.unnamed_requirements = [] # type: List[InstallRequirement]
  25. self.successfully_downloaded = [] # type: List[InstallRequirement]
  26. self.reqs_to_cleanup = [] # type: List[InstallRequirement]
  27. def __str__(self):
  28. # type: () -> str
  29. requirements = sorted(
  30. (req for req in self.requirements.values() if not req.comes_from),
  31. key=lambda req: canonicalize_name(req.name),
  32. )
  33. return ' '.join(str(req.req) for req in requirements)
  34. def __repr__(self):
  35. # type: () -> str
  36. requirements = sorted(
  37. self.requirements.values(),
  38. key=lambda req: canonicalize_name(req.name),
  39. )
  40. format_string = '<{classname} object; {count} requirement(s): {reqs}>'
  41. return format_string.format(
  42. classname=self.__class__.__name__,
  43. count=len(requirements),
  44. reqs=', '.join(str(req.req) for req in requirements),
  45. )
  46. def add_unnamed_requirement(self, install_req):
  47. # type: (InstallRequirement) -> None
  48. assert not install_req.name
  49. self.unnamed_requirements.append(install_req)
  50. def add_named_requirement(self, install_req):
  51. # type: (InstallRequirement) -> None
  52. assert install_req.name
  53. project_name = canonicalize_name(install_req.name)
  54. self.requirements[project_name] = install_req
  55. def add_requirement(
  56. self,
  57. install_req, # type: InstallRequirement
  58. parent_req_name=None, # type: Optional[str]
  59. extras_requested=None # type: Optional[Iterable[str]]
  60. ):
  61. # type: (...) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]] # noqa: E501
  62. """Add install_req as a requirement to install.
  63. :param parent_req_name: The name of the requirement that needed this
  64. added. The name is used because when multiple unnamed requirements
  65. resolve to the same name, we could otherwise end up with dependency
  66. links that point outside the Requirements set. parent_req must
  67. already be added. Note that None implies that this is a user
  68. supplied requirement, vs an inferred one.
  69. :param extras_requested: an iterable of extras used to evaluate the
  70. environment markers.
  71. :return: Additional requirements to scan. That is either [] if
  72. the requirement is not applicable, or [install_req] if the
  73. requirement is applicable and has just been added.
  74. """
  75. # If the markers do not match, ignore this requirement.
  76. if not install_req.match_markers(extras_requested):
  77. logger.info(
  78. "Ignoring %s: markers '%s' don't match your environment",
  79. install_req.name, install_req.markers,
  80. )
  81. return [], None
  82. # If the wheel is not supported, raise an error.
  83. # Should check this after filtering out based on environment markers to
  84. # allow specifying different wheels based on the environment/OS, in a
  85. # single requirements file.
  86. if install_req.link and install_req.link.is_wheel:
  87. wheel = Wheel(install_req.link.filename)
  88. tags = pep425tags.get_supported()
  89. if (self.check_supported_wheels and not wheel.supported(tags)):
  90. raise InstallationError(
  91. "%s is not a supported wheel on this platform." %
  92. wheel.filename
  93. )
  94. # This next bit is really a sanity check.
  95. assert install_req.is_direct == (parent_req_name is None), (
  96. "a direct req shouldn't have a parent and also, "
  97. "a non direct req should have a parent"
  98. )
  99. # Unnamed requirements are scanned again and the requirement won't be
  100. # added as a dependency until after scanning.
  101. if not install_req.name:
  102. self.add_unnamed_requirement(install_req)
  103. return [install_req], None
  104. try:
  105. existing_req = self.get_requirement(install_req.name)
  106. except KeyError:
  107. existing_req = None
  108. has_conflicting_requirement = (
  109. parent_req_name is None and
  110. existing_req and
  111. not existing_req.constraint and
  112. existing_req.extras == install_req.extras and
  113. existing_req.req.specifier != install_req.req.specifier
  114. )
  115. if has_conflicting_requirement:
  116. raise InstallationError(
  117. "Double requirement given: %s (already in %s, name=%r)"
  118. % (install_req, existing_req, install_req.name)
  119. )
  120. # When no existing requirement exists, add the requirement as a
  121. # dependency and it will be scanned again after.
  122. if not existing_req:
  123. self.add_named_requirement(install_req)
  124. # We'd want to rescan this requirement later
  125. return [install_req], install_req
  126. # Assume there's no need to scan, and that we've already
  127. # encountered this for scanning.
  128. if install_req.constraint or not existing_req.constraint:
  129. return [], existing_req
  130. does_not_satisfy_constraint = (
  131. install_req.link and
  132. not (
  133. existing_req.link and
  134. install_req.link.path == existing_req.link.path
  135. )
  136. )
  137. if does_not_satisfy_constraint:
  138. self.reqs_to_cleanup.append(install_req)
  139. raise InstallationError(
  140. "Could not satisfy constraints for '%s': "
  141. "installation from path or url cannot be "
  142. "constrained to a version" % install_req.name,
  143. )
  144. # If we're now installing a constraint, mark the existing
  145. # object for real installation.
  146. existing_req.constraint = False
  147. existing_req.extras = tuple(sorted(
  148. set(existing_req.extras) | set(install_req.extras)
  149. ))
  150. logger.debug(
  151. "Setting %s extras to: %s",
  152. existing_req, existing_req.extras,
  153. )
  154. # Return the existing requirement for addition to the parent and
  155. # scanning again.
  156. return [existing_req], existing_req
  157. def has_requirement(self, name):
  158. # type: (str) -> bool
  159. project_name = canonicalize_name(name)
  160. return (
  161. project_name in self.requirements and
  162. not self.requirements[project_name].constraint
  163. )
  164. def get_requirement(self, name):
  165. # type: (str) -> InstallRequirement
  166. project_name = canonicalize_name(name)
  167. if project_name in self.requirements:
  168. return self.requirements[project_name]
  169. raise KeyError("No project with the name %r" % name)
  170. def cleanup_files(self):
  171. # type: () -> None
  172. """Clean up files, remove builds."""
  173. logger.debug('Cleaning up...')
  174. with indent_log():
  175. for req in self.reqs_to_cleanup:
  176. req.remove_temporary_source()