~anitanayak/charms/trusty/ibm-mq/devel

« back to all changes in this revision

Viewing changes to .tox/py35/lib/python3.5/site-packages/pip/_vendor/distlib/database.py

  • Committer: Anita Nayak
  • Date: 2016-10-24 07:11:28 UTC
  • Revision ID: anitanayak@in.ibm.com-20161024071128-oufsbvyx8x344p2j
checking in after fixing lint errors

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Copyright (C) 2012-2016 The Python Software Foundation.
 
4
# See LICENSE.txt and CONTRIBUTORS.txt.
 
5
#
 
6
"""PEP 376 implementation."""
 
7
 
 
8
from __future__ import unicode_literals
 
9
 
 
10
import base64
 
11
import codecs
 
12
import contextlib
 
13
import hashlib
 
14
import logging
 
15
import os
 
16
import posixpath
 
17
import sys
 
18
import zipimport
 
19
 
 
20
from . import DistlibException, resources
 
21
from .compat import StringIO
 
22
from .version import get_scheme, UnsupportedVersionError
 
23
from .metadata import Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME
 
24
from .util import (parse_requirement, cached_property, parse_name_and_version,
 
25
                   read_exports, write_exports, CSVReader, CSVWriter)
 
26
 
 
27
 
 
28
__all__ = ['Distribution', 'BaseInstalledDistribution',
 
29
           'InstalledDistribution', 'EggInfoDistribution',
 
30
           'DistributionPath']
 
31
 
 
32
 
 
33
logger = logging.getLogger(__name__)
 
34
 
 
35
EXPORTS_FILENAME = 'pydist-exports.json'
 
36
COMMANDS_FILENAME = 'pydist-commands.json'
 
37
 
 
38
DIST_FILES = ('INSTALLER', METADATA_FILENAME, 'RECORD', 'REQUESTED',
 
39
              'RESOURCES', EXPORTS_FILENAME, 'SHARED')
 
40
 
 
41
DISTINFO_EXT = '.dist-info'
 
42
 
 
43
 
 
44
class _Cache(object):
 
45
    """
 
46
    A simple cache mapping names and .dist-info paths to distributions
 
47
    """
 
48
    def __init__(self):
 
49
        """
 
50
        Initialise an instance. There is normally one for each DistributionPath.
 
51
        """
 
52
        self.name = {}
 
53
        self.path = {}
 
54
        self.generated = False
 
55
 
 
56
    def clear(self):
 
57
        """
 
58
        Clear the cache, setting it to its initial state.
 
59
        """
 
60
        self.name.clear()
 
61
        self.path.clear()
 
62
        self.generated = False
 
63
 
 
64
    def add(self, dist):
 
65
        """
 
66
        Add a distribution to the cache.
 
67
        :param dist: The distribution to add.
 
68
        """
 
69
        if dist.path not in self.path:
 
70
            self.path[dist.path] = dist
 
71
            self.name.setdefault(dist.key, []).append(dist)
 
72
 
 
73
 
 
74
class DistributionPath(object):
 
75
    """
 
76
    Represents a set of distributions installed on a path (typically sys.path).
 
77
    """
 
78
    def __init__(self, path=None, include_egg=False):
 
79
        """
 
80
        Create an instance from a path, optionally including legacy (distutils/
 
81
        setuptools/distribute) distributions.
 
82
        :param path: The path to use, as a list of directories. If not specified,
 
83
                     sys.path is used.
 
84
        :param include_egg: If True, this instance will look for and return legacy
 
85
                            distributions as well as those based on PEP 376.
 
86
        """
 
87
        if path is None:
 
88
            path = sys.path
 
89
        self.path = path
 
90
        self._include_dist = True
 
91
        self._include_egg = include_egg
 
92
 
 
93
        self._cache = _Cache()
 
94
        self._cache_egg = _Cache()
 
95
        self._cache_enabled = True
 
96
        self._scheme = get_scheme('default')
 
97
 
 
98
    def _get_cache_enabled(self):
 
99
        return self._cache_enabled
 
100
 
 
101
    def _set_cache_enabled(self, value):
 
102
        self._cache_enabled = value
 
103
 
 
104
    cache_enabled = property(_get_cache_enabled, _set_cache_enabled)
 
105
 
 
106
    def clear_cache(self):
 
107
        """
 
108
        Clears the internal cache.
 
109
        """
 
110
        self._cache.clear()
 
111
        self._cache_egg.clear()
 
112
 
 
113
 
 
114
    def _yield_distributions(self):
 
115
        """
 
116
        Yield .dist-info and/or .egg(-info) distributions.
 
117
        """
 
118
        # We need to check if we've seen some resources already, because on
 
119
        # some Linux systems (e.g. some Debian/Ubuntu variants) there are
 
120
        # symlinks which alias other files in the environment.
 
121
        seen = set()
 
122
        for path in self.path:
 
123
            finder = resources.finder_for_path(path)
 
124
            if finder is None:
 
125
                continue
 
126
            r = finder.find('')
 
127
            if not r or not r.is_container:
 
128
                continue
 
129
            rset = sorted(r.resources)
 
130
            for entry in rset:
 
131
                r = finder.find(entry)
 
132
                if not r or r.path in seen:
 
133
                    continue
 
134
                if self._include_dist and entry.endswith(DISTINFO_EXT):
 
135
                    possible_filenames = [METADATA_FILENAME, WHEEL_METADATA_FILENAME]
 
136
                    for metadata_filename in possible_filenames:
 
137
                        metadata_path = posixpath.join(entry, metadata_filename)
 
138
                        pydist = finder.find(metadata_path)
 
139
                        if pydist:
 
140
                            break
 
141
                    else:
 
142
                        continue
 
143
 
 
144
                    with contextlib.closing(pydist.as_stream()) as stream:
 
145
                        metadata = Metadata(fileobj=stream, scheme='legacy')
 
146
                    logger.debug('Found %s', r.path)
 
147
                    seen.add(r.path)
 
148
                    yield new_dist_class(r.path, metadata=metadata,
 
149
                                         env=self)
 
150
                elif self._include_egg and entry.endswith(('.egg-info',
 
151
                                                          '.egg')):
 
152
                    logger.debug('Found %s', r.path)
 
153
                    seen.add(r.path)
 
154
                    yield old_dist_class(r.path, self)
 
155
 
 
156
    def _generate_cache(self):
 
157
        """
 
158
        Scan the path for distributions and populate the cache with
 
159
        those that are found.
 
160
        """
 
161
        gen_dist = not self._cache.generated
 
162
        gen_egg = self._include_egg and not self._cache_egg.generated
 
163
        if gen_dist or gen_egg:
 
164
            for dist in self._yield_distributions():
 
165
                if isinstance(dist, InstalledDistribution):
 
166
                    self._cache.add(dist)
 
167
                else:
 
168
                    self._cache_egg.add(dist)
 
169
 
 
170
            if gen_dist:
 
171
                self._cache.generated = True
 
172
            if gen_egg:
 
173
                self._cache_egg.generated = True
 
174
 
 
175
    @classmethod
 
176
    def distinfo_dirname(cls, name, version):
 
177
        """
 
178
        The *name* and *version* parameters are converted into their
 
179
        filename-escaped form, i.e. any ``'-'`` characters are replaced
 
180
        with ``'_'`` other than the one in ``'dist-info'`` and the one
 
181
        separating the name from the version number.
 
182
 
 
183
        :parameter name: is converted to a standard distribution name by replacing
 
184
                         any runs of non- alphanumeric characters with a single
 
185
                         ``'-'``.
 
186
        :type name: string
 
187
        :parameter version: is converted to a standard version string. Spaces
 
188
                            become dots, and all other non-alphanumeric characters
 
189
                            (except dots) become dashes, with runs of multiple
 
190
                            dashes condensed to a single dash.
 
191
        :type version: string
 
192
        :returns: directory name
 
193
        :rtype: string"""
 
194
        name = name.replace('-', '_')
 
195
        return '-'.join([name, version]) + DISTINFO_EXT
 
196
 
 
197
    def get_distributions(self):
 
198
        """
 
199
        Provides an iterator that looks for distributions and returns
 
200
        :class:`InstalledDistribution` or
 
201
        :class:`EggInfoDistribution` instances for each one of them.
 
202
 
 
203
        :rtype: iterator of :class:`InstalledDistribution` and
 
204
                :class:`EggInfoDistribution` instances
 
205
        """
 
206
        if not self._cache_enabled:
 
207
            for dist in self._yield_distributions():
 
208
                yield dist
 
209
        else:
 
210
            self._generate_cache()
 
211
 
 
212
            for dist in self._cache.path.values():
 
213
                yield dist
 
214
 
 
215
            if self._include_egg:
 
216
                for dist in self._cache_egg.path.values():
 
217
                    yield dist
 
218
 
 
219
    def get_distribution(self, name):
 
220
        """
 
221
        Looks for a named distribution on the path.
 
222
 
 
223
        This function only returns the first result found, as no more than one
 
224
        value is expected. If nothing is found, ``None`` is returned.
 
225
 
 
226
        :rtype: :class:`InstalledDistribution`, :class:`EggInfoDistribution`
 
227
                or ``None``
 
228
        """
 
229
        result = None
 
230
        name = name.lower()
 
231
        if not self._cache_enabled:
 
232
            for dist in self._yield_distributions():
 
233
                if dist.key == name:
 
234
                    result = dist
 
235
                    break
 
236
        else:
 
237
            self._generate_cache()
 
238
 
 
239
            if name in self._cache.name:
 
240
                result = self._cache.name[name][0]
 
241
            elif self._include_egg and name in self._cache_egg.name:
 
242
                result = self._cache_egg.name[name][0]
 
243
        return result
 
244
 
 
245
    def provides_distribution(self, name, version=None):
 
246
        """
 
247
        Iterates over all distributions to find which distributions provide *name*.
 
248
        If a *version* is provided, it will be used to filter the results.
 
249
 
 
250
        This function only returns the first result found, since no more than
 
251
        one values are expected. If the directory is not found, returns ``None``.
 
252
 
 
253
        :parameter version: a version specifier that indicates the version
 
254
                            required, conforming to the format in ``PEP-345``
 
255
 
 
256
        :type name: string
 
257
        :type version: string
 
258
        """
 
259
        matcher = None
 
260
        if not version is None:
 
261
            try:
 
262
                matcher = self._scheme.matcher('%s (%s)' % (name, version))
 
263
            except ValueError:
 
264
                raise DistlibException('invalid name or version: %r, %r' %
 
265
                                      (name, version))
 
266
 
 
267
        for dist in self.get_distributions():
 
268
            provided = dist.provides
 
269
 
 
270
            for p in provided:
 
271
                p_name, p_ver = parse_name_and_version(p)
 
272
                if matcher is None:
 
273
                    if p_name == name:
 
274
                        yield dist
 
275
                        break
 
276
                else:
 
277
                    if p_name == name and matcher.match(p_ver):
 
278
                        yield dist
 
279
                        break
 
280
 
 
281
    def get_file_path(self, name, relative_path):
 
282
        """
 
283
        Return the path to a resource file.
 
284
        """
 
285
        dist = self.get_distribution(name)
 
286
        if dist is None:
 
287
            raise LookupError('no distribution named %r found' % name)
 
288
        return dist.get_resource_path(relative_path)
 
289
 
 
290
    def get_exported_entries(self, category, name=None):
 
291
        """
 
292
        Return all of the exported entries in a particular category.
 
293
 
 
294
        :param category: The category to search for entries.
 
295
        :param name: If specified, only entries with that name are returned.
 
296
        """
 
297
        for dist in self.get_distributions():
 
298
            r = dist.exports
 
299
            if category in r:
 
300
                d = r[category]
 
301
                if name is not None:
 
302
                    if name in d:
 
303
                        yield d[name]
 
304
                else:
 
305
                    for v in d.values():
 
306
                        yield v
 
307
 
 
308
 
 
309
class Distribution(object):
 
310
    """
 
311
    A base class for distributions, whether installed or from indexes.
 
312
    Either way, it must have some metadata, so that's all that's needed
 
313
    for construction.
 
314
    """
 
315
 
 
316
    build_time_dependency = False
 
317
    """
 
318
    Set to True if it's known to be only a build-time dependency (i.e.
 
319
    not needed after installation).
 
320
    """
 
321
 
 
322
    requested = False
 
323
    """A boolean that indicates whether the ``REQUESTED`` metadata file is
 
324
    present (in other words, whether the package was installed by user
 
325
    request or it was installed as a dependency)."""
 
326
 
 
327
    def __init__(self, metadata):
 
328
        """
 
329
        Initialise an instance.
 
330
        :param metadata: The instance of :class:`Metadata` describing this
 
331
        distribution.
 
332
        """
 
333
        self.metadata = metadata
 
334
        self.name = metadata.name
 
335
        self.key = self.name.lower()    # for case-insensitive comparisons
 
336
        self.version = metadata.version
 
337
        self.locator = None
 
338
        self.digest = None
 
339
        self.extras = None      # additional features requested
 
340
        self.context = None     # environment marker overrides
 
341
        self.download_urls = set()
 
342
        self.digests = {}
 
343
 
 
344
    @property
 
345
    def source_url(self):
 
346
        """
 
347
        The source archive download URL for this distribution.
 
348
        """
 
349
        return self.metadata.source_url
 
350
 
 
351
    download_url = source_url   # Backward compatibility
 
352
 
 
353
    @property
 
354
    def name_and_version(self):
 
355
        """
 
356
        A utility property which displays the name and version in parentheses.
 
357
        """
 
358
        return '%s (%s)' % (self.name, self.version)
 
359
 
 
360
    @property
 
361
    def provides(self):
 
362
        """
 
363
        A set of distribution names and versions provided by this distribution.
 
364
        :return: A set of "name (version)" strings.
 
365
        """
 
366
        plist = self.metadata.provides
 
367
        s = '%s (%s)' % (self.name, self.version)
 
368
        if s not in plist:
 
369
            plist.append(s)
 
370
        return plist
 
371
 
 
372
    def _get_requirements(self, req_attr):
 
373
        md = self.metadata
 
374
        logger.debug('Getting requirements from metadata %r', md.todict())
 
375
        reqts = getattr(md, req_attr)
 
376
        return set(md.get_requirements(reqts, extras=self.extras,
 
377
                                       env=self.context))
 
378
 
 
379
    @property
 
380
    def run_requires(self):
 
381
        return self._get_requirements('run_requires')
 
382
 
 
383
    @property
 
384
    def meta_requires(self):
 
385
        return self._get_requirements('meta_requires')
 
386
 
 
387
    @property
 
388
    def build_requires(self):
 
389
        return self._get_requirements('build_requires')
 
390
 
 
391
    @property
 
392
    def test_requires(self):
 
393
        return self._get_requirements('test_requires')
 
394
 
 
395
    @property
 
396
    def dev_requires(self):
 
397
        return self._get_requirements('dev_requires')
 
398
 
 
399
    def matches_requirement(self, req):
 
400
        """
 
401
        Say if this instance matches (fulfills) a requirement.
 
402
        :param req: The requirement to match.
 
403
        :rtype req: str
 
404
        :return: True if it matches, else False.
 
405
        """
 
406
        # Requirement may contain extras - parse to lose those
 
407
        # from what's passed to the matcher
 
408
        r = parse_requirement(req)
 
409
        scheme = get_scheme(self.metadata.scheme)
 
410
        try:
 
411
            matcher = scheme.matcher(r.requirement)
 
412
        except UnsupportedVersionError:
 
413
            # XXX compat-mode if cannot read the version
 
414
            logger.warning('could not read version %r - using name only',
 
415
                           req)
 
416
            name = req.split()[0]
 
417
            matcher = scheme.matcher(name)
 
418
 
 
419
        name = matcher.key   # case-insensitive
 
420
 
 
421
        result = False
 
422
        for p in self.provides:
 
423
            p_name, p_ver = parse_name_and_version(p)
 
424
            if p_name != name:
 
425
                continue
 
426
            try:
 
427
                result = matcher.match(p_ver)
 
428
                break
 
429
            except UnsupportedVersionError:
 
430
                pass
 
431
        return result
 
432
 
 
433
    def __repr__(self):
 
434
        """
 
435
        Return a textual representation of this instance,
 
436
        """
 
437
        if self.source_url:
 
438
            suffix = ' [%s]' % self.source_url
 
439
        else:
 
440
            suffix = ''
 
441
        return '<Distribution %s (%s)%s>' % (self.name, self.version, suffix)
 
442
 
 
443
    def __eq__(self, other):
 
444
        """
 
445
        See if this distribution is the same as another.
 
446
        :param other: The distribution to compare with. To be equal to one
 
447
                      another. distributions must have the same type, name,
 
448
                      version and source_url.
 
449
        :return: True if it is the same, else False.
 
450
        """
 
451
        if type(other) is not type(self):
 
452
            result = False
 
453
        else:
 
454
            result = (self.name == other.name and
 
455
                      self.version == other.version and
 
456
                      self.source_url == other.source_url)
 
457
        return result
 
458
 
 
459
    def __hash__(self):
 
460
        """
 
461
        Compute hash in a way which matches the equality test.
 
462
        """
 
463
        return hash(self.name) + hash(self.version) + hash(self.source_url)
 
464
 
 
465
 
 
466
class BaseInstalledDistribution(Distribution):
 
467
    """
 
468
    This is the base class for installed distributions (whether PEP 376 or
 
469
    legacy).
 
470
    """
 
471
 
 
472
    hasher = None
 
473
 
 
474
    def __init__(self, metadata, path, env=None):
 
475
        """
 
476
        Initialise an instance.
 
477
        :param metadata: An instance of :class:`Metadata` which describes the
 
478
                         distribution. This will normally have been initialised
 
479
                         from a metadata file in the ``path``.
 
480
        :param path:     The path of the ``.dist-info`` or ``.egg-info``
 
481
                         directory for the distribution.
 
482
        :param env:      This is normally the :class:`DistributionPath`
 
483
                         instance where this distribution was found.
 
484
        """
 
485
        super(BaseInstalledDistribution, self).__init__(metadata)
 
486
        self.path = path
 
487
        self.dist_path = env
 
488
 
 
489
    def get_hash(self, data, hasher=None):
 
490
        """
 
491
        Get the hash of some data, using a particular hash algorithm, if
 
492
        specified.
 
493
 
 
494
        :param data: The data to be hashed.
 
495
        :type data: bytes
 
496
        :param hasher: The name of a hash implementation, supported by hashlib,
 
497
                       or ``None``. Examples of valid values are ``'sha1'``,
 
498
                       ``'sha224'``, ``'sha384'``, '``sha256'``, ``'md5'`` and
 
499
                       ``'sha512'``. If no hasher is specified, the ``hasher``
 
500
                       attribute of the :class:`InstalledDistribution` instance
 
501
                       is used. If the hasher is determined to be ``None``, MD5
 
502
                       is used as the hashing algorithm.
 
503
        :returns: The hash of the data. If a hasher was explicitly specified,
 
504
                  the returned hash will be prefixed with the specified hasher
 
505
                  followed by '='.
 
506
        :rtype: str
 
507
        """
 
508
        if hasher is None:
 
509
            hasher = self.hasher
 
510
        if hasher is None:
 
511
            hasher = hashlib.md5
 
512
            prefix = ''
 
513
        else:
 
514
            hasher = getattr(hashlib, hasher)
 
515
            prefix = '%s=' % self.hasher
 
516
        digest = hasher(data).digest()
 
517
        digest = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
 
518
        return '%s%s' % (prefix, digest)
 
519
 
 
520
 
 
521
class InstalledDistribution(BaseInstalledDistribution):
 
522
    """
 
523
    Created with the *path* of the ``.dist-info`` directory provided to the
 
524
    constructor. It reads the metadata contained in ``pydist.json`` when it is
 
525
    instantiated., or uses a passed in Metadata instance (useful for when
 
526
    dry-run mode is being used).
 
527
    """
 
528
 
 
529
    hasher = 'sha256'
 
530
 
 
531
    def __init__(self, path, metadata=None, env=None):
 
532
        self.finder = finder = resources.finder_for_path(path)
 
533
        if finder is None:
 
534
            import pdb; pdb.set_trace ()
 
535
        if env and env._cache_enabled and path in env._cache.path:
 
536
            metadata = env._cache.path[path].metadata
 
537
        elif metadata is None:
 
538
            r = finder.find(METADATA_FILENAME)
 
539
            # Temporary - for Wheel 0.23 support
 
540
            if r is None:
 
541
                r = finder.find(WHEEL_METADATA_FILENAME)
 
542
            # Temporary - for legacy support
 
543
            if r is None:
 
544
                r = finder.find('METADATA')
 
545
            if r is None:
 
546
                raise ValueError('no %s found in %s' % (METADATA_FILENAME,
 
547
                                                        path))
 
548
            with contextlib.closing(r.as_stream()) as stream:
 
549
                metadata = Metadata(fileobj=stream, scheme='legacy')
 
550
 
 
551
        super(InstalledDistribution, self).__init__(metadata, path, env)
 
552
 
 
553
        if env and env._cache_enabled:
 
554
            env._cache.add(self)
 
555
 
 
556
        try:
 
557
            r = finder.find('REQUESTED')
 
558
        except AttributeError:
 
559
            import pdb; pdb.set_trace ()
 
560
        self.requested = r is not None
 
561
 
 
562
    def __repr__(self):
 
563
        return '<InstalledDistribution %r %s at %r>' % (
 
564
            self.name, self.version, self.path)
 
565
 
 
566
    def __str__(self):
 
567
        return "%s %s" % (self.name, self.version)
 
568
 
 
569
    def _get_records(self):
 
570
        """
 
571
        Get the list of installed files for the distribution
 
572
        :return: A list of tuples of path, hash and size. Note that hash and
 
573
                 size might be ``None`` for some entries. The path is exactly
 
574
                 as stored in the file (which is as in PEP 376).
 
575
        """
 
576
        results = []
 
577
        r = self.get_distinfo_resource('RECORD')
 
578
        with contextlib.closing(r.as_stream()) as stream:
 
579
            with CSVReader(stream=stream) as record_reader:
 
580
                # Base location is parent dir of .dist-info dir
 
581
                #base_location = os.path.dirname(self.path)
 
582
                #base_location = os.path.abspath(base_location)
 
583
                for row in record_reader:
 
584
                    missing = [None for i in range(len(row), 3)]
 
585
                    path, checksum, size = row + missing
 
586
                    #if not os.path.isabs(path):
 
587
                    #    path = path.replace('/', os.sep)
 
588
                    #    path = os.path.join(base_location, path)
 
589
                    results.append((path, checksum, size))
 
590
        return results
 
591
 
 
592
    @cached_property
 
593
    def exports(self):
 
594
        """
 
595
        Return the information exported by this distribution.
 
596
        :return: A dictionary of exports, mapping an export category to a dict
 
597
                 of :class:`ExportEntry` instances describing the individual
 
598
                 export entries, and keyed by name.
 
599
        """
 
600
        result = {}
 
601
        r = self.get_distinfo_resource(EXPORTS_FILENAME)
 
602
        if r:
 
603
            result = self.read_exports()
 
604
        return result
 
605
 
 
606
    def read_exports(self):
 
607
        """
 
608
        Read exports data from a file in .ini format.
 
609
 
 
610
        :return: A dictionary of exports, mapping an export category to a list
 
611
                 of :class:`ExportEntry` instances describing the individual
 
612
                 export entries.
 
613
        """
 
614
        result = {}
 
615
        r = self.get_distinfo_resource(EXPORTS_FILENAME)
 
616
        if r:
 
617
            with contextlib.closing(r.as_stream()) as stream:
 
618
                result = read_exports(stream)
 
619
        return result
 
620
 
 
621
    def write_exports(self, exports):
 
622
        """
 
623
        Write a dictionary of exports to a file in .ini format.
 
624
        :param exports: A dictionary of exports, mapping an export category to
 
625
                        a list of :class:`ExportEntry` instances describing the
 
626
                        individual export entries.
 
627
        """
 
628
        rf = self.get_distinfo_file(EXPORTS_FILENAME)
 
629
        with open(rf, 'w') as f:
 
630
            write_exports(exports, f)
 
631
 
 
632
    def get_resource_path(self, relative_path):
 
633
        """
 
634
        NOTE: This API may change in the future.
 
635
 
 
636
        Return the absolute path to a resource file with the given relative
 
637
        path.
 
638
 
 
639
        :param relative_path: The path, relative to .dist-info, of the resource
 
640
                              of interest.
 
641
        :return: The absolute path where the resource is to be found.
 
642
        """
 
643
        r = self.get_distinfo_resource('RESOURCES')
 
644
        with contextlib.closing(r.as_stream()) as stream:
 
645
            with CSVReader(stream=stream) as resources_reader:
 
646
                for relative, destination in resources_reader:
 
647
                    if relative == relative_path:
 
648
                        return destination
 
649
        raise KeyError('no resource file with relative path %r '
 
650
                       'is installed' % relative_path)
 
651
 
 
652
    def list_installed_files(self):
 
653
        """
 
654
        Iterates over the ``RECORD`` entries and returns a tuple
 
655
        ``(path, hash, size)`` for each line.
 
656
 
 
657
        :returns: iterator of (path, hash, size)
 
658
        """
 
659
        for result in self._get_records():
 
660
            yield result
 
661
 
 
662
    def write_installed_files(self, paths, prefix, dry_run=False):
 
663
        """
 
664
        Writes the ``RECORD`` file, using the ``paths`` iterable passed in. Any
 
665
        existing ``RECORD`` file is silently overwritten.
 
666
 
 
667
        prefix is used to determine when to write absolute paths.
 
668
        """
 
669
        prefix = os.path.join(prefix, '')
 
670
        base = os.path.dirname(self.path)
 
671
        base_under_prefix = base.startswith(prefix)
 
672
        base = os.path.join(base, '')
 
673
        record_path = self.get_distinfo_file('RECORD')
 
674
        logger.info('creating %s', record_path)
 
675
        if dry_run:
 
676
            return None
 
677
        with CSVWriter(record_path) as writer:
 
678
            for path in paths:
 
679
                if os.path.isdir(path) or path.endswith(('.pyc', '.pyo')):
 
680
                    # do not put size and hash, as in PEP-376
 
681
                    hash_value = size = ''
 
682
                else:
 
683
                    size = '%d' % os.path.getsize(path)
 
684
                    with open(path, 'rb') as fp:
 
685
                        hash_value = self.get_hash(fp.read())
 
686
                if path.startswith(base) or (base_under_prefix and
 
687
                                             path.startswith(prefix)):
 
688
                    path = os.path.relpath(path, base)
 
689
                writer.writerow((path, hash_value, size))
 
690
 
 
691
            # add the RECORD file itself
 
692
            if record_path.startswith(base):
 
693
                record_path = os.path.relpath(record_path, base)
 
694
            writer.writerow((record_path, '', ''))
 
695
        return record_path
 
696
 
 
697
    def check_installed_files(self):
 
698
        """
 
699
        Checks that the hashes and sizes of the files in ``RECORD`` are
 
700
        matched by the files themselves. Returns a (possibly empty) list of
 
701
        mismatches. Each entry in the mismatch list will be a tuple consisting
 
702
        of the path, 'exists', 'size' or 'hash' according to what didn't match
 
703
        (existence is checked first, then size, then hash), the expected
 
704
        value and the actual value.
 
705
        """
 
706
        mismatches = []
 
707
        base = os.path.dirname(self.path)
 
708
        record_path = self.get_distinfo_file('RECORD')
 
709
        for path, hash_value, size in self.list_installed_files():
 
710
            if not os.path.isabs(path):
 
711
                path = os.path.join(base, path)
 
712
            if path == record_path:
 
713
                continue
 
714
            if not os.path.exists(path):
 
715
                mismatches.append((path, 'exists', True, False))
 
716
            elif os.path.isfile(path):
 
717
                actual_size = str(os.path.getsize(path))
 
718
                if size and actual_size != size:
 
719
                    mismatches.append((path, 'size', size, actual_size))
 
720
                elif hash_value:
 
721
                    if '=' in hash_value:
 
722
                        hasher = hash_value.split('=', 1)[0]
 
723
                    else:
 
724
                        hasher = None
 
725
 
 
726
                    with open(path, 'rb') as f:
 
727
                        actual_hash = self.get_hash(f.read(), hasher)
 
728
                        if actual_hash != hash_value:
 
729
                            mismatches.append((path, 'hash', hash_value, actual_hash))
 
730
        return mismatches
 
731
 
 
732
    @cached_property
 
733
    def shared_locations(self):
 
734
        """
 
735
        A dictionary of shared locations whose keys are in the set 'prefix',
 
736
        'purelib', 'platlib', 'scripts', 'headers', 'data' and 'namespace'.
 
737
        The corresponding value is the absolute path of that category for
 
738
        this distribution, and takes into account any paths selected by the
 
739
        user at installation time (e.g. via command-line arguments). In the
 
740
        case of the 'namespace' key, this would be a list of absolute paths
 
741
        for the roots of namespace packages in this distribution.
 
742
 
 
743
        The first time this property is accessed, the relevant information is
 
744
        read from the SHARED file in the .dist-info directory.
 
745
        """
 
746
        result = {}
 
747
        shared_path = os.path.join(self.path, 'SHARED')
 
748
        if os.path.isfile(shared_path):
 
749
            with codecs.open(shared_path, 'r', encoding='utf-8') as f:
 
750
                lines = f.read().splitlines()
 
751
            for line in lines:
 
752
                key, value = line.split('=', 1)
 
753
                if key == 'namespace':
 
754
                    result.setdefault(key, []).append(value)
 
755
                else:
 
756
                    result[key] = value
 
757
        return result
 
758
 
 
759
    def write_shared_locations(self, paths, dry_run=False):
 
760
        """
 
761
        Write shared location information to the SHARED file in .dist-info.
 
762
        :param paths: A dictionary as described in the documentation for
 
763
        :meth:`shared_locations`.
 
764
        :param dry_run: If True, the action is logged but no file is actually
 
765
                        written.
 
766
        :return: The path of the file written to.
 
767
        """
 
768
        shared_path = os.path.join(self.path, 'SHARED')
 
769
        logger.info('creating %s', shared_path)
 
770
        if dry_run:
 
771
            return None
 
772
        lines = []
 
773
        for key in ('prefix', 'lib', 'headers', 'scripts', 'data'):
 
774
            path = paths[key]
 
775
            if os.path.isdir(paths[key]):
 
776
                lines.append('%s=%s' % (key,  path))
 
777
        for ns in paths.get('namespace', ()):
 
778
            lines.append('namespace=%s' % ns)
 
779
 
 
780
        with codecs.open(shared_path, 'w', encoding='utf-8') as f:
 
781
            f.write('\n'.join(lines))
 
782
        return shared_path
 
783
 
 
784
    def get_distinfo_resource(self, path):
 
785
        if path not in DIST_FILES:
 
786
            raise DistlibException('invalid path for a dist-info file: '
 
787
                                   '%r at %r' % (path, self.path))
 
788
        finder = resources.finder_for_path(self.path)
 
789
        if finder is None:
 
790
            raise DistlibException('Unable to get a finder for %s' % self.path)
 
791
        return finder.find(path)
 
792
 
 
793
    def get_distinfo_file(self, path):
 
794
        """
 
795
        Returns a path located under the ``.dist-info`` directory. Returns a
 
796
        string representing the path.
 
797
 
 
798
        :parameter path: a ``'/'``-separated path relative to the
 
799
                         ``.dist-info`` directory or an absolute path;
 
800
                         If *path* is an absolute path and doesn't start
 
801
                         with the ``.dist-info`` directory path,
 
802
                         a :class:`DistlibException` is raised
 
803
        :type path: str
 
804
        :rtype: str
 
805
        """
 
806
        # Check if it is an absolute path  # XXX use relpath, add tests
 
807
        if path.find(os.sep) >= 0:
 
808
            # it's an absolute path?
 
809
            distinfo_dirname, path = path.split(os.sep)[-2:]
 
810
            if distinfo_dirname != self.path.split(os.sep)[-1]:
 
811
                raise DistlibException(
 
812
                    'dist-info file %r does not belong to the %r %s '
 
813
                    'distribution' % (path, self.name, self.version))
 
814
 
 
815
        # The file must be relative
 
816
        if path not in DIST_FILES:
 
817
            raise DistlibException('invalid path for a dist-info file: '
 
818
                                   '%r at %r' % (path, self.path))
 
819
 
 
820
        return os.path.join(self.path, path)
 
821
 
 
822
    def list_distinfo_files(self):
 
823
        """
 
824
        Iterates over the ``RECORD`` entries and returns paths for each line if
 
825
        the path is pointing to a file located in the ``.dist-info`` directory
 
826
        or one of its subdirectories.
 
827
 
 
828
        :returns: iterator of paths
 
829
        """
 
830
        base = os.path.dirname(self.path)
 
831
        for path, checksum, size in self._get_records():
 
832
            # XXX add separator or use real relpath algo
 
833
            if not os.path.isabs(path):
 
834
                path = os.path.join(base, path)
 
835
            if path.startswith(self.path):
 
836
                yield path
 
837
 
 
838
    def __eq__(self, other):
 
839
        return (isinstance(other, InstalledDistribution) and
 
840
                self.path == other.path)
 
841
 
 
842
    # See http://docs.python.org/reference/datamodel#object.__hash__
 
843
    __hash__ = object.__hash__
 
844
 
 
845
 
 
846
class EggInfoDistribution(BaseInstalledDistribution):
 
847
    """Created with the *path* of the ``.egg-info`` directory or file provided
 
848
    to the constructor. It reads the metadata contained in the file itself, or
 
849
    if the given path happens to be a directory, the metadata is read from the
 
850
    file ``PKG-INFO`` under that directory."""
 
851
 
 
852
    requested = True    # as we have no way of knowing, assume it was
 
853
    shared_locations = {}
 
854
 
 
855
    def __init__(self, path, env=None):
 
856
        def set_name_and_version(s, n, v):
 
857
            s.name = n
 
858
            s.key = n.lower()   # for case-insensitive comparisons
 
859
            s.version = v
 
860
 
 
861
        self.path = path
 
862
        self.dist_path = env
 
863
        if env and env._cache_enabled and path in env._cache_egg.path:
 
864
            metadata = env._cache_egg.path[path].metadata
 
865
            set_name_and_version(self, metadata.name, metadata.version)
 
866
        else:
 
867
            metadata = self._get_metadata(path)
 
868
 
 
869
            # Need to be set before caching
 
870
            set_name_and_version(self, metadata.name, metadata.version)
 
871
 
 
872
            if env and env._cache_enabled:
 
873
                env._cache_egg.add(self)
 
874
        super(EggInfoDistribution, self).__init__(metadata, path, env)
 
875
 
 
876
    def _get_metadata(self, path):
 
877
        requires = None
 
878
 
 
879
        def parse_requires_data(data):
 
880
            """Create a list of dependencies from a requires.txt file.
 
881
 
 
882
            *data*: the contents of a setuptools-produced requires.txt file.
 
883
            """
 
884
            reqs = []
 
885
            lines = data.splitlines()
 
886
            for line in lines:
 
887
                line = line.strip()
 
888
                if line.startswith('['):
 
889
                    logger.warning('Unexpected line: quitting requirement scan: %r',
 
890
                                   line)
 
891
                    break
 
892
                r = parse_requirement(line)
 
893
                if not r:
 
894
                    logger.warning('Not recognised as a requirement: %r', line)
 
895
                    continue
 
896
                if r.extras:
 
897
                    logger.warning('extra requirements in requires.txt are '
 
898
                                   'not supported')
 
899
                if not r.constraints:
 
900
                    reqs.append(r.name)
 
901
                else:
 
902
                    cons = ', '.join('%s%s' % c for c in r.constraints)
 
903
                    reqs.append('%s (%s)' % (r.name, cons))
 
904
            return reqs
 
905
 
 
906
        def parse_requires_path(req_path):
 
907
            """Create a list of dependencies from a requires.txt file.
 
908
 
 
909
            *req_path*: the path to a setuptools-produced requires.txt file.
 
910
            """
 
911
 
 
912
            reqs = []
 
913
            try:
 
914
                with codecs.open(req_path, 'r', 'utf-8') as fp:
 
915
                    reqs = parse_requires_data(fp.read())
 
916
            except IOError:
 
917
                pass
 
918
            return reqs
 
919
 
 
920
        if path.endswith('.egg'):
 
921
            if os.path.isdir(path):
 
922
                meta_path = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
 
923
                metadata = Metadata(path=meta_path, scheme='legacy')
 
924
                req_path = os.path.join(path, 'EGG-INFO', 'requires.txt')
 
925
                requires = parse_requires_path(req_path)
 
926
            else:
 
927
                # FIXME handle the case where zipfile is not available
 
928
                zipf = zipimport.zipimporter(path)
 
929
                fileobj = StringIO(
 
930
                    zipf.get_data('EGG-INFO/PKG-INFO').decode('utf8'))
 
931
                metadata = Metadata(fileobj=fileobj, scheme='legacy')
 
932
                try:
 
933
                    data = zipf.get_data('EGG-INFO/requires.txt')
 
934
                    requires = parse_requires_data(data.decode('utf-8'))
 
935
                except IOError:
 
936
                    requires = None
 
937
        elif path.endswith('.egg-info'):
 
938
            if os.path.isdir(path):
 
939
                req_path = os.path.join(path, 'requires.txt')
 
940
                requires = parse_requires_path(req_path)
 
941
                path = os.path.join(path, 'PKG-INFO')
 
942
            metadata = Metadata(path=path, scheme='legacy')
 
943
        else:
 
944
            raise DistlibException('path must end with .egg-info or .egg, '
 
945
                                   'got %r' % path)
 
946
 
 
947
        if requires:
 
948
            metadata.add_requirements(requires)
 
949
        return metadata
 
950
 
 
951
    def __repr__(self):
 
952
        return '<EggInfoDistribution %r %s at %r>' % (
 
953
            self.name, self.version, self.path)
 
954
 
 
955
    def __str__(self):
 
956
        return "%s %s" % (self.name, self.version)
 
957
 
 
958
    def check_installed_files(self):
 
959
        """
 
960
        Checks that the hashes and sizes of the files in ``RECORD`` are
 
961
        matched by the files themselves. Returns a (possibly empty) list of
 
962
        mismatches. Each entry in the mismatch list will be a tuple consisting
 
963
        of the path, 'exists', 'size' or 'hash' according to what didn't match
 
964
        (existence is checked first, then size, then hash), the expected
 
965
        value and the actual value.
 
966
        """
 
967
        mismatches = []
 
968
        record_path = os.path.join(self.path, 'installed-files.txt')
 
969
        if os.path.exists(record_path):
 
970
            for path, _, _ in self.list_installed_files():
 
971
                if path == record_path:
 
972
                    continue
 
973
                if not os.path.exists(path):
 
974
                    mismatches.append((path, 'exists', True, False))
 
975
        return mismatches
 
976
 
 
977
    def list_installed_files(self):
 
978
        """
 
979
        Iterates over the ``installed-files.txt`` entries and returns a tuple
 
980
        ``(path, hash, size)`` for each line.
 
981
 
 
982
        :returns: a list of (path, hash, size)
 
983
        """
 
984
 
 
985
        def _md5(path):
 
986
            f = open(path, 'rb')
 
987
            try:
 
988
                content = f.read()
 
989
            finally:
 
990
                f.close()
 
991
            return hashlib.md5(content).hexdigest()
 
992
 
 
993
        def _size(path):
 
994
            return os.stat(path).st_size
 
995
 
 
996
        record_path = os.path.join(self.path, 'installed-files.txt')
 
997
        result = []
 
998
        if os.path.exists(record_path):
 
999
            with codecs.open(record_path, 'r', encoding='utf-8') as f:
 
1000
                for line in f:
 
1001
                    line = line.strip()
 
1002
                    p = os.path.normpath(os.path.join(self.path, line))
 
1003
                    # "./" is present as a marker between installed files
 
1004
                    # and installation metadata files
 
1005
                    if not os.path.exists(p):
 
1006
                        logger.warning('Non-existent file: %s', p)
 
1007
                        if p.endswith(('.pyc', '.pyo')):
 
1008
                            continue
 
1009
                        #otherwise fall through and fail
 
1010
                    if not os.path.isdir(p):
 
1011
                        result.append((p, _md5(p), _size(p)))
 
1012
            result.append((record_path, None, None))
 
1013
        return result
 
1014
 
 
1015
    def list_distinfo_files(self, absolute=False):
 
1016
        """
 
1017
        Iterates over the ``installed-files.txt`` entries and returns paths for
 
1018
        each line if the path is pointing to a file located in the
 
1019
        ``.egg-info`` directory or one of its subdirectories.
 
1020
 
 
1021
        :parameter absolute: If *absolute* is ``True``, each returned path is
 
1022
                          transformed into a local absolute path. Otherwise the
 
1023
                          raw value from ``installed-files.txt`` is returned.
 
1024
        :type absolute: boolean
 
1025
        :returns: iterator of paths
 
1026
        """
 
1027
        record_path = os.path.join(self.path, 'installed-files.txt')
 
1028
        skip = True
 
1029
        with codecs.open(record_path, 'r', encoding='utf-8') as f:
 
1030
            for line in f:
 
1031
                line = line.strip()
 
1032
                if line == './':
 
1033
                    skip = False
 
1034
                    continue
 
1035
                if not skip:
 
1036
                    p = os.path.normpath(os.path.join(self.path, line))
 
1037
                    if p.startswith(self.path):
 
1038
                        if absolute:
 
1039
                            yield p
 
1040
                        else:
 
1041
                            yield line
 
1042
 
 
1043
    def __eq__(self, other):
 
1044
        return (isinstance(other, EggInfoDistribution) and
 
1045
                self.path == other.path)
 
1046
 
 
1047
    # See http://docs.python.org/reference/datamodel#object.__hash__
 
1048
    __hash__ = object.__hash__
 
1049
 
 
1050
new_dist_class = InstalledDistribution
 
1051
old_dist_class = EggInfoDistribution
 
1052
 
 
1053
 
 
1054
class DependencyGraph(object):
 
1055
    """
 
1056
    Represents a dependency graph between distributions.
 
1057
 
 
1058
    The dependency relationships are stored in an ``adjacency_list`` that maps
 
1059
    distributions to a list of ``(other, label)`` tuples where  ``other``
 
1060
    is a distribution and the edge is labeled with ``label`` (i.e. the version
 
1061
    specifier, if such was provided). Also, for more efficient traversal, for
 
1062
    every distribution ``x``, a list of predecessors is kept in
 
1063
    ``reverse_list[x]``. An edge from distribution ``a`` to
 
1064
    distribution ``b`` means that ``a`` depends on ``b``. If any missing
 
1065
    dependencies are found, they are stored in ``missing``, which is a
 
1066
    dictionary that maps distributions to a list of requirements that were not
 
1067
    provided by any other distributions.
 
1068
    """
 
1069
 
 
1070
    def __init__(self):
 
1071
        self.adjacency_list = {}
 
1072
        self.reverse_list = {}
 
1073
        self.missing = {}
 
1074
 
 
1075
    def add_distribution(self, distribution):
 
1076
        """Add the *distribution* to the graph.
 
1077
 
 
1078
        :type distribution: :class:`distutils2.database.InstalledDistribution`
 
1079
                            or :class:`distutils2.database.EggInfoDistribution`
 
1080
        """
 
1081
        self.adjacency_list[distribution] = []
 
1082
        self.reverse_list[distribution] = []
 
1083
        #self.missing[distribution] = []
 
1084
 
 
1085
    def add_edge(self, x, y, label=None):
 
1086
        """Add an edge from distribution *x* to distribution *y* with the given
 
1087
        *label*.
 
1088
 
 
1089
        :type x: :class:`distutils2.database.InstalledDistribution` or
 
1090
                 :class:`distutils2.database.EggInfoDistribution`
 
1091
        :type y: :class:`distutils2.database.InstalledDistribution` or
 
1092
                 :class:`distutils2.database.EggInfoDistribution`
 
1093
        :type label: ``str`` or ``None``
 
1094
        """
 
1095
        self.adjacency_list[x].append((y, label))
 
1096
        # multiple edges are allowed, so be careful
 
1097
        if x not in self.reverse_list[y]:
 
1098
            self.reverse_list[y].append(x)
 
1099
 
 
1100
    def add_missing(self, distribution, requirement):
 
1101
        """
 
1102
        Add a missing *requirement* for the given *distribution*.
 
1103
 
 
1104
        :type distribution: :class:`distutils2.database.InstalledDistribution`
 
1105
                            or :class:`distutils2.database.EggInfoDistribution`
 
1106
        :type requirement: ``str``
 
1107
        """
 
1108
        logger.debug('%s missing %r', distribution, requirement)
 
1109
        self.missing.setdefault(distribution, []).append(requirement)
 
1110
 
 
1111
    def _repr_dist(self, dist):
 
1112
        return '%s %s' % (dist.name, dist.version)
 
1113
 
 
1114
    def repr_node(self, dist, level=1):
 
1115
        """Prints only a subgraph"""
 
1116
        output = [self._repr_dist(dist)]
 
1117
        for other, label in self.adjacency_list[dist]:
 
1118
            dist = self._repr_dist(other)
 
1119
            if label is not None:
 
1120
                dist = '%s [%s]' % (dist, label)
 
1121
            output.append('    ' * level + str(dist))
 
1122
            suboutput = self.repr_node(other, level + 1)
 
1123
            subs = suboutput.split('\n')
 
1124
            output.extend(subs[1:])
 
1125
        return '\n'.join(output)
 
1126
 
 
1127
    def to_dot(self, f, skip_disconnected=True):
 
1128
        """Writes a DOT output for the graph to the provided file *f*.
 
1129
 
 
1130
        If *skip_disconnected* is set to ``True``, then all distributions
 
1131
        that are not dependent on any other distribution are skipped.
 
1132
 
 
1133
        :type f: has to support ``file``-like operations
 
1134
        :type skip_disconnected: ``bool``
 
1135
        """
 
1136
        disconnected = []
 
1137
 
 
1138
        f.write("digraph dependencies {\n")
 
1139
        for dist, adjs in self.adjacency_list.items():
 
1140
            if len(adjs) == 0 and not skip_disconnected:
 
1141
                disconnected.append(dist)
 
1142
            for other, label in adjs:
 
1143
                if not label is None:
 
1144
                    f.write('"%s" -> "%s" [label="%s"]\n' %
 
1145
                            (dist.name, other.name, label))
 
1146
                else:
 
1147
                    f.write('"%s" -> "%s"\n' % (dist.name, other.name))
 
1148
        if not skip_disconnected and len(disconnected) > 0:
 
1149
            f.write('subgraph disconnected {\n')
 
1150
            f.write('label = "Disconnected"\n')
 
1151
            f.write('bgcolor = red\n')
 
1152
 
 
1153
            for dist in disconnected:
 
1154
                f.write('"%s"' % dist.name)
 
1155
                f.write('\n')
 
1156
            f.write('}\n')
 
1157
        f.write('}\n')
 
1158
 
 
1159
    def topological_sort(self):
 
1160
        """
 
1161
        Perform a topological sort of the graph.
 
1162
        :return: A tuple, the first element of which is a topologically sorted
 
1163
                 list of distributions, and the second element of which is a
 
1164
                 list of distributions that cannot be sorted because they have
 
1165
                 circular dependencies and so form a cycle.
 
1166
        """
 
1167
        result = []
 
1168
        # Make a shallow copy of the adjacency list
 
1169
        alist = {}
 
1170
        for k, v in self.adjacency_list.items():
 
1171
            alist[k] = v[:]
 
1172
        while True:
 
1173
            # See what we can remove in this run
 
1174
            to_remove = []
 
1175
            for k, v in list(alist.items())[:]:
 
1176
                if not v:
 
1177
                    to_remove.append(k)
 
1178
                    del alist[k]
 
1179
            if not to_remove:
 
1180
                # What's left in alist (if anything) is a cycle.
 
1181
                break
 
1182
            # Remove from the adjacency list of others
 
1183
            for k, v in alist.items():
 
1184
                alist[k] = [(d, r) for d, r in v if d not in to_remove]
 
1185
            logger.debug('Moving to result: %s',
 
1186
                         ['%s (%s)' % (d.name, d.version) for d in to_remove])
 
1187
            result.extend(to_remove)
 
1188
        return result, list(alist.keys())
 
1189
 
 
1190
    def __repr__(self):
 
1191
        """Representation of the graph"""
 
1192
        output = []
 
1193
        for dist, adjs in self.adjacency_list.items():
 
1194
            output.append(self.repr_node(dist))
 
1195
        return '\n'.join(output)
 
1196
 
 
1197
 
 
1198
def make_graph(dists, scheme='default'):
 
1199
    """Makes a dependency graph from the given distributions.
 
1200
 
 
1201
    :parameter dists: a list of distributions
 
1202
    :type dists: list of :class:`distutils2.database.InstalledDistribution` and
 
1203
                 :class:`distutils2.database.EggInfoDistribution` instances
 
1204
    :rtype: a :class:`DependencyGraph` instance
 
1205
    """
 
1206
    scheme = get_scheme(scheme)
 
1207
    graph = DependencyGraph()
 
1208
    provided = {}  # maps names to lists of (version, dist) tuples
 
1209
 
 
1210
    # first, build the graph and find out what's provided
 
1211
    for dist in dists:
 
1212
        graph.add_distribution(dist)
 
1213
 
 
1214
        for p in dist.provides:
 
1215
            name, version = parse_name_and_version(p)
 
1216
            logger.debug('Add to provided: %s, %s, %s', name, version, dist)
 
1217
            provided.setdefault(name, []).append((version, dist))
 
1218
 
 
1219
    # now make the edges
 
1220
    for dist in dists:
 
1221
        requires = (dist.run_requires | dist.meta_requires |
 
1222
                    dist.build_requires | dist.dev_requires)
 
1223
        for req in requires:
 
1224
            try:
 
1225
                matcher = scheme.matcher(req)
 
1226
            except UnsupportedVersionError:
 
1227
                # XXX compat-mode if cannot read the version
 
1228
                logger.warning('could not read version %r - using name only',
 
1229
                               req)
 
1230
                name = req.split()[0]
 
1231
                matcher = scheme.matcher(name)
 
1232
 
 
1233
            name = matcher.key   # case-insensitive
 
1234
 
 
1235
            matched = False
 
1236
            if name in provided:
 
1237
                for version, provider in provided[name]:
 
1238
                    try:
 
1239
                        match = matcher.match(version)
 
1240
                    except UnsupportedVersionError:
 
1241
                        match = False
 
1242
 
 
1243
                    if match:
 
1244
                        graph.add_edge(dist, provider, req)
 
1245
                        matched = True
 
1246
                        break
 
1247
            if not matched:
 
1248
                graph.add_missing(dist, req)
 
1249
    return graph
 
1250
 
 
1251
 
 
1252
def get_dependent_dists(dists, dist):
 
1253
    """Recursively generate a list of distributions from *dists* that are
 
1254
    dependent on *dist*.
 
1255
 
 
1256
    :param dists: a list of distributions
 
1257
    :param dist: a distribution, member of *dists* for which we are interested
 
1258
    """
 
1259
    if dist not in dists:
 
1260
        raise DistlibException('given distribution %r is not a member '
 
1261
                               'of the list' % dist.name)
 
1262
    graph = make_graph(dists)
 
1263
 
 
1264
    dep = [dist]  # dependent distributions
 
1265
    todo = graph.reverse_list[dist]  # list of nodes we should inspect
 
1266
 
 
1267
    while todo:
 
1268
        d = todo.pop()
 
1269
        dep.append(d)
 
1270
        for succ in graph.reverse_list[d]:
 
1271
            if succ not in dep:
 
1272
                todo.append(succ)
 
1273
 
 
1274
    dep.pop(0)  # remove dist from dep, was there to prevent infinite loops
 
1275
    return dep
 
1276
 
 
1277
 
 
1278
def get_required_dists(dists, dist):
 
1279
    """Recursively generate a list of distributions from *dists* that are
 
1280
    required by *dist*.
 
1281
 
 
1282
    :param dists: a list of distributions
 
1283
    :param dist: a distribution, member of *dists* for which we are interested
 
1284
    """
 
1285
    if dist not in dists:
 
1286
        raise DistlibException('given distribution %r is not a member '
 
1287
                               'of the list' % dist.name)
 
1288
    graph = make_graph(dists)
 
1289
 
 
1290
    req = []  # required distributions
 
1291
    todo = graph.adjacency_list[dist]  # list of nodes we should inspect
 
1292
 
 
1293
    while todo:
 
1294
        d = todo.pop()[0]
 
1295
        req.append(d)
 
1296
        for pred in graph.adjacency_list[d]:
 
1297
            if pred not in req:
 
1298
                todo.append(pred)
 
1299
 
 
1300
    return req
 
1301
 
 
1302
 
 
1303
def make_dist(name, version, **kwargs):
 
1304
    """
 
1305
    A convenience method for making a dist given just a name and version.
 
1306
    """
 
1307
    summary = kwargs.pop('summary', 'Placeholder for summary')
 
1308
    md = Metadata(**kwargs)
 
1309
    md.name = name
 
1310
    md.version = version
 
1311
    md.summary = summary or 'Plaeholder for summary'
 
1312
    return Distribution(md)