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