1# -*- coding=utf-8 -*-
2
3import atexit
4import contextlib
5import copy
6import functools
7import os
8
9import attr
10import packaging.markers
11import packaging.version
12import pip_shims.shims
13import requests
14from packaging.utils import canonicalize_name
15from vistir.compat import JSONDecodeError, fs_str
16from vistir.contextmanagers import cd, temp_environ
17from vistir.path import create_tracked_tempdir
18
19from ..environment import MYPY_RUNNING
20from ..utils import _ensure_dir, prepare_pip_source_args
21from .cache import CACHE_DIR, DependencyCache
22from .setup_info import SetupInfo
23from .utils import (
24    clean_requires_python,
25    fix_requires_python_marker,
26    format_requirement,
27    full_groupby,
28    is_pinned_requirement,
29    key_from_ireq,
30    make_install_requirement,
31    name_from_req,
32    version_from_ireq,
33)
34
35try:
36    from contextlib import ExitStack
37except ImportError:
38    from contextlib2 import ExitStack
39
40if MYPY_RUNNING:
41    from typing import (
42        Any,
43        Dict,
44        List,
45        Generator,
46        Optional,
47        Union,
48        Tuple,
49        TypeVar,
50        Text,
51        Set,
52    )
53    from pip_shims.shims import (
54        InstallRequirement,
55        InstallationCandidate,
56        PackageFinder,
57        Command,
58    )
59    from packaging.requirements import Requirement as PackagingRequirement
60    from packaging.markers import Marker
61
62    TRequirement = TypeVar("TRequirement")
63    RequirementType = TypeVar(
64        "RequirementType", covariant=True, bound=PackagingRequirement
65    )
66    MarkerType = TypeVar("MarkerType", covariant=True, bound=Marker)
67    STRING_TYPE = Union[str, bytes, Text]
68    S = TypeVar("S", bytes, str, Text)
69
70
71PKGS_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "pkgs"))
72WHEEL_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "wheels"))
73
74DEPENDENCY_CACHE = DependencyCache()
75
76
77@contextlib.contextmanager
78def _get_wheel_cache():
79    with pip_shims.shims.global_tempdir_manager():
80        yield pip_shims.shims.WheelCache(
81            CACHE_DIR, pip_shims.shims.FormatControl(set(), set())
82        )
83
84
85def _get_filtered_versions(ireq, versions, prereleases):
86    return set(ireq.specifier.filter(versions, prereleases=prereleases))
87
88
89def find_all_matches(finder, ireq, pre=False):
90    # type: (PackageFinder, InstallRequirement, bool) -> List[InstallationCandidate]
91    """Find all matching dependencies using the supplied finder and the
92    given ireq.
93
94    :param finder: A package finder for discovering matching candidates.
95    :type finder: :class:`~pip._internal.index.PackageFinder`
96    :param ireq: An install requirement.
97    :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
98    :return: A list of matching candidates.
99    :rtype: list[:class:`~pip._internal.index.InstallationCandidate`]
100    """
101
102    candidates = clean_requires_python(finder.find_all_candidates(ireq.name))
103    versions = {candidate.version for candidate in candidates}
104    allowed_versions = _get_filtered_versions(ireq, versions, pre)
105    if not pre and not allowed_versions:
106        allowed_versions = _get_filtered_versions(ireq, versions, True)
107    candidates = {c for c in candidates if c.version in allowed_versions}
108    return candidates
109
110
111def get_pip_command():
112    # type: () -> Command
113    # Use pip's parser for pip.conf management and defaults.
114    # General options (find_links, index_url, extra_index_url, trusted_host,
115    # and pre) are defered to pip.
116    pip_command = pip_shims.shims.InstallCommand()
117    return pip_command
118
119
120@attr.s
121class AbstractDependency(object):
122    name = attr.ib()  # type: STRING_TYPE
123    specifiers = attr.ib()
124    markers = attr.ib()
125    candidates = attr.ib()
126    requirement = attr.ib()
127    parent = attr.ib()
128    finder = attr.ib()
129    dep_dict = attr.ib(default=attr.Factory(dict))
130
131    @property
132    def version_set(self):
133        """Return the set of versions for the candidates in this abstract dependency.
134
135        :return: A set of matching versions
136        :rtype: set(str)
137        """
138
139        if len(self.candidates) == 1:
140            return set()
141        return set(packaging.version.parse(version_from_ireq(c)) for c in self.candidates)
142
143    def compatible_versions(self, other):
144        """Find compatible version numbers between this abstract
145        dependency and another one.
146
147        :param other: An abstract dependency to compare with.
148        :type other: :class:`~requirementslib.models.dependency.AbstractDependency`
149        :return: A set of compatible version strings
150        :rtype: set(str)
151        """
152
153        if len(self.candidates) == 1 and next(iter(self.candidates)).editable:
154            return self
155        elif len(other.candidates) == 1 and next(iter(other.candidates)).editable:
156            return other
157        return self.version_set & other.version_set
158
159    def compatible_abstract_dep(self, other):
160        """Merge this abstract dependency with another one.
161
162        Return the result of the merge as a new abstract dependency.
163
164        :param other: An abstract dependency to merge with
165        :type other: :class:`~requirementslib.models.dependency.AbstractDependency`
166        :return: A new, combined abstract dependency
167        :rtype: :class:`~requirementslib.models.dependency.AbstractDependency`
168        """
169
170        from .requirements import Requirement
171
172        if len(self.candidates) == 1 and next(iter(self.candidates)).editable:
173            return self
174        elif len(other.candidates) == 1 and next(iter(other.candidates)).editable:
175            return other
176        new_specifiers = self.specifiers & other.specifiers
177        markers = set(self.markers) if self.markers else set()
178        if other.markers:
179            markers.add(other.markers)
180        new_markers = None
181        if markers:
182            new_markers = packaging.markers.Marker(
183                " or ".join(str(m) for m in sorted(markers))
184            )
185        new_ireq = copy.deepcopy(self.requirement.ireq)
186        new_ireq.req.specifier = new_specifiers
187        new_ireq.req.marker = new_markers
188        new_requirement = Requirement.from_line(format_requirement(new_ireq))
189        compatible_versions = self.compatible_versions(other)
190        if isinstance(compatible_versions, AbstractDependency):
191            return compatible_versions
192        candidates = [
193            c
194            for c in self.candidates
195            if packaging.version.parse(version_from_ireq(c)) in compatible_versions
196        ]
197        dep_dict = {}
198        candidate_strings = [format_requirement(c) for c in candidates]
199        for c in candidate_strings:
200            if c in self.dep_dict:
201                dep_dict[c] = self.dep_dict.get(c)
202        return AbstractDependency(
203            name=self.name,
204            specifiers=new_specifiers,
205            markers=new_markers,
206            candidates=candidates,
207            requirement=new_requirement,
208            parent=self.parent,
209            dep_dict=dep_dict,
210            finder=self.finder,
211        )
212
213    def get_deps(self, candidate):
214        """Get the dependencies of the supplied candidate.
215
216        :param candidate: An installrequirement
217        :type candidate: :class:`~pip._internal.req.req_install.InstallRequirement`
218        :return: A list of abstract dependencies
219        :rtype: list[:class:`~requirementslib.models.dependency.AbstractDependency`]
220        """
221
222        key = format_requirement(candidate)
223        if key not in self.dep_dict:
224            from .requirements import Requirement
225
226            req = Requirement.from_line(key)
227            req = req.merge_markers(self.markers)
228            self.dep_dict[key] = req.get_abstract_dependencies()
229        return self.dep_dict[key]
230
231    @classmethod
232    def from_requirement(cls, requirement, parent=None):
233        """Creates a new :class:`~requirementslib.models.dependency.AbstractDependency`
234        from a :class:`~requirementslib.models.requirements.Requirement` object.
235
236        This class is used to find all candidates matching a given set of specifiers
237        and a given requirement.
238
239        :param requirement: A requirement for resolution
240        :type requirement: :class:`~requirementslib.models.requirements.Requirement` object.
241        """
242        name = requirement.normalized_name
243        specifiers = requirement.ireq.specifier if not requirement.editable else ""
244        markers = requirement.ireq.markers
245        extras = requirement.ireq.extras
246        is_pinned = is_pinned_requirement(requirement.ireq)
247        is_constraint = bool(parent)
248        _, finder = get_finder(sources=None)
249        candidates = []
250        if not is_pinned and not requirement.editable:
251            for r in requirement.find_all_matches(finder=finder):
252                req = make_install_requirement(
253                    name,
254                    r.version,
255                    extras=extras,
256                    markers=markers,
257                    constraint=is_constraint,
258                )
259                req.req.link = getattr(r, "location", getattr(r, "link", None))
260                req.parent = parent
261                candidates.append(req)
262                candidates = sorted(
263                    set(candidates),
264                    key=lambda k: packaging.version.parse(version_from_ireq(k)),
265                )
266        else:
267            candidates = [requirement.ireq]
268        return cls(
269            name=name,
270            specifiers=specifiers,
271            markers=markers,
272            candidates=candidates,
273            requirement=requirement,
274            parent=parent,
275            finder=finder,
276        )
277
278    @classmethod
279    def from_string(cls, line, parent=None):
280        from .requirements import Requirement
281
282        req = Requirement.from_line(line)
283        abstract_dep = cls.from_requirement(req, parent=parent)
284        return abstract_dep
285
286
287def get_abstract_dependencies(reqs, sources=None, parent=None):
288    """Get all abstract dependencies for a given list of requirements.
289
290    Given a set of requirements, convert each requirement to an Abstract Dependency.
291
292    :param reqs: A list of Requirements
293    :type reqs: list[:class:`~requirementslib.models.requirements.Requirement`]
294    :param sources: Pipfile-formatted sources, defaults to None
295    :param sources: list[dict], optional
296    :param parent: The parent of this list of dependencies, defaults to None
297    :param parent: :class:`~requirementslib.models.requirements.Requirement`, optional
298    :return: A list of Abstract Dependencies
299    :rtype: list[:class:`~requirementslib.models.dependency.AbstractDependency`]
300    """
301
302    deps = []
303    from .requirements import Requirement
304
305    for req in reqs:
306        if isinstance(req, pip_shims.shims.InstallRequirement):
307            requirement = Requirement.from_line("{0}{1}".format(req.name, req.specifier))
308            if req.link:
309                requirement.req.link = req.link
310                requirement.markers = req.markers
311                requirement.req.markers = req.markers
312                requirement.extras = req.extras
313                requirement.req.extras = req.extras
314        elif isinstance(req, Requirement):
315            requirement = copy.deepcopy(req)
316        else:
317            requirement = Requirement.from_line(req)
318        dep = AbstractDependency.from_requirement(requirement, parent=parent)
319        deps.append(dep)
320    return deps
321
322
323def get_dependencies(ireq, sources=None, parent=None):
324    # type: (Union[InstallRequirement, InstallationCandidate], Optional[List[Dict[S, Union[S, bool]]]], Optional[AbstractDependency]) -> Set[S, ...]
325    """Get all dependencies for a given install requirement.
326
327    :param ireq: A single InstallRequirement
328    :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
329    :param sources: Pipfile-formatted sources, defaults to None
330    :type sources: list[dict], optional
331    :param parent: The parent of this list of dependencies, defaults to None
332    :type parent: :class:`~pip._internal.req.req_install.InstallRequirement`
333    :return: A set of dependency lines for generating new InstallRequirements.
334    :rtype: set(str)
335    """
336    if not isinstance(ireq, pip_shims.shims.InstallRequirement):
337        name = getattr(ireq, "project_name", getattr(ireq, "project", ireq.name))
338        version = getattr(ireq, "version", None)
339        if not version:
340            ireq = pip_shims.shims.InstallRequirement.from_line("{0}".format(name))
341        else:
342            ireq = pip_shims.shims.InstallRequirement.from_line(
343                "{0}=={1}".format(name, version)
344            )
345    pip_options = get_pip_options(sources=sources)
346    getters = [
347        get_dependencies_from_cache,
348        get_dependencies_from_wheel_cache,
349        get_dependencies_from_json,
350        functools.partial(get_dependencies_from_index, pip_options=pip_options),
351    ]
352    for getter in getters:
353        deps = getter(ireq)
354        if deps is not None:
355            return deps
356    raise RuntimeError("failed to get dependencies for {}".format(ireq))
357
358
359def get_dependencies_from_wheel_cache(ireq):
360    # type: (pip_shims.shims.InstallRequirement) -> Optional[Set[pip_shims.shims.InstallRequirement]]
361    """Retrieves dependencies for the given install requirement from the wheel cache.
362
363    :param ireq: A single InstallRequirement
364    :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
365    :return: A set of dependency lines for generating new InstallRequirements.
366    :rtype: set(str) or None
367    """
368
369    if ireq.editable or not is_pinned_requirement(ireq):
370        return
371    with _get_wheel_cache() as wheel_cache:
372        matches = wheel_cache.get(ireq.link, name_from_req(ireq.req))
373        if matches:
374            matches = set(matches)
375            if not DEPENDENCY_CACHE.get(ireq):
376                DEPENDENCY_CACHE[ireq] = [format_requirement(m) for m in matches]
377            return matches
378        return None
379
380
381def _marker_contains_extra(ireq):
382    # TODO: Implement better parsing logic avoid false-positives.
383    return "extra" in repr(ireq.markers)
384
385
386def get_dependencies_from_json(ireq):
387    """Retrieves dependencies for the given install requirement from the json api.
388
389    :param ireq: A single InstallRequirement
390    :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
391    :return: A set of dependency lines for generating new InstallRequirements.
392    :rtype: set(str) or None
393    """
394
395    if ireq.editable or not is_pinned_requirement(ireq):
396        return
397
398    # It is technically possible to parse extras out of the JSON API's
399    # requirement format, but it is such a chore let's just use the simple API.
400    if ireq.extras:
401        return
402
403    session = requests.session()
404    atexit.register(session.close)
405    version = str(ireq.req.specifier).lstrip("=")
406
407    def gen(ireq):
408        info = None
409        try:
410            info = session.get(
411                "https://pypi.org/pypi/{0}/{1}/json".format(ireq.req.name, version)
412            ).json()["info"]
413        finally:
414            session.close()
415        requires_dist = info.get("requires_dist", info.get("requires"))
416        if not requires_dist:  # The API can return None for this.
417            return
418        for requires in requires_dist:
419            i = pip_shims.shims.InstallRequirement.from_line(requires)
420            # See above, we don't handle requirements with extras.
421            if not _marker_contains_extra(i):
422                yield format_requirement(i)
423
424    if ireq not in DEPENDENCY_CACHE:
425        try:
426            reqs = DEPENDENCY_CACHE[ireq] = list(gen(ireq))
427        except JSONDecodeError:
428            return
429        req_iter = iter(reqs)
430    else:
431        req_iter = gen(ireq)
432    return set(req_iter)
433
434
435def get_dependencies_from_cache(ireq):
436    """Retrieves dependencies for the given install requirement from the dependency cache.
437
438    :param ireq: A single InstallRequirement
439    :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
440    :return: A set of dependency lines for generating new InstallRequirements.
441    :rtype: set(str) or None
442    """
443    if ireq.editable or not is_pinned_requirement(ireq):
444        return
445    if ireq not in DEPENDENCY_CACHE:
446        return
447    cached = set(DEPENDENCY_CACHE[ireq])
448
449    # Preserving sanity: Run through the cache and make sure every entry if
450    # valid. If this fails, something is wrong with the cache. Drop it.
451    try:
452        broken = False
453        for line in cached:
454            dep_ireq = pip_shims.shims.InstallRequirement.from_line(line)
455            name = canonicalize_name(dep_ireq.name)
456            if _marker_contains_extra(dep_ireq):
457                broken = True  # The "extra =" marker breaks everything.
458            elif name == canonicalize_name(ireq.name):
459                broken = True  # A package cannot depend on itself.
460            if broken:
461                break
462    except Exception:
463        broken = True
464
465    if broken:
466        del DEPENDENCY_CACHE[ireq]
467        return
468
469    return cached
470
471
472def is_python(section):
473    return section.startswith("[") and ":" in section
474
475
476def get_dependencies_from_index(dep, sources=None, pip_options=None, wheel_cache=None):
477    """Retrieves dependencies for the given install requirement from the pip resolver.
478
479    :param dep: A single InstallRequirement
480    :type dep: :class:`~pip._internal.req.req_install.InstallRequirement`
481    :param sources: Pipfile-formatted sources, defaults to None
482    :type sources: list[dict], optional
483    :return: A set of dependency lines for generating new InstallRequirements.
484    :rtype: set(str) or None
485    """
486
487    session, finder = get_finder(sources=sources, pip_options=pip_options)
488    dep.is_direct = True
489    requirements = None
490    setup_requires = {}
491    with temp_environ(), ExitStack() as stack:
492        if not wheel_cache:
493            wheel_cache = stack.enter_context(_get_wheel_cache())
494        os.environ["PIP_EXISTS_ACTION"] = "i"
495        if dep.editable and not dep.prepared and not dep.req:
496            setup_info = SetupInfo.from_ireq(dep)
497            results = setup_info.get_info()
498            setup_requires.update(results["setup_requires"])
499            requirements = set(results["requires"].values())
500        else:
501            results = pip_shims.shims.resolve(dep)
502            requirements = [v for v in results.values() if v.name != dep.name]
503        requirements = set([format_requirement(r) for r in requirements])
504    if not dep.editable and is_pinned_requirement(dep) and requirements is not None:
505        DEPENDENCY_CACHE[dep] = list(requirements)
506    return requirements
507
508
509def get_pip_options(args=[], sources=None, pip_command=None):
510    """Build a pip command from a list of sources
511
512    :param args: positional arguments passed through to the pip parser
513    :param sources: A list of pipfile-formatted sources, defaults to None
514    :param sources: list[dict], optional
515    :param pip_command: A pre-built pip command instance
516    :type pip_command: :class:`~pip._internal.cli.base_command.Command`
517    :return: An instance of pip_options using the supplied arguments plus sane defaults
518    :rtype: :class:`~pip._internal.cli.cmdoptions`
519    """
520
521    if not pip_command:
522        pip_command = get_pip_command()
523    if not sources:
524        sources = [{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}]
525    _ensure_dir(CACHE_DIR)
526    pip_args = args
527    pip_args = prepare_pip_source_args(sources, pip_args)
528    pip_options, _ = pip_command.parser.parse_args(pip_args)
529    pip_options.cache_dir = CACHE_DIR
530    return pip_options
531
532
533def get_finder(sources=None, pip_command=None, pip_options=None):
534    # type: (List[Dict[S, Union[S, bool]]], Optional[Command], Any) -> PackageFinder
535    """Get a package finder for looking up candidates to install
536
537    :param sources: A list of pipfile-formatted sources, defaults to None
538    :param sources: list[dict], optional
539    :param pip_command: A pip command instance, defaults to None
540    :type pip_command: :class:`~pip._internal.cli.base_command.Command`
541    :param pip_options: A pip options, defaults to None
542    :type pip_options: :class:`~pip._internal.cli.cmdoptions`
543    :return: A package finder
544    :rtype: :class:`~pip._internal.index.PackageFinder`
545    """
546
547    if not pip_command:
548        pip_command = pip_shims.shims.InstallCommand()
549    if not sources:
550        sources = [{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}]
551    if not pip_options:
552        pip_options = get_pip_options(sources=sources, pip_command=pip_command)
553    session = pip_command._build_session(pip_options)
554    atexit.register(session.close)
555    finder = pip_shims.shims.get_package_finder(
556        pip_shims.shims.InstallCommand(), options=pip_options, session=session
557    )
558    return session, finder
559
560
561@contextlib.contextmanager
562def start_resolver(finder=None, session=None, wheel_cache=None):
563    """Context manager to produce a resolver.
564
565    :param finder: A package finder to use for searching the index
566    :type finder: :class:`~pip._internal.index.PackageFinder`
567    :param :class:`~requests.Session` session: A session instance
568    :param :class:`~pip._internal.cache.WheelCache` wheel_cache: A pip WheelCache instance
569    :return: A 3-tuple of finder, preparer, resolver
570    :rtype: (:class:`~pip._internal.operations.prepare.RequirementPreparer`, :class:`~pip._internal.resolve.Resolver`)
571    """
572
573    pip_command = get_pip_command()
574    pip_options = get_pip_options(pip_command=pip_command)
575    session = None
576    if not finder:
577        session, finder = get_finder(pip_command=pip_command, pip_options=pip_options)
578    if not session:
579        session = pip_command._build_session(pip_options)
580
581    download_dir = PKGS_DOWNLOAD_DIR
582    _ensure_dir(download_dir)
583
584    _build_dir = create_tracked_tempdir(fs_str("build"))
585    _source_dir = create_tracked_tempdir(fs_str("source"))
586    try:
587        with ExitStack() as ctx:
588            ctx.enter_context(pip_shims.shims.global_tempdir_manager())
589            if not wheel_cache:
590                wheel_cache = ctx.enter_context(_get_wheel_cache())
591            _ensure_dir(fs_str(os.path.join(wheel_cache.cache_dir, "wheels")))
592            preparer = ctx.enter_context(
593                pip_shims.shims.make_preparer(
594                    options=pip_options,
595                    finder=finder,
596                    session=session,
597                    build_dir=_build_dir,
598                    src_dir=_source_dir,
599                    download_dir=download_dir,
600                    wheel_download_dir=WHEEL_DOWNLOAD_DIR,
601                    progress_bar="off",
602                    build_isolation=False,
603                    install_cmd=pip_command,
604                )
605            )
606            resolver = pip_shims.shims.get_resolver(
607                finder=finder,
608                ignore_dependencies=False,
609                ignore_requires_python=True,
610                preparer=preparer,
611                session=session,
612                options=pip_options,
613                install_cmd=pip_command,
614                wheel_cache=wheel_cache,
615                force_reinstall=True,
616                ignore_installed=True,
617                upgrade_strategy="to-satisfy-only",
618                isolated=False,
619                use_user_site=False,
620            )
621            yield resolver
622    finally:
623        session.close()
624
625
626def get_grouped_dependencies(constraints):
627    # We need to track what contributed a specifierset
628    # as well as which specifiers were required by the root node
629    # in order to resolve any conflicts when we are deciding which thing to backtrack on
630    # then we take the loose match (which _is_ flexible) and start moving backwards in
631    # versions by popping them off of a stack and checking for the conflicting package
632    for _, ireqs in full_groupby(constraints, key=key_from_ireq):
633        ireqs = sorted(ireqs, key=lambda ireq: ireq.editable)
634        editable_ireq = next(iter(ireq for ireq in ireqs if ireq.editable), None)
635        if editable_ireq:
636            yield editable_ireq  # only the editable match mattters, ignore all others
637            continue
638        ireqs = iter(ireqs)
639        # deepcopy the accumulator so as to not modify the self.our_constraints invariant
640        combined_ireq = copy.deepcopy(next(ireqs))
641        for ireq in ireqs:
642            # NOTE we may be losing some info on dropped reqs here
643            try:
644                combined_ireq.req.specifier &= ireq.req.specifier
645            except TypeError:
646                if ireq.req.specifier._specs and not combined_ireq.req.specifier._specs:
647                    combined_ireq.req.specifier._specs = ireq.req.specifier._specs
648            combined_ireq.constraint &= ireq.constraint
649            if not combined_ireq.markers:
650                combined_ireq.markers = ireq.markers
651            else:
652                _markers = combined_ireq.markers._markers
653                if not isinstance(_markers[0], (tuple, list)):
654                    combined_ireq.markers._markers = [
655                        _markers,
656                        "and",
657                        ireq.markers._markers,
658                    ]
659            # Return a sorted, de-duped tuple of extras
660            combined_ireq.extras = tuple(
661                sorted(set(tuple(combined_ireq.extras) + tuple(ireq.extras)))
662            )
663        yield combined_ireq
664