1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2012-2013 Vinay Sajip.
4
# Licensed to the Python Software Foundation under a contributor agreement.
5
# See LICENSE.txt and CONTRIBUTORS.txt.
18
from . import DistlibException
19
from .compat import (urljoin, urlparse, urlunparse, url2pathname, pathname2url,
20
queue, quote, unescape, string_types, build_opener,
21
HTTPRedirectHandler as BaseRedirectHandler,
22
Request, HTTPError, URLError)
23
from .database import Distribution, DistributionPath, make_dist
24
from .metadata import Metadata
25
from .util import (cached_property, parse_credentials, ensure_slash,
26
split_filename, get_project_data, parse_requirement,
28
from .version import get_scheme, UnsupportedVersionError
29
from .wheel import Wheel, is_compatible
31
logger = logging.getLogger(__name__)
33
MD5_HASH = re.compile('^md5=([a-f0-9]+)$')
34
CHARSET = re.compile(r';\s*charset\s*=\s*(.*)\s*$', re.I)
35
HTML_CONTENT_TYPE = re.compile('text/html|application/x(ht)?ml')
36
DEFAULT_INDEX = 'http://python.org/pypi'
38
def get_all_distribution_names(url=None):
40
Return all distribution names known by an index.
41
:param url: The URL of the index.
42
:return: A list of all known distribution names.
46
client = ServerProxy(url, timeout=3.0)
47
return client.list_packages()
49
class RedirectHandler(BaseRedirectHandler):
51
A class to work around a bug in some Python 3.2.x releases.
53
# There's a bug in the base version for some 3.2.x
54
# (e.g. 3.2.2 on Ubuntu Oneiric). If a Location header
55
# returns e.g. /abc, it bails because it says the scheme ''
56
# is bogus, when actually it should use the request's
57
# URL for the scheme. See Python issue #13696.
58
def http_error_302(self, req, fp, code, msg, headers):
59
# Some servers (incorrectly) return multiple Location headers
60
# (so probably same goes for URI). Use first header.
62
for key in ('location', 'uri'):
68
urlparts = urlparse(newurl)
69
if urlparts.scheme == '':
70
newurl = urljoin(req.get_full_url(), newurl)
71
if hasattr(headers, 'replace_header'):
72
headers.replace_header(key, newurl)
75
return BaseRedirectHandler.http_error_302(self, req, fp, code, msg,
78
http_error_301 = http_error_303 = http_error_307 = http_error_302
80
class Locator(object):
82
A base class for locators - things that locate distributions.
84
source_extensions = ('.tar.gz', '.tar.bz2', '.tar', '.zip', '.tgz', '.tbz')
85
binary_extensions = ('.egg', '.exe', '.whl')
86
excluded_extensions = ('.pdf',)
88
# A list of tags indicating which wheels you want to match. The default
89
# value of None matches against the tags compatible with the running
90
# Python. If you want to match other values, set wheel_tags on a locator
91
# instance to a list of tuples (pyver, abi, arch) which you want to match.
94
downloadable_extensions = source_extensions + ('.whl',)
96
def __init__(self, scheme='default'):
98
Initialise an instance.
99
:param scheme: Because locators look for most recent versions, they
100
need to know the version scheme to use. This specifies
101
the current PEP-recommended scheme - use ``'legacy'``
102
if you need to support existing distributions on PyPI.
106
# Because of bugs in some of the handlers on some of the platforms,
107
# we use our own opener rather than just using urlopen.
108
self.opener = build_opener(RedirectHandler())
110
def clear_cache(self):
113
def _get_scheme(self):
116
def _set_scheme(self, value):
119
scheme = property(_get_scheme, _set_scheme)
121
def _get_project(self, name):
123
For a given project, get a dictionary mapping available versions to Distribution
126
This should be implemented in subclasses.
128
raise NotImplementedError('Please implement in the subclass')
130
def get_distribution_names(self):
132
Return all the distribution names known to this locator.
134
raise NotImplementedError('Please implement in the subclass')
136
def get_project(self, name):
138
For a given project, get a dictionary mapping available versions to Distribution
141
This calls _get_project to do all the work, and just implements a caching layer on top.
143
if self._cache is None:
144
result = self._get_project(name)
145
elif name in self._cache:
146
result = self._cache[name]
148
result = self._get_project(name)
149
self._cache[name] = result
152
def score_url(self, url):
154
Give an url a score which can be used to choose preferred URLs
155
for a given project release.
158
return (t.scheme != 'https', 'pypi.python.org' in t.netloc,
159
posixpath.basename(t.path))
161
def prefer_url(self, url1, url2):
163
Choose one of two URLs where both are candidates for distribution
164
archives for the same version of a distribution (for example,
167
The current implement favours http:// URLs over https://, archives
168
from PyPI over those from other locations and then the archive name.
170
if url1 == 'UNKNOWN':
174
s1 = self.score_url(url1)
175
s2 = self.score_url(url2)
179
logger.debug('Not replacing %r with %r', url1, url2)
181
logger.debug('Replacing %r with %r', url1, url2)
184
def split_filename(self, filename, project_name):
186
Attempt to split a filename in project name, version and Python version.
188
return split_filename(filename, project_name)
190
def convert_url_to_download_info(self, url, project_name):
192
See if a URL is a candidate for a download URL for a project (the URL
193
has typically been scraped from an HTML page).
195
If it is, a dictionary is returned with keys "name", "version",
196
"filename" and "url"; otherwise, None is returned.
198
def same_project(name1, name2):
199
name1, name2 = name1.lower(), name2.lower()
203
# distribute replaces '-' by '_' in project names, so it
204
# can tell where the version starts in a filename.
205
result = name1.replace('_', '-') == name2.replace('_', '-')
209
scheme, netloc, path, params, query, frag = urlparse(url)
210
if frag.lower().startswith('egg='):
211
logger.debug('%s: version hint in fragment: %r',
214
if path and path[-1] == '/':
216
if path.endswith('.whl'):
219
if is_compatible(wheel, self.wheel_tags):
220
if project_name is None:
223
include = same_project(wheel.name, project_name)
227
'version': wheel.version,
228
'filename': wheel.filename,
229
'url': urlunparse((scheme, netloc, origpath,
231
'python-version': ', '.join(
232
['.'.join(list(v[2:])) for v in wheel.pyver]),
234
m = MD5_HASH.match(frag)
236
result['md5_digest'] = m.group(1)
237
except Exception as e:
238
logger.warning('invalid path for wheel: %s', path)
239
elif path.endswith(self.downloadable_extensions):
240
path = filename = posixpath.basename(path)
241
for ext in self.downloadable_extensions:
242
if path.endswith(ext):
243
path = path[:-len(ext)]
244
t = self.split_filename(path, project_name)
246
logger.debug('No match for project/version: %s', path)
248
name, version, pyver = t
249
if not project_name or same_project(project_name, name):
253
'filename': filename,
254
'url': urlunparse((scheme, netloc, origpath,
256
#'packagetype': 'sdist',
259
result['python-version'] = pyver
260
m = MD5_HASH.match(frag)
262
result['md5_digest'] = m.group(1)
266
def _update_version_data(self, result, info):
268
Update a result dictionary (the final result from _get_project) with a dictionary for a
269
specific version, whih typically holds information gleaned from a filename or URL for an
270
archive for the distribution.
272
name = info.pop('name')
273
version = info.pop('version')
274
if version in result:
275
dist = result[version]
278
dist = make_dist(name, version, scheme=self.scheme)
280
dist.md5_digest = info.get('md5_digest')
281
if 'python-version' in info:
282
md['Requires-Python'] = info['python-version']
283
if md['Download-URL'] != info['url']:
284
md['Download-URL'] = self.prefer_url(md['Download-URL'],
287
result[version] = dist
289
def locate(self, requirement, prereleases=False):
291
Find the most recent distribution which matches the given
294
:param requirement: A requirement of the form 'foo (1.0)' or perhaps
295
'foo (>= 1.0, < 2.0, != 1.3)'
296
:param prereleases: If ``True``, allow pre-release versions
297
to be located. Otherwise, pre-release versions
299
:return: A :class:`Distribution` instance, or ``None`` if no such
300
distribution could be located.
303
scheme = get_scheme(self.scheme)
304
r = parse_requirement(requirement)
306
raise DistlibException('Not a valid requirement: %r' % requirement)
308
# lose the extras part of the requirement
309
requirement = r.requirement
310
matcher = scheme.matcher(requirement)
311
vcls = matcher.version_class
312
logger.debug('matcher: %s (%s)', matcher, type(matcher).__name__)
313
versions = self.get_project(matcher.name)
315
# sometimes, versions are invalid
319
if not matcher.match(k):
320
logger.debug('%s did not match %r', matcher, k)
322
if prereleases or not vcls(k).is_prerelease:
325
logger.debug('skipping pre-release version %s', k)
327
logger.warning('error matching %s with %r', matcher, k)
328
pass # slist.append(k)
330
slist = sorted(slist, key=scheme.key)
332
logger.debug('sorted list: %s', slist)
333
result = versions[slist[-1]]
334
if result and r.extras:
335
result.extras = r.extras
339
class PyPIRPCLocator(Locator):
341
This locator uses XML-RPC to locate distributions. It therefore cannot be
342
used with simple mirrors (that only mirror file content).
344
def __init__(self, url, **kwargs):
346
Initialise an instance.
348
:param url: The URL to use for XML-RPC.
349
:param kwargs: Passed to the superclass constructor.
351
super(PyPIRPCLocator, self).__init__(**kwargs)
353
self.client = ServerProxy(url, timeout=3.0)
355
def get_distribution_names(self):
357
Return all the distribution names known to this locator.
359
return set(self.client.list_packages())
361
def _get_project(self, name):
363
versions = self.client.package_releases(name, True)
365
urls = self.client.release_urls(name, v)
366
data = self.client.release_data(name, v)
367
metadata = Metadata(scheme=self.scheme)
368
metadata.update(data)
369
dist = Distribution(metadata)
372
metadata['Download-URL'] = info['url']
373
dist.md5_digest = info.get('md5_digest')
378
class PyPIJSONLocator(Locator):
380
This locator uses PyPI's JSON interface. It's very limited in functionality
381
nad probably not worth using.
383
def __init__(self, url, **kwargs):
384
super(PyPIJSONLocator, self).__init__(**kwargs)
385
self.base_url = ensure_slash(url)
387
def get_distribution_names(self):
389
Return all the distribution names known to this locator.
391
raise NotImplementedError('Not available from this locator')
393
def _get_project(self, name):
395
url = urljoin(self.base_url, '%s/json' % quote(name))
397
resp = self.opener.open(url)
398
data = resp.read().decode() # for now
400
md = Metadata(scheme=self.scheme)
402
dist = Distribution(md)
406
md['Download-URL'] = info['url']
407
dist.md5_digest = info.get('md5_digest')
409
result[md.version] = dist
410
except Exception as e:
411
logger.exception('JSON fetch failed: %s', e)
417
This class represents a scraped HTML page.
419
# The following slightly hairy-looking regex just looks for the contents of
420
# an anchor link, which has an attribute "href" either immediately preceded
421
# or immediately followed by a "rel" attribute. The attribute values can be
422
# declared with double quotes, single quotes or no quotes - which leads to
423
# the length of the expression.
424
_href = re.compile("""
425
(rel\s*=\s*(?:"(?P<rel1>[^"]*)"|'(?P<rel2>[^']*)'|(?P<rel3>[^>\s\n]*))\s+)?
426
href\s*=\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\s\n]*))
427
(\s+rel\s*=\s*(?:"(?P<rel4>[^"]*)"|'(?P<rel5>[^']*)'|(?P<rel6>[^>\s\n]*)))?
428
""", re.I | re.S | re.X)
429
_base = re.compile(r"""<base\s+href\s*=\s*['"]?([^'">]+)""", re.I | re.S)
431
def __init__(self, data, url):
433
Initialise an instance with the Unicode page contents and the URL they
437
self.base_url = self.url = url
438
m = self._base.search(self.data)
440
self.base_url = m.group(1)
442
_clean_re = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.I)
447
Return the URLs of all the links on a page together with information
448
about their "rel" attribute, for determining which ones to treat as
449
downloads and which ones to queue for further scraping.
453
scheme, netloc, path, params, query, frag = urlparse(url)
454
return urlunparse((scheme, netloc, quote(path),
455
params, query, frag))
458
for match in self._href.finditer(self.data):
459
d = match.groupdict('')
460
rel = (d['rel1'] or d['rel2'] or d['rel3'] or
461
d['rel4'] or d['rel5'] or d['rel6'])
462
url = d['url1'] or d['url2'] or d['url3']
463
url = urljoin(self.base_url, url)
465
url = self._clean_re.sub(lambda m: '%%%2x' % ord(m.group(0)), url)
466
result.add((url, rel))
467
# We sort the result, hoping to bring the most recent versions
469
result = sorted(result, key=lambda t: t[0], reverse=True)
473
class SimpleScrapingLocator(Locator):
475
A locator which scrapes HTML pages to locate downloads for a distribution.
476
This runs multiple threads to do the I/O; performance is at least as good
477
as pip's PackageFinder, which works in an analogous fashion.
480
# These are used to deal with various Content-Encoding schemes.
482
'deflate': zlib.decompress,
483
'gzip': lambda b: gzip.GzipFile(fileobj=BytesIO(d)).read(),
487
def __init__(self, url, timeout=None, num_workers=10, **kwargs):
489
Initialise an instance.
490
:param url: The root URL to use for scraping.
491
:param timeout: The timeout, in seconds, to be applied to requests.
492
This defaults to ``None`` (no timeout specified).
493
:param num_workers: The number of worker threads you want to do I/O,
495
:param kwargs: Passed to the superclass.
497
super(SimpleScrapingLocator, self).__init__(**kwargs)
498
self.base_url = ensure_slash(url)
499
self.timeout = timeout
500
self._page_cache = {}
502
self._to_fetch = queue.Queue()
503
self._bad_hosts = set()
504
self.skip_externals = False
505
self.num_workers = num_workers
506
self._lock = threading.RLock()
508
def _prepare_threads(self):
510
Threads are created only when get_project is called, and terminate
511
before it returns. They are there primarily to parallelise I/O (i.e.
515
for i in range(self.num_workers):
516
t = threading.Thread(target=self._fetch)
519
self._threads.append(t)
521
def _wait_threads(self):
523
Tell all the threads to terminate (by sending a sentinel value) and
524
wait for them to do so.
526
# Note that you need two loops, since you can't say which
527
# thread will get each sentinel
528
for t in self._threads:
529
self._to_fetch.put(None) # sentinel
530
for t in self._threads:
534
def _get_project(self, name):
535
self.result = result = {}
536
self.project_name = name
537
url = urljoin(self.base_url, '%s/' % quote(name))
539
self._page_cache.clear()
540
self._prepare_threads()
542
logger.debug('Queueing %s', url)
543
self._to_fetch.put(url)
544
self._to_fetch.join()
550
platform_dependent = re.compile(r'\b(linux-(i\d86|x86_64|arm\w+)|'
551
r'win(32|-amd64)|macosx-?\d+)\b', re.I)
553
def _is_platform_dependent(self, url):
555
Does an URL refer to a platform-specific download?
557
return self.platform_dependent.search(url)
559
def _process_download(self, url):
561
See if an URL is a suitable download for a project.
563
If it is, register information in the result dictionary (for
564
_get_project) about the specific version it's for.
566
Note that the return value isn't actually used other than as a boolean
569
if self._is_platform_dependent(url):
572
info = self.convert_url_to_download_info(url, self.project_name)
573
logger.debug('process_download: %s -> %s', url, info)
575
with self._lock: # needed because self.result is shared
576
self._update_version_data(self.result, info)
579
def _should_queue(self, link, referrer, rel):
581
Determine whether a link URL from a referring page and with a
582
particular "rel" attribute should be queued for scraping.
584
scheme, netloc, path, _, _, _ = urlparse(link)
585
if path.endswith(self.source_extensions + self.binary_extensions +
586
self.excluded_extensions):
588
elif self.skip_externals and not link.startswith(self.base_url):
590
elif not referrer.startswith(self.base_url):
592
elif rel not in ('homepage', 'download'):
594
elif scheme not in ('http', 'https', 'ftp'):
596
elif self._is_platform_dependent(link):
599
host = netloc.split(':', 1)[0]
600
if host.lower() == 'localhost':
604
logger.debug('should_queue: %s (%s) from %s -> %s', link, rel,
610
Get a URL to fetch from the work queue, get the HTML page, examine its
611
links for download candidates and candidates for further scraping.
613
This is a handy method to run in a thread.
616
url = self._to_fetch.get()
619
page = self.get_page(url)
620
if page is None: # e.g. after an error
622
for link, rel in page.links:
623
if link not in self._seen:
625
if (not self._process_download(link) and
626
self._should_queue(link, url, rel)):
627
logger.debug('Queueing %s from %s', link, url)
628
self._to_fetch.put(link)
630
# always do this, to avoid hangs :-)
631
self._to_fetch.task_done()
633
#logger.debug('Sentinel seen, quitting.')
636
def get_page(self, url):
638
Get the HTML for an URL, possibly from an in-memory cache.
640
XXX TODO Note: this cache is never actually cleared. It's assumed that
641
the data won't get stale over the lifetime of a locator instance (not
642
necessarily true for the default_locator).
644
# http://peak.telecommunity.com/DevCenter/EasyInstall#package-index-api
645
scheme, netloc, path, _, _, _ = urlparse(url)
646
if scheme == 'file' and os.path.isdir(url2pathname(path)):
647
url = urljoin(ensure_slash(url), 'index.html')
649
if url in self._page_cache:
650
result = self._page_cache[url]
651
logger.debug('Returning %s from cache: %s', url, result)
653
host = netloc.split(':', 1)[0]
655
if host in self._bad_hosts:
656
logger.debug('Skipping %s due to bad host %s', url, host)
658
req = Request(url, headers={'Accept-encoding': 'identity'})
660
logger.debug('Fetching %s', url)
661
resp = self.opener.open(req, timeout=self.timeout)
662
logger.debug('Fetched %s', url)
663
headers = resp.info()
664
content_type = headers.get('Content-Type', '')
665
if HTML_CONTENT_TYPE.match(content_type):
666
final_url = resp.geturl()
668
encoding = headers.get('Content-Encoding')
670
decoder = self.decoders[encoding] # fail if not found
673
m = CHARSET.search(content_type)
675
encoding = m.group(1)
677
data = data.decode(encoding)
679
data = data.decode('latin-1') # fallback
680
result = Page(data, final_url)
681
self._page_cache[final_url] = result
682
except HTTPError as e:
684
logger.exception('Fetch failed: %s: %s', url, e)
685
except URLError as e:
686
logger.exception('Fetch failed: %s: %s', url, e)
688
self._bad_hosts.add(host)
689
except Exception as e:
690
logger.exception('Fetch failed: %s: %s', url, e)
692
self._page_cache[url] = result # even if None (failure)
695
_distname_re = re.compile('<a href=[^>]*>([^<]+)<')
697
def get_distribution_names(self):
699
Return all the distribution names known to this locator.
702
page = self.get_page(self.base_url)
704
raise DistlibException('Unable to get %s' % self.base_url)
705
for match in self._distname_re.finditer(page.data):
706
result.add(match.group(1))
709
class DirectoryLocator(Locator):
711
This class locates distributions in a directory tree.
714
def __init__(self, path, **kwargs):
716
Initialise an instance.
717
:param path: The root of the directory tree to search.
718
:param kwargs: Passed to the superclass constructor,
720
* recursive - if True (the default), subdirectories are
721
recursed into. If False, only the top-level directory
724
self.recursive = kwargs.pop('recursive', True)
725
super(DirectoryLocator, self).__init__(**kwargs)
726
path = os.path.abspath(path)
727
if not os.path.isdir(path):
728
raise DistlibException('Not a directory: %r' % path)
731
def should_include(self, filename, parent):
733
Should a filename be considered as a candidate for a distribution
734
archive? As well as the filename, the directory which contains it
735
is provided, though not used by the current implementation.
737
return filename.endswith(self.downloadable_extensions)
739
def _get_project(self, name):
741
for root, dirs, files in os.walk(self.base_dir):
743
if self.should_include(fn, root):
744
fn = os.path.join(root, fn)
745
url = urlunparse(('file', '',
746
pathname2url(os.path.abspath(fn)),
748
info = self.convert_url_to_download_info(url, name)
750
self._update_version_data(result, info)
751
if not self.recursive:
755
def get_distribution_names(self):
757
Return all the distribution names known to this locator.
760
for root, dirs, files in os.walk(self.base_dir):
762
if self.should_include(fn, root):
763
fn = os.path.join(root, fn)
764
url = urlunparse(('file', '',
765
pathname2url(os.path.abspath(fn)),
767
info = self.convert_url_to_download_info(url, None)
769
result.add(info['name'])
770
if not self.recursive:
774
class JSONLocator(Locator):
776
This locator uses special extended metadata (not available on PyPI) and is
777
the basis of performant dependency resolution in distlib. Other locators
778
require archive downloads before dependencies can be determined! As you
779
might imagine, that can be slow.
781
def get_distribution_names(self):
783
Return all the distribution names known to this locator.
785
raise NotImplementedError('Not available from this locator')
787
def _get_project(self, name):
789
data = get_project_data(name)
791
for info in data.get('files', []):
792
if info['ptype'] != 'sdist' or info['pyversion'] != 'source':
794
dist = make_dist(data['name'], info['version'],
797
md['Download-URL'] = info['url']
798
dist.md5_digest = info.get('digest')
799
md.dependencies = info.get('requirements', {})
800
dist.exports = info.get('exports', {})
801
result[dist.version] = dist
804
class DistPathLocator(Locator):
806
This locator finds installed distributions in a path. It can be useful for
807
adding to an :class:`AggregatingLocator`.
809
def __init__(self, distpath, **kwargs):
811
Initialise an instance.
813
:param distpath: A :class:`DistributionPath` instance to search.
815
super(DistPathLocator, self).__init__(**kwargs)
816
assert isinstance(distpath, DistributionPath)
817
self.distpath = distpath
819
def _get_project(self, name):
820
dist = self.distpath.get_distribution(name)
824
result = { dist.version: dist }
828
class AggregatingLocator(Locator):
830
This class allows you to chain and/or merge a list of locators.
832
def __init__(self, *locators, **kwargs):
834
Initialise an instance.
836
:param locators: The list of locators to search.
837
:param kwargs: Passed to the superclass constructor,
839
* merge - if False (the default), the first successful
840
search from any of the locators is returned. If True,
841
the results from all locators are merged (this can be
844
self.merge = kwargs.pop('merge', False)
845
self.locators = locators
846
super(AggregatingLocator, self).__init__(**kwargs)
848
def clear_cache(self):
849
super(AggregatingLocator, self).clear_cache()
850
for locator in self.locators:
851
locator.clear_cache()
853
def _set_scheme(self, value):
855
for locator in self.locators:
856
locator.scheme = value
858
scheme = property(Locator.scheme.fget, _set_scheme)
860
def _get_project(self, name):
862
for locator in self.locators:
863
r = locator.get_project(name)
872
def get_distribution_names(self):
874
Return all the distribution names known to this locator.
877
for locator in self.locators:
879
result |= locator.get_distribution_names()
880
except NotImplementedError:
885
default_locator = AggregatingLocator(
887
SimpleScrapingLocator('https://pypi.python.org/simple/',
890
locate = default_locator.locate
892
class DependencyFinder(object):
894
Locate dependencies for distributions.
897
def __init__(self, locator=None):
899
Initialise an instance, using the specified locator
900
to locate distributions.
902
self.locator = locator or default_locator
903
self.scheme = get_scheme(self.locator.scheme)
905
def _get_name_and_version(self, p):
907
A utility method used to get name and version from e.g. a Provides-Dist
910
:param p: A value in a form foo (1.0)
911
:return: The name and version as a tuple.
913
comps = p.strip().rsplit(' ', 1)
918
if len(version) < 3 or version[0] != '(' or version[-1] != ')':
919
raise DistlibException('Ill-formed provides field: %r' % p)
920
version = version[1:-1] # trim off parentheses
921
# Name in lower case for case-insensitivity
922
return name.lower(), version
924
def add_distribution(self, dist):
926
Add a distribution to the finder. This will update internal information
927
about who provides what.
928
:param dist: The distribution to add.
930
logger.debug('adding distribution %s', dist)
932
self.dists_by_name[name] = dist
933
self.dists[(name, dist.version)] = dist
934
for p in dist.provides:
935
name, version = self._get_name_and_version(p)
936
logger.debug('Add to provided: %s, %s, %s', name, version, dist)
937
self.provided.setdefault(name, set()).add((version, dist))
939
def remove_distribution(self, dist):
941
Remove a distribution from the finder. This will update internal
942
information about who provides what.
943
:param dist: The distribution to remove.
945
logger.debug('removing distribution %s', dist)
947
del self.dists_by_name[name]
948
del self.dists[(name, dist.version)]
949
for p in dist.provides:
950
name, version = self._get_name_and_version(p)
951
logger.debug('Remove from provided: %s, %s, %s', name, version, dist)
952
s = self.provided[name]
953
s.remove((version, dist))
955
del self.provided[name]
957
def get_matcher(self, reqt):
959
Get a version matcher for a requirement.
960
:param reqt: The requirement
962
:return: A version matcher (an instance of
963
:class:`distlib.version.Matcher`).
966
matcher = self.scheme.matcher(reqt)
967
except UnsupportedVersionError:
968
# XXX compat-mode if cannot read the version
969
name = reqt.split()[0]
970
matcher = self.scheme.matcher(name)
973
def find_providers(self, reqt):
975
Find the distributions which can fulfill a requirement.
977
:param reqt: The requirement.
979
:return: A set of distribution which can fulfill the requirement.
981
matcher = self.get_matcher(reqt)
982
name = matcher.key # case-insensitive
984
provided = self.provided
986
for version, provider in provided[name]:
988
match = matcher.match(version)
989
except UnsupportedVersionError:
997
def try_to_replace(self, provider, other, problems):
999
Attempt to replace one provider with another. This is typically used
1000
when resolving dependencies from multiple sources, e.g. A requires
1001
(B >= 1.0) while C requires (B >= 1.1).
1003
For successful replacement, ``provider`` must meet all the requirements
1004
which ``other`` fulfills.
1006
:param provider: The provider we are trying to replace with.
1007
:param other: The provider we're trying to replace.
1008
:param problems: If False is returned, this will contain what
1009
problems prevented replacement. This is currently
1010
a tuple of the literal string 'cantreplace',
1011
``provider``, ``other`` and the set of requirements
1012
that ``provider`` couldn't fulfill.
1013
:return: True if we can replace ``other`` with ``provider``, else
1016
rlist = self.reqts[other]
1019
matcher = self.get_matcher(s)
1020
if not matcher.match(provider.version):
1023
# can't replace other with provider
1024
problems.add(('cantreplace', provider, other, unmatched))
1027
# can replace other with provider
1028
self.remove_distribution(other)
1029
del self.reqts[other]
1031
self.reqts.setdefault(provider, set()).add(s)
1032
self.add_distribution(provider)
1036
def find(self, requirement, tests=False, prereleases=False):
1038
Find a distribution matching requirement and all distributions
1039
it depends on. Use the ``tests`` argument to determine whether
1040
distributions used only for testing should be included in the
1041
results. Allow ``requirement`` to be either a :class:`Distribution`
1042
instance or a string expressing a requirement. If ``prereleases``
1043
is True, allow pre-release versions to be returned - otherwise,
1046
Return a set of :class:`Distribution` instances and a set of
1049
The distributions returned should be such that they have the
1050
:attr:`required` attribute set to ``True`` if they were
1051
from the ``requirement`` passed to ``find()``, and they have the
1052
:attr:`build_time_dependency` attribute set to ``True`` unless they
1053
are post-installation dependencies of the ``requirement``.
1055
The problems should be a tuple consisting of the string
1056
``'unsatisfied'`` and the requirement which couldn't be satisfied
1057
by any distribution known to the locator.
1062
self.dists_by_name = {}
1065
if isinstance(requirement, Distribution):
1066
dist = odist = requirement
1067
logger.debug('passed %s as requirement', odist)
1069
dist = odist = self.locator.locate(requirement,
1070
prereleases=prereleases)
1072
raise DistlibException('Unable to locate %r' % requirement)
1073
logger.debug('located %s', odist)
1074
dist.requested = True
1077
install_dists = set([odist])
1080
name = dist.key # case-insensitive
1081
if name not in self.dists_by_name:
1082
self.add_distribution(dist)
1084
#import pdb; pdb.set_trace()
1085
other = self.dists_by_name[name]
1087
self.try_to_replace(dist, other, problems)
1089
ireqts = dist.requires
1090
sreqts = dist.setup_requires
1092
if not tests or dist not in install_dists:
1095
treqts = dist.test_requires
1096
all_reqts = ireqts | sreqts | treqts | ereqts
1098
providers = self.find_providers(r)
1100
logger.debug('No providers found for %r', r)
1101
provider = self.locator.locate(r, prereleases=prereleases)
1102
if provider is None:
1103
logger.debug('Cannot satisfy %r', r)
1104
problems.add(('unsatisfied', r))
1106
n, v = provider.key, provider.version
1107
if (n, v) not in self.dists:
1109
providers.add(provider)
1110
if r in ireqts and dist in install_dists:
1111
install_dists.add(provider)
1112
logger.debug('Adding %s to install_dists',
1113
provider.name_and_version)
1116
if name not in self.dists_by_name:
1117
self.reqts.setdefault(p, set()).add(r)
1119
other = self.dists_by_name[name]
1121
# see if other can be replaced by p
1122
self.try_to_replace(p, other, problems)
1124
dists = set(self.dists.values())
1126
dist.build_time_dependency = dist not in install_dists
1127
if dist.build_time_dependency:
1128
logger.debug('%s is a build-time dependency only.',
1129
dist.name_and_version)
1130
logger.debug('find done for %s', odist)
1131
return dists, problems