/usr/lib/python2.7/dist-packages/pex/resolver.py is in python-pex 1.1.14-2ubuntu2.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import print_function
import os
import shutil
import time
from collections import namedtuple
from pkg_resources import safe_name
from .common import safe_mkdir
from .fetcher import Fetcher
from .interpreter import PythonInterpreter
from .iterator import Iterator, IteratorInterface
from .orderedset import OrderedSet
from .package import Package, distribution_compatible
from .platforms import Platform
from .resolvable import ResolvableRequirement, resolvables_from_iterable
from .resolver_options import ResolverOptionsBuilder
from .tracer import TRACER
from .util import DistributionHelper
class Untranslateable(Exception):
pass
class Unsatisfiable(Exception):
pass
class StaticIterator(IteratorInterface):
"""An iterator that iterates over a static list of packages."""
def __init__(self, packages):
self._packages = packages
def iter(self, req):
for package in self._packages:
if package.satisfies(req):
yield package
class _ResolvedPackages(namedtuple('_ResolvedPackages', 'resolvable packages parent')):
@classmethod
def empty(cls):
return cls(None, OrderedSet(), None)
def merge(self, other):
if other.resolvable is None:
return _ResolvedPackages(self.resolvable, self.packages, self.parent)
return _ResolvedPackages(
self.resolvable,
self.packages & other.packages,
self.parent)
class _ResolvableSet(object):
@classmethod
def normalize(cls, name):
return safe_name(name).lower()
def __init__(self, tuples=None):
# A list of _ResolvedPackages
self.__tuples = tuples or []
def _collapse(self):
# Collapse all resolvables by name along with the intersection of all compatible packages.
# If the set of compatible packages is the empty set, then we cannot satisfy all the
# specifications for a particular name (e.g. "setuptools==2.2 setuptools>4".)
#
# We need to return the resolvable since it carries its own network context and configuration
# regarding package precedence. This is arbitrary -- we could just as easily say "last
# resolvable wins" but it seems highly unlikely this will materially affect anybody
# adversely but could be the source of subtle resolution quirks.
resolvables = {}
for resolved_packages in self.__tuples:
key = self.normalize(resolved_packages.resolvable.name)
previous = resolvables.get(key, _ResolvedPackages.empty())
if previous.resolvable is None:
resolvables[key] = resolved_packages
else:
resolvables[key] = previous.merge(resolved_packages)
return resolvables
def _synthesize_parents(self, name):
def render_resolvable(resolved_packages):
return '%s%s' % (
str(resolved_packages.resolvable),
'(from: %s)' % resolved_packages.parent if resolved_packages.parent else '')
return ', '.join(
render_resolvable(resolved_packages) for resolved_packages in self.__tuples
if self.normalize(resolved_packages.resolvable.name) == self.normalize(name))
def _check(self):
# Check whether or not the resolvables in this set are satisfiable, raise an exception if not.
for name, resolved_packages in self._collapse().items():
if not resolved_packages.packages:
raise Unsatisfiable('Could not satisfy all requirements for %s:\n %s' % (
resolved_packages.resolvable, self._synthesize_parents(name)))
def merge(self, resolvable, packages, parent=None):
"""Add a resolvable and its resolved packages."""
self.__tuples.append(_ResolvedPackages(resolvable, OrderedSet(packages), parent))
self._check()
def get(self, name):
"""Get the set of compatible packages given a resolvable name."""
resolvable, packages, parent = self._collapse().get(
self.normalize(name), _ResolvedPackages.empty())
return packages
def packages(self):
"""Return a snapshot of resolvable => compatible packages set from the resolvable set."""
return list(self._collapse().values())
def extras(self, name):
return set.union(
*[set(tup.resolvable.extras()) for tup in self.__tuples
if self.normalize(tup.resolvable.name) == self.normalize(name)])
def replace_built(self, built_packages):
"""Return a copy of this resolvable set but with built packages.
:param dict built_packages: A mapping from a resolved package to its locally built package.
:returns: A new resolvable set with built package replacements made.
"""
def map_packages(resolved_packages):
packages = OrderedSet(built_packages.get(p, p) for p in resolved_packages.packages)
return _ResolvedPackages(resolved_packages.resolvable, packages, resolved_packages.parent)
return _ResolvableSet([map_packages(rp) for rp in self.__tuples])
class Resolver(object):
"""Interface for resolving resolvable entities into python packages."""
class Error(Exception): pass
@classmethod
def filter_packages_by_interpreter(cls, packages, interpreter, platform):
return [package for package in packages
if package.compatible(interpreter.identity, platform)]
def __init__(self, interpreter=None, platform=None):
self._interpreter = interpreter or PythonInterpreter.get()
self._platform = platform or Platform.current()
def package_iterator(self, resolvable, existing=None):
if existing:
existing = resolvable.compatible(StaticIterator(existing))
else:
existing = resolvable.packages()
return self.filter_packages_by_interpreter(existing, self._interpreter, self._platform)
def build(self, package, options):
context = options.get_context()
translator = options.get_translator(self._interpreter, self._platform)
with TRACER.timed('Fetching %s' % package.url, V=2):
local_package = Package.from_href(context.fetch(package))
if local_package is None:
raise Untranslateable('Could not fetch package %s' % package)
with TRACER.timed('Translating %s into distribution' % local_package.local_path, V=2):
dist = translator.translate(local_package)
if dist is None:
raise Untranslateable('Package %s is not translateable by %s' % (package, translator))
if not distribution_compatible(dist, self._interpreter, self._platform):
raise Untranslateable(
'Could not get distribution for %s on platform %s.' % (package, self._platform))
return dist
def resolve(self, resolvables, resolvable_set=None):
resolvables = [(resolvable, None) for resolvable in resolvables]
resolvable_set = resolvable_set or _ResolvableSet()
processed_resolvables = set()
processed_packages = {}
distributions = {}
while resolvables:
while resolvables:
resolvable, parent = resolvables.pop(0)
if resolvable in processed_resolvables:
continue
packages = self.package_iterator(resolvable, existing=resolvable_set.get(resolvable.name))
resolvable_set.merge(resolvable, packages, parent)
processed_resolvables.add(resolvable)
built_packages = {}
for resolvable, packages, parent in resolvable_set.packages():
assert len(packages) > 0, 'ResolvableSet.packages(%s) should not be empty' % resolvable
package = next(iter(packages))
if resolvable.name in processed_packages:
# TODO implement backtracking?
if package != processed_packages[resolvable.name]:
raise self.Error('Ambiguous resolvable: %s' % resolvable)
continue
if package not in distributions:
dist = self.build(package, resolvable.options)
built_package = Package.from_href(dist.location)
built_packages[package] = built_package
distributions[built_package] = dist
package = built_package
distribution = distributions[package]
processed_packages[resolvable.name] = package
new_parent = '%s->%s' % (parent, resolvable) if parent else str(resolvable)
resolvables.extend(
(ResolvableRequirement(req, resolvable.options), new_parent) for req in
distribution.requires(extras=resolvable_set.extras(resolvable.name)))
resolvable_set = resolvable_set.replace_built(built_packages)
return list(distributions.values())
class CachingResolver(Resolver):
"""A package resolver implementing a package cache."""
@classmethod
def filter_packages_by_ttl(cls, packages, ttl, now=None):
now = now if now is not None else time.time()
return [package for package in packages
if package.remote or package.local and (now - os.path.getmtime(package.local_path)) < ttl]
def __init__(self, cache, cache_ttl, *args, **kw):
self.__cache = cache
self.__cache_ttl = cache_ttl
safe_mkdir(self.__cache)
super(CachingResolver, self).__init__(*args, **kw)
# Short-circuiting package iterator.
def package_iterator(self, resolvable, existing=None):
iterator = Iterator(fetchers=[Fetcher([self.__cache])])
packages = self.filter_packages_by_interpreter(
resolvable.compatible(iterator), self._interpreter, self._platform)
if packages:
if resolvable.exact:
return packages
if self.__cache_ttl:
packages = self.filter_packages_by_ttl(packages, self.__cache_ttl)
if packages:
return packages
return super(CachingResolver, self).package_iterator(resolvable, existing=existing)
# Caching sandwich.
def build(self, package, options):
# cache package locally
if package.remote:
package = Package.from_href(options.get_context().fetch(package, into=self.__cache))
os.utime(package.local_path, None)
# build into distribution
dist = super(CachingResolver, self).build(package, options)
# if distribution is not in cache, copy
target = os.path.join(self.__cache, os.path.basename(dist.location))
if not os.path.exists(target):
shutil.copyfile(dist.location, target + '~')
os.rename(target + '~', target)
os.utime(target, None)
return DistributionHelper.distribution_from_path(target)
def resolve(
requirements,
fetchers=None,
interpreter=None,
platform=None,
context=None,
precedence=None,
cache=None,
cache_ttl=None):
"""Produce all distributions needed to (recursively) meet `requirements`
:param requirements: An iterator of Requirement-like things, either
:class:`pkg_resources.Requirement` objects or requirement strings.
:keyword fetchers: (optional) A list of :class:`Fetcher` objects for locating packages. If
unspecified, the default is to look for packages on PyPI.
:keyword interpreter: (optional) A :class:`PythonInterpreter` object to use for building
distributions and for testing distribution compatibility.
:keyword platform: (optional) A PEP425-compatible platform string to use for filtering
compatible distributions. If unspecified, the current platform is used, as determined by
`Platform.current()`.
:keyword context: (optional) A :class:`Context` object to use for network access. If
unspecified, the resolver will attempt to use the best available network context.
:type threads: int
:keyword precedence: (optional) An ordered list of allowable :class:`Package` classes
to be used for producing distributions. For example, if precedence is supplied as
``(WheelPackage, SourcePackage)``, wheels will be preferred over building from source, and
eggs will not be used at all. If ``(WheelPackage, EggPackage)`` is suppplied, both wheels and
eggs will be used, but the resolver will not resort to building anything from source.
:keyword cache: (optional) A directory to use to cache distributions locally.
:keyword cache_ttl: (optional integer in seconds) If specified, consider non-exact matches when
resolving requirements. For example, if ``setuptools==2.2`` is specified and setuptools 2.2 is
available in the cache, it will always be used. However, if a non-exact requirement such as
``setuptools>=2,<3`` is specified and there exists a setuptools distribution newer than
cache_ttl seconds that satisfies the requirement, then it will be used. If the distribution
is older than cache_ttl seconds, it will be ignored. If ``cache_ttl`` is not specified,
resolving inexact requirements will always result in making network calls through the
``context``.
:returns: List of :class:`pkg_resources.Distribution` instances meeting ``requirements``.
:raises Unsatisfiable: If ``requirements`` is not transitively satisfiable.
:raises Untranslateable: If no compatible distributions could be acquired for
a particular requirement.
This method improves upon the setuptools dependency resolution algorithm by maintaining sets of
all compatible distributions encountered for each requirement rather than the single best
distribution encountered for each requirement. This prevents situations where ``tornado`` and
``tornado==2.0`` could be treated as incompatible with each other because the "best
distribution" when encountering ``tornado`` was tornado 3.0. Instead, ``resolve`` maintains the
set of compatible distributions for each requirement as it is encountered, and iteratively filters
the set. If the set of distributions ever becomes empty, then ``Unsatisfiable`` is raised.
.. versionchanged:: 0.8
A number of keywords were added to make requirement resolution slightly easier to configure.
The optional ``obtainer`` keyword was replaced by ``fetchers``, ``translator``, ``context``,
``threads``, ``precedence``, ``cache`` and ``cache_ttl``, also all optional keywords.
.. versionchanged:: 1.0
The ``translator`` and ``threads`` keywords have been removed. The choice of threading
policy is now implicit. The choice of translation policy is dictated by ``precedence``
directly.
.. versionchanged:: 1.0
``resolver`` is now just a wrapper around the :class:`Resolver` and :class:`CachingResolver`
classes.
"""
builder = ResolverOptionsBuilder(
fetchers=fetchers,
precedence=precedence,
context=context,
)
if cache:
resolver = CachingResolver(cache, cache_ttl, interpreter=interpreter, platform=platform)
else:
resolver = Resolver(interpreter=interpreter, platform=platform)
return resolver.resolve(resolvables_from_iterable(requirements, builder))
|