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.

req_set.py 7.9KB

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