1from __future__ import absolute_import
2
3import logging
4from collections import OrderedDict
5
6from pip._internal.exceptions import InstallationError
7from pip._internal.utils.logging import indent_log
8from pip._internal.utils.typing import MYPY_CHECK_RUNNING
9from pip._internal.wheel import Wheel
10
11if MYPY_CHECK_RUNNING:
12    from typing import Dict, Iterable, List, Optional, Tuple
13    from pip._internal.req.req_install import InstallRequirement
14
15
16logger = logging.getLogger(__name__)
17
18
19class RequirementSet(object):
20
21    def __init__(self, require_hashes=False, check_supported_wheels=True):
22        # type: (bool, bool) -> None
23        """Create a RequirementSet.
24        """
25
26        self.requirements = OrderedDict()  # type: Dict[str, InstallRequirement]  # noqa: E501
27        self.require_hashes = require_hashes
28        self.check_supported_wheels = check_supported_wheels
29
30        # Mapping of alias: real_name
31        self.requirement_aliases = {}  # type: Dict[str, str]
32        self.unnamed_requirements = []  # type: List[InstallRequirement]
33        self.successfully_downloaded = []  # type: List[InstallRequirement]
34        self.reqs_to_cleanup = []  # type: List[InstallRequirement]
35
36    def __str__(self):
37        # type: () -> str
38        reqs = [req for req in self.requirements.values()
39                if not req.comes_from]
40        reqs.sort(key=lambda req: req.name.lower())
41        return ' '.join([str(req.req) for req in reqs])
42
43    def __repr__(self):
44        # type: () -> str
45        reqs = [req for req in self.requirements.values()]
46        reqs.sort(key=lambda req: req.name.lower())
47        reqs_str = ', '.join([str(req.req) for req in reqs])
48        return ('<%s object; %d requirement(s): %s>'
49                % (self.__class__.__name__, len(reqs), reqs_str))
50
51    def add_requirement(
52        self,
53        install_req,  # type: InstallRequirement
54        parent_req_name=None,  # type: Optional[str]
55        extras_requested=None  # type: Optional[Iterable[str]]
56    ):
57        # type: (...) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]  # noqa: E501
58        """Add install_req as a requirement to install.
59
60        :param parent_req_name: The name of the requirement that needed this
61            added. The name is used because when multiple unnamed requirements
62            resolve to the same name, we could otherwise end up with dependency
63            links that point outside the Requirements set. parent_req must
64            already be added. Note that None implies that this is a user
65            supplied requirement, vs an inferred one.
66        :param extras_requested: an iterable of extras used to evaluate the
67            environment markers.
68        :return: Additional requirements to scan. That is either [] if
69            the requirement is not applicable, or [install_req] if the
70            requirement is applicable and has just been added.
71        """
72        name = install_req.name
73
74        # If the markers do not match, ignore this requirement.
75        if not install_req.match_markers(extras_requested):
76            logger.info(
77                "Ignoring %s: markers '%s' don't match your environment",
78                name, install_req.markers,
79            )
80            return [], None
81
82        # If the wheel is not supported, raise an error.
83        # Should check this after filtering out based on environment markers to
84        # allow specifying different wheels based on the environment/OS, in a
85        # single requirements file.
86        if install_req.link and install_req.link.is_wheel:
87            wheel = Wheel(install_req.link.filename)
88            if self.check_supported_wheels and not wheel.supported():
89                raise InstallationError(
90                    "%s is not a supported wheel on this platform." %
91                    wheel.filename
92                )
93
94        # This next bit is really a sanity check.
95        assert install_req.is_direct == (parent_req_name is None), (
96            "a direct req shouldn't have a parent and also, "
97            "a non direct req should have a parent"
98        )
99
100        # Unnamed requirements are scanned again and the requirement won't be
101        # added as a dependency until after scanning.
102        if not name:
103            # url or path requirement w/o an egg fragment
104            self.unnamed_requirements.append(install_req)
105            return [install_req], None
106
107        try:
108            existing_req = self.get_requirement(name)
109        except KeyError:
110            existing_req = None
111
112        has_conflicting_requirement = (
113            parent_req_name is None and
114            existing_req and
115            not existing_req.constraint and
116            existing_req.extras == install_req.extras and
117            existing_req.req.specifier != install_req.req.specifier
118        )
119        if has_conflicting_requirement:
120            raise InstallationError(
121                "Double requirement given: %s (already in %s, name=%r)"
122                % (install_req, existing_req, name)
123            )
124
125        # When no existing requirement exists, add the requirement as a
126        # dependency and it will be scanned again after.
127        if not existing_req:
128            self.requirements[name] = install_req
129            # FIXME: what about other normalizations?  E.g., _ vs. -?
130            if name.lower() != name:
131                self.requirement_aliases[name.lower()] = name
132            # We'd want to rescan this requirements later
133            return [install_req], install_req
134
135        # Assume there's no need to scan, and that we've already
136        # encountered this for scanning.
137        if install_req.constraint or not existing_req.constraint:
138            return [], existing_req
139
140        does_not_satisfy_constraint = (
141            install_req.link and
142            not (
143                existing_req.link and
144                install_req.link.path == existing_req.link.path
145            )
146        )
147        if does_not_satisfy_constraint:
148            self.reqs_to_cleanup.append(install_req)
149            raise InstallationError(
150                "Could not satisfy constraints for '%s': "
151                "installation from path or url cannot be "
152                "constrained to a version" % name,
153            )
154        # If we're now installing a constraint, mark the existing
155        # object for real installation.
156        existing_req.constraint = False
157        existing_req.extras = tuple(sorted(
158            set(existing_req.extras) | set(install_req.extras)
159        ))
160        logger.debug(
161            "Setting %s extras to: %s",
162            existing_req, existing_req.extras,
163        )
164        # Return the existing requirement for addition to the parent and
165        # scanning again.
166        return [existing_req], existing_req
167
168    def has_requirement(self, project_name):
169        # type: (str) -> bool
170        name = project_name.lower()
171        if (name in self.requirements and
172           not self.requirements[name].constraint or
173           name in self.requirement_aliases and
174           not self.requirements[self.requirement_aliases[name]].constraint):
175            return True
176        return False
177
178    def get_requirement(self, project_name):
179        # type: (str) -> InstallRequirement
180        for name in project_name, project_name.lower():
181            if name in self.requirements:
182                return self.requirements[name]
183            if name in self.requirement_aliases:
184                return self.requirements[self.requirement_aliases[name]]
185        raise KeyError("No project with the name %r" % project_name)
186
187    def cleanup_files(self):
188        # type: () -> None
189        """Clean up files, remove builds."""
190        logger.debug('Cleaning up...')
191        with indent_log():
192            for req in self.reqs_to_cleanup:
193                req.remove_temporary_source()
194