comparison planemo/lib/python3.7/site-packages/pip/_internal/legacy_resolve.py @ 1:56ad4e20f292 draft

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