summaryrefslogtreecommitdiff
path: root/setuptools/_vendor/nspektr/__init__.py
blob: 938bbdb980a7bc835f7e1bfbab82b9d6501a5e51 (plain)
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
import itertools
import functools
import contextlib

from setuptools.extern.packaging.requirements import Requirement
from setuptools.extern.packaging.version import Version
from setuptools.extern.more_itertools import always_iterable
from setuptools.extern.jaraco.context import suppress
from setuptools.extern.jaraco.functools import apply

from ._compat import metadata, repair_extras


def resolve(req: Requirement) -> metadata.Distribution:
    """
    Resolve the requirement to its distribution.

    Ignore exception detail for Python 3.9 compatibility.

    >>> resolve(Requirement('pytest<3'))  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    importlib.metadata.PackageNotFoundError: No package metadata was found for pytest<3
    """
    dist = metadata.distribution(req.name)
    if not req.specifier.contains(Version(dist.version), prereleases=True):
        raise metadata.PackageNotFoundError(str(req))
    dist.extras = req.extras  # type: ignore
    return dist


@apply(bool)
@suppress(metadata.PackageNotFoundError)
def is_satisfied(req: Requirement):
    return resolve(req)


unsatisfied = functools.partial(itertools.filterfalse, is_satisfied)


class NullMarker:
    @classmethod
    def wrap(cls, req: Requirement):
        return req.marker or cls()

    def evaluate(self, *args, **kwargs):
        return True


def find_direct_dependencies(dist, extras=None):
    """
    Find direct, declared dependencies for dist.
    """
    simple = (
        req
        for req in map(Requirement, always_iterable(dist.requires))
        if NullMarker.wrap(req).evaluate(dict(extra=None))
    )
    extra_deps = (
        req
        for req in map(Requirement, always_iterable(dist.requires))
        for extra in always_iterable(getattr(dist, 'extras', extras))
        if NullMarker.wrap(req).evaluate(dict(extra=extra))
    )
    return itertools.chain(simple, extra_deps)


def traverse(items, visit):
    """
    Given an iterable of items, traverse the items.

    For each item, visit is called to return any additional items
    to include in the traversal.
    """
    while True:
        try:
            item = next(items)
        except StopIteration:
            return
        yield item
        items = itertools.chain(items, visit(item))


def find_req_dependencies(req):
    with contextlib.suppress(metadata.PackageNotFoundError):
        dist = resolve(req)
        yield from find_direct_dependencies(dist)


def find_dependencies(dist, extras=None):
    """
    Find all reachable dependencies for dist.

    dist is an importlib.metadata.Distribution (or similar).
    TODO: create a suitable protocol for type hint.

    >>> deps = find_dependencies(resolve(Requirement('nspektr')))
    >>> all(isinstance(dep, Requirement) for dep in deps)
    True
    >>> not any('pytest' in str(dep) for dep in deps)
    True
    >>> test_deps = find_dependencies(resolve(Requirement('nspektr[testing]')))
    >>> any('pytest' in str(dep) for dep in test_deps)
    True
    """

    def visit(req, seen=set()):
        if req in seen:
            return ()
        seen.add(req)
        return find_req_dependencies(req)

    return traverse(find_direct_dependencies(dist, extras), visit)


class Unresolved(Exception):
    def __iter__(self):
        return iter(self.args[0])


def missing(ep):
    """
    Generate the unresolved dependencies (if any) of ep.
    """
    return unsatisfied(find_dependencies(ep.dist, repair_extras(ep.extras)))


def check(ep):
    """
    >>> ep, = metadata.entry_points(group='console_scripts', name='pip')
    >>> check(ep)
    >>> dist = metadata.distribution('nspektr')

    Since 'docs' extras are not installed, requesting them should fail.

    >>> ep = metadata.EntryPoint(
    ...     group=None, name=None, value='nspektr [docs]')._for(dist)
    >>> check(ep)
    Traceback (most recent call last):
    ...
    nspektr.Unresolved: [...]
    """
    missed = list(missing(ep))
    if missed:
        raise Unresolved(missed)