Mercurial > repos > guerler > hhblits
comparison lib/python3.8/site-packages/pip/_internal/legacy_resolve.py @ 0:9e54283cc701 draft
"planemo upload commit d12c32a45bcd441307e632fca6d9af7d60289d44"
author | guerler |
---|---|
date | Mon, 27 Jul 2020 03:47:31 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:9e54283cc701 |
---|---|
1 """Dependency Resolution | |
2 | |
3 The dependency resolution in pip is performed as follows: | |
4 | |
5 for top-level requirements: | |
6 a. only one spec allowed per project, regardless of conflicts or not. | |
7 otherwise a "double requirement" exception is raised | |
8 b. they override sub-dependency requirements. | |
9 for sub-dependencies | |
10 a. "first found, wins" (where the order is breadth first) | |
11 """ | |
12 | |
13 # The following comment should be removed at some point in the future. | |
14 # mypy: strict-optional=False | |
15 # mypy: disallow-untyped-defs=False | |
16 | |
17 import logging | |
18 import sys | |
19 from collections import defaultdict | |
20 from itertools import chain | |
21 | |
22 from pip._vendor.packaging import specifiers | |
23 | |
24 from pip._internal.exceptions import ( | |
25 BestVersionAlreadyInstalled, | |
26 DistributionNotFound, | |
27 HashError, | |
28 HashErrors, | |
29 UnsupportedPythonVersion, | |
30 ) | |
31 from pip._internal.utils.logging import indent_log | |
32 from pip._internal.utils.misc import dist_in_usersite, normalize_version_info | |
33 from pip._internal.utils.packaging import ( | |
34 check_requires_python, | |
35 get_requires_python, | |
36 ) | |
37 from pip._internal.utils.typing import MYPY_CHECK_RUNNING | |
38 | |
39 if MYPY_CHECK_RUNNING: | |
40 from typing import Callable, DefaultDict, List, Optional, Set, Tuple | |
41 from pip._vendor import pkg_resources | |
42 | |
43 from pip._internal.distributions import AbstractDistribution | |
44 from pip._internal.index.package_finder import PackageFinder | |
45 from pip._internal.operations.prepare import RequirementPreparer | |
46 from pip._internal.req.req_install import InstallRequirement | |
47 from pip._internal.req.req_set import RequirementSet | |
48 | |
49 InstallRequirementProvider = Callable[ | |
50 [str, InstallRequirement], InstallRequirement | |
51 ] | |
52 DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]] | |
53 | |
54 logger = logging.getLogger(__name__) | |
55 | |
56 | |
57 def _check_dist_requires_python( | |
58 dist, # type: pkg_resources.Distribution | |
59 version_info, # type: Tuple[int, int, int] | |
60 ignore_requires_python=False, # type: bool | |
61 ): | |
62 # type: (...) -> None | |
63 """ | |
64 Check whether the given Python version is compatible with a distribution's | |
65 "Requires-Python" value. | |
66 | |
67 :param version_info: A 3-tuple of ints representing the Python | |
68 major-minor-micro version to check. | |
69 :param ignore_requires_python: Whether to ignore the "Requires-Python" | |
70 value if the given Python version isn't compatible. | |
71 | |
72 :raises UnsupportedPythonVersion: When the given Python version isn't | |
73 compatible. | |
74 """ | |
75 requires_python = get_requires_python(dist) | |
76 try: | |
77 is_compatible = check_requires_python( | |
78 requires_python, version_info=version_info, | |
79 ) | |
80 except specifiers.InvalidSpecifier as exc: | |
81 logger.warning( | |
82 "Package %r has an invalid Requires-Python: %s", | |
83 dist.project_name, exc, | |
84 ) | |
85 return | |
86 | |
87 if is_compatible: | |
88 return | |
89 | |
90 version = '.'.join(map(str, version_info)) | |
91 if ignore_requires_python: | |
92 logger.debug( | |
93 'Ignoring failed Requires-Python check for package %r: ' | |
94 '%s not in %r', | |
95 dist.project_name, version, requires_python, | |
96 ) | |
97 return | |
98 | |
99 raise UnsupportedPythonVersion( | |
100 'Package {!r} requires a different Python: {} not in {!r}'.format( | |
101 dist.project_name, version, requires_python, | |
102 )) | |
103 | |
104 | |
105 class Resolver(object): | |
106 """Resolves which packages need to be installed/uninstalled to perform \ | |
107 the requested operation without breaking the requirements of any package. | |
108 """ | |
109 | |
110 _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"} | |
111 | |
112 def __init__( | |
113 self, | |
114 preparer, # type: RequirementPreparer | |
115 finder, # type: PackageFinder | |
116 make_install_req, # type: InstallRequirementProvider | |
117 use_user_site, # type: bool | |
118 ignore_dependencies, # type: bool | |
119 ignore_installed, # type: bool | |
120 ignore_requires_python, # type: bool | |
121 force_reinstall, # type: bool | |
122 upgrade_strategy, # type: str | |
123 py_version_info=None, # type: Optional[Tuple[int, ...]] | |
124 ): | |
125 # type: (...) -> None | |
126 super(Resolver, self).__init__() | |
127 assert upgrade_strategy in self._allowed_strategies | |
128 | |
129 if py_version_info is None: | |
130 py_version_info = sys.version_info[:3] | |
131 else: | |
132 py_version_info = normalize_version_info(py_version_info) | |
133 | |
134 self._py_version_info = py_version_info | |
135 | |
136 self.preparer = preparer | |
137 self.finder = finder | |
138 | |
139 self.upgrade_strategy = upgrade_strategy | |
140 self.force_reinstall = force_reinstall | |
141 self.ignore_dependencies = ignore_dependencies | |
142 self.ignore_installed = ignore_installed | |
143 self.ignore_requires_python = ignore_requires_python | |
144 self.use_user_site = use_user_site | |
145 self._make_install_req = make_install_req | |
146 | |
147 self._discovered_dependencies = \ | |
148 defaultdict(list) # type: DiscoveredDependencies | |
149 | |
150 def resolve(self, requirement_set): | |
151 # type: (RequirementSet) -> None | |
152 """Resolve what operations need to be done | |
153 | |
154 As a side-effect of this method, the packages (and their dependencies) | |
155 are downloaded, unpacked and prepared for installation. This | |
156 preparation is done by ``pip.operations.prepare``. | |
157 | |
158 Once PyPI has static dependency metadata available, it would be | |
159 possible to move the preparation to become a step separated from | |
160 dependency resolution. | |
161 """ | |
162 # If any top-level requirement has a hash specified, enter | |
163 # hash-checking mode, which requires hashes from all. | |
164 root_reqs = ( | |
165 requirement_set.unnamed_requirements + | |
166 list(requirement_set.requirements.values()) | |
167 ) | |
168 | |
169 # Actually prepare the files, and collect any exceptions. Most hash | |
170 # exceptions cannot be checked ahead of time, because | |
171 # req.populate_link() needs to be called before we can make decisions | |
172 # based on link type. | |
173 discovered_reqs = [] # type: List[InstallRequirement] | |
174 hash_errors = HashErrors() | |
175 for req in chain(root_reqs, discovered_reqs): | |
176 try: | |
177 discovered_reqs.extend(self._resolve_one(requirement_set, req)) | |
178 except HashError as exc: | |
179 exc.req = req | |
180 hash_errors.append(exc) | |
181 | |
182 if hash_errors: | |
183 raise hash_errors | |
184 | |
185 def _is_upgrade_allowed(self, req): | |
186 # type: (InstallRequirement) -> bool | |
187 if self.upgrade_strategy == "to-satisfy-only": | |
188 return False | |
189 elif self.upgrade_strategy == "eager": | |
190 return True | |
191 else: | |
192 assert self.upgrade_strategy == "only-if-needed" | |
193 return req.is_direct | |
194 | |
195 def _set_req_to_reinstall(self, req): | |
196 # type: (InstallRequirement) -> None | |
197 """ | |
198 Set a requirement to be installed. | |
199 """ | |
200 # Don't uninstall the conflict if doing a user install and the | |
201 # conflict is not a user install. | |
202 if not self.use_user_site or dist_in_usersite(req.satisfied_by): | |
203 req.should_reinstall = True | |
204 req.satisfied_by = None | |
205 | |
206 def _check_skip_installed(self, req_to_install): | |
207 # type: (InstallRequirement) -> Optional[str] | |
208 """Check if req_to_install should be skipped. | |
209 | |
210 This will check if the req is installed, and whether we should upgrade | |
211 or reinstall it, taking into account all the relevant user options. | |
212 | |
213 After calling this req_to_install will only have satisfied_by set to | |
214 None if the req_to_install is to be upgraded/reinstalled etc. Any | |
215 other value will be a dist recording the current thing installed that | |
216 satisfies the requirement. | |
217 | |
218 Note that for vcs urls and the like we can't assess skipping in this | |
219 routine - we simply identify that we need to pull the thing down, | |
220 then later on it is pulled down and introspected to assess upgrade/ | |
221 reinstalls etc. | |
222 | |
223 :return: A text reason for why it was skipped, or None. | |
224 """ | |
225 if self.ignore_installed: | |
226 return None | |
227 | |
228 req_to_install.check_if_exists(self.use_user_site) | |
229 if not req_to_install.satisfied_by: | |
230 return None | |
231 | |
232 if self.force_reinstall: | |
233 self._set_req_to_reinstall(req_to_install) | |
234 return None | |
235 | |
236 if not self._is_upgrade_allowed(req_to_install): | |
237 if self.upgrade_strategy == "only-if-needed": | |
238 return 'already satisfied, skipping upgrade' | |
239 return 'already satisfied' | |
240 | |
241 # Check for the possibility of an upgrade. For link-based | |
242 # requirements we have to pull the tree down and inspect to assess | |
243 # the version #, so it's handled way down. | |
244 if not req_to_install.link: | |
245 try: | |
246 self.finder.find_requirement(req_to_install, upgrade=True) | |
247 except BestVersionAlreadyInstalled: | |
248 # Then the best version is installed. | |
249 return 'already up-to-date' | |
250 except DistributionNotFound: | |
251 # No distribution found, so we squash the error. It will | |
252 # be raised later when we re-try later to do the install. | |
253 # Why don't we just raise here? | |
254 pass | |
255 | |
256 self._set_req_to_reinstall(req_to_install) | |
257 return None | |
258 | |
259 def _get_abstract_dist_for(self, req): | |
260 # type: (InstallRequirement) -> AbstractDistribution | |
261 """Takes a InstallRequirement and returns a single AbstractDist \ | |
262 representing a prepared variant of the same. | |
263 """ | |
264 if req.editable: | |
265 return self.preparer.prepare_editable_requirement(req) | |
266 | |
267 # satisfied_by is only evaluated by calling _check_skip_installed, | |
268 # so it must be None here. | |
269 assert req.satisfied_by is None | |
270 skip_reason = self._check_skip_installed(req) | |
271 | |
272 if req.satisfied_by: | |
273 return self.preparer.prepare_installed_requirement( | |
274 req, skip_reason | |
275 ) | |
276 | |
277 upgrade_allowed = self._is_upgrade_allowed(req) | |
278 | |
279 # We eagerly populate the link, since that's our "legacy" behavior. | |
280 require_hashes = self.preparer.require_hashes | |
281 req.populate_link(self.finder, upgrade_allowed, require_hashes) | |
282 abstract_dist = self.preparer.prepare_linked_requirement(req) | |
283 | |
284 # NOTE | |
285 # The following portion is for determining if a certain package is | |
286 # going to be re-installed/upgraded or not and reporting to the user. | |
287 # This should probably get cleaned up in a future refactor. | |
288 | |
289 # req.req is only avail after unpack for URL | |
290 # pkgs repeat check_if_exists to uninstall-on-upgrade | |
291 # (#14) | |
292 if not self.ignore_installed: | |
293 req.check_if_exists(self.use_user_site) | |
294 | |
295 if req.satisfied_by: | |
296 should_modify = ( | |
297 self.upgrade_strategy != "to-satisfy-only" or | |
298 self.force_reinstall or | |
299 self.ignore_installed or | |
300 req.link.scheme == 'file' | |
301 ) | |
302 if should_modify: | |
303 self._set_req_to_reinstall(req) | |
304 else: | |
305 logger.info( | |
306 'Requirement already satisfied (use --upgrade to upgrade):' | |
307 ' %s', req, | |
308 ) | |
309 | |
310 return abstract_dist | |
311 | |
312 def _resolve_one( | |
313 self, | |
314 requirement_set, # type: RequirementSet | |
315 req_to_install, # type: InstallRequirement | |
316 ): | |
317 # type: (...) -> List[InstallRequirement] | |
318 """Prepare a single requirements file. | |
319 | |
320 :return: A list of additional InstallRequirements to also install. | |
321 """ | |
322 # Tell user what we are doing for this requirement: | |
323 # obtain (editable), skipping, processing (local url), collecting | |
324 # (remote url or package name) | |
325 if req_to_install.constraint or req_to_install.prepared: | |
326 return [] | |
327 | |
328 req_to_install.prepared = True | |
329 | |
330 # register tmp src for cleanup in case something goes wrong | |
331 requirement_set.reqs_to_cleanup.append(req_to_install) | |
332 | |
333 abstract_dist = self._get_abstract_dist_for(req_to_install) | |
334 | |
335 # Parse and return dependencies | |
336 dist = abstract_dist.get_pkg_resources_distribution() | |
337 # This will raise UnsupportedPythonVersion if the given Python | |
338 # version isn't compatible with the distribution's Requires-Python. | |
339 _check_dist_requires_python( | |
340 dist, version_info=self._py_version_info, | |
341 ignore_requires_python=self.ignore_requires_python, | |
342 ) | |
343 | |
344 more_reqs = [] # type: List[InstallRequirement] | |
345 | |
346 def add_req(subreq, extras_requested): | |
347 sub_install_req = self._make_install_req( | |
348 str(subreq), | |
349 req_to_install, | |
350 ) | |
351 parent_req_name = req_to_install.name | |
352 to_scan_again, add_to_parent = requirement_set.add_requirement( | |
353 sub_install_req, | |
354 parent_req_name=parent_req_name, | |
355 extras_requested=extras_requested, | |
356 ) | |
357 if parent_req_name and add_to_parent: | |
358 self._discovered_dependencies[parent_req_name].append( | |
359 add_to_parent | |
360 ) | |
361 more_reqs.extend(to_scan_again) | |
362 | |
363 with indent_log(): | |
364 # We add req_to_install before its dependencies, so that we | |
365 # can refer to it when adding dependencies. | |
366 if not requirement_set.has_requirement(req_to_install.name): | |
367 # 'unnamed' requirements will get added here | |
368 # 'unnamed' requirements can only come from being directly | |
369 # provided by the user. | |
370 assert req_to_install.is_direct | |
371 requirement_set.add_requirement( | |
372 req_to_install, parent_req_name=None, | |
373 ) | |
374 | |
375 if not self.ignore_dependencies: | |
376 if req_to_install.extras: | |
377 logger.debug( | |
378 "Installing extra requirements: %r", | |
379 ','.join(req_to_install.extras), | |
380 ) | |
381 missing_requested = sorted( | |
382 set(req_to_install.extras) - set(dist.extras) | |
383 ) | |
384 for missing in missing_requested: | |
385 logger.warning( | |
386 '%s does not provide the extra \'%s\'', | |
387 dist, missing | |
388 ) | |
389 | |
390 available_requested = sorted( | |
391 set(dist.extras) & set(req_to_install.extras) | |
392 ) | |
393 for subreq in dist.requires(available_requested): | |
394 add_req(subreq, extras_requested=available_requested) | |
395 | |
396 if not req_to_install.editable and not req_to_install.satisfied_by: | |
397 # XXX: --no-install leads this to report 'Successfully | |
398 # downloaded' for only non-editable reqs, even though we took | |
399 # action on them. | |
400 requirement_set.successfully_downloaded.append(req_to_install) | |
401 | |
402 return more_reqs | |
403 | |
404 def get_installation_order(self, req_set): | |
405 # type: (RequirementSet) -> List[InstallRequirement] | |
406 """Create the installation order. | |
407 | |
408 The installation order is topological - requirements are installed | |
409 before the requiring thing. We break cycles at an arbitrary point, | |
410 and make no other guarantees. | |
411 """ | |
412 # The current implementation, which we may change at any point | |
413 # installs the user specified things in the order given, except when | |
414 # dependencies must come earlier to achieve topological order. | |
415 order = [] | |
416 ordered_reqs = set() # type: Set[InstallRequirement] | |
417 | |
418 def schedule(req): | |
419 if req.satisfied_by or req in ordered_reqs: | |
420 return | |
421 if req.constraint: | |
422 return | |
423 ordered_reqs.add(req) | |
424 for dep in self._discovered_dependencies[req.name]: | |
425 schedule(dep) | |
426 order.append(req) | |
427 | |
428 for install_req in req_set.requirements.values(): | |
429 schedule(install_req) | |
430 return order |