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