1"""Exceptions used throughout package"""
2from __future__ import absolute_import
3
4from itertools import chain, groupby, repeat
5
6from pip9._vendor.six import iteritems
7
8
9class PipError(Exception):
10    """Base pip exception"""
11
12
13class InstallationError(PipError):
14    """General exception during installation"""
15
16
17class UninstallationError(PipError):
18    """General exception during uninstallation"""
19
20
21class DistributionNotFound(InstallationError):
22    """Raised when a distribution cannot be found to satisfy a requirement"""
23
24
25class RequirementsFileParseError(InstallationError):
26    """Raised when a general error occurs parsing a requirements file line."""
27
28
29class BestVersionAlreadyInstalled(PipError):
30    """Raised when the most up-to-date version of a package is already
31    installed."""
32
33
34class BadCommand(PipError):
35    """Raised when virtualenv or a command is not found"""
36
37
38class CommandError(PipError):
39    """Raised when there is an error in command-line arguments"""
40
41
42class PreviousBuildDirError(PipError):
43    """Raised when there's a previous conflicting build directory"""
44
45
46class InvalidWheelFilename(InstallationError):
47    """Invalid wheel filename."""
48
49
50class UnsupportedWheel(InstallationError):
51    """Unsupported wheel."""
52
53
54class HashErrors(InstallationError):
55    """Multiple HashError instances rolled into one for reporting"""
56
57    def __init__(self):
58        self.errors = []
59
60    def append(self, error):
61        self.errors.append(error)
62
63    def __str__(self):
64        lines = []
65        self.errors.sort(key=lambda e: e.order)
66        for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__):
67            lines.append(cls.head)
68            lines.extend(e.body() for e in errors_of_cls)
69        if lines:
70            return '\n'.join(lines)
71
72    def __nonzero__(self):
73        return bool(self.errors)
74
75    def __bool__(self):
76        return self.__nonzero__()
77
78
79class HashError(InstallationError):
80    """
81    A failure to verify a package against known-good hashes
82
83    :cvar order: An int sorting hash exception classes by difficulty of
84        recovery (lower being harder), so the user doesn't bother fretting
85        about unpinned packages when he has deeper issues, like VCS
86        dependencies, to deal with. Also keeps error reports in a
87        deterministic order.
88    :cvar head: A section heading for display above potentially many
89        exceptions of this kind
90    :ivar req: The InstallRequirement that triggered this error. This is
91        pasted on after the exception is instantiated, because it's not
92        typically available earlier.
93
94    """
95    req = None
96    head = ''
97
98    def body(self):
99        """Return a summary of me for display under the heading.
100
101        This default implementation simply prints a description of the
102        triggering requirement.
103
104        :param req: The InstallRequirement that provoked this error, with
105            populate_link() having already been called
106
107        """
108        return '    %s' % self._requirement_name()
109
110    def __str__(self):
111        return '%s\n%s' % (self.head, self.body())
112
113    def _requirement_name(self):
114        """Return a description of the requirement that triggered me.
115
116        This default implementation returns long description of the req, with
117        line numbers
118
119        """
120        return str(self.req) if self.req else 'unknown package'
121
122
123class VcsHashUnsupported(HashError):
124    """A hash was provided for a version-control-system-based requirement, but
125    we don't have a method for hashing those."""
126
127    order = 0
128    head = ("Can't verify hashes for these requirements because we don't "
129            "have a way to hash version control repositories:")
130
131
132class DirectoryUrlHashUnsupported(HashError):
133    """A hash was provided for a version-control-system-based requirement, but
134    we don't have a method for hashing those."""
135
136    order = 1
137    head = ("Can't verify hashes for these file:// requirements because they "
138            "point to directories:")
139
140
141class HashMissing(HashError):
142    """A hash was needed for a requirement but is absent."""
143
144    order = 2
145    head = ('Hashes are required in --require-hashes mode, but they are '
146            'missing from some requirements. Here is a list of those '
147            'requirements along with the hashes their downloaded archives '
148            'actually had. Add lines like these to your requirements files to '
149            'prevent tampering. (If you did not enable --require-hashes '
150            'manually, note that it turns on automatically when any package '
151            'has a hash.)')
152
153    def __init__(self, gotten_hash):
154        """
155        :param gotten_hash: The hash of the (possibly malicious) archive we
156            just downloaded
157        """
158        self.gotten_hash = gotten_hash
159
160    def body(self):
161        from pip9.utils.hashes import FAVORITE_HASH  # Dodge circular import.
162
163        package = None
164        if self.req:
165            # In the case of URL-based requirements, display the original URL
166            # seen in the requirements file rather than the package name,
167            # so the output can be directly copied into the requirements file.
168            package = (self.req.original_link if self.req.original_link
169                       # In case someone feeds something downright stupid
170                       # to InstallRequirement's constructor.
171                       else getattr(self.req, 'req', None))
172        return '    %s --hash=%s:%s' % (package or 'unknown package',
173                                        FAVORITE_HASH,
174                                        self.gotten_hash)
175
176
177class HashUnpinned(HashError):
178    """A requirement had a hash specified but was not pinned to a specific
179    version."""
180
181    order = 3
182    head = ('In --require-hashes mode, all requirements must have their '
183            'versions pinned with ==. These do not:')
184
185
186class HashMismatch(HashError):
187    """
188    Distribution file hash values don't match.
189
190    :ivar package_name: The name of the package that triggered the hash
191        mismatch. Feel free to write to this after the exception is raise to
192        improve its error message.
193
194    """
195    order = 4
196    head = ('THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS '
197            'FILE. If you have updated the package versions, please update '
198            'the hashes. Otherwise, examine the package contents carefully; '
199            'someone may have tampered with them.')
200
201    def __init__(self, allowed, gots):
202        """
203        :param allowed: A dict of algorithm names pointing to lists of allowed
204            hex digests
205        :param gots: A dict of algorithm names pointing to hashes we
206            actually got from the files under suspicion
207        """
208        self.allowed = allowed
209        self.gots = gots
210
211    def body(self):
212        return '    %s:\n%s' % (self._requirement_name(),
213                                self._hash_comparison())
214
215    def _hash_comparison(self):
216        """
217        Return a comparison of actual and expected hash values.
218
219        Example::
220
221               Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
222                            or 123451234512345123451234512345123451234512345
223                    Got        bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
224
225        """
226        def hash_then_or(hash_name):
227            # For now, all the decent hashes have 6-char names, so we can get
228            # away with hard-coding space literals.
229            return chain([hash_name], repeat('    or'))
230
231        lines = []
232        for hash_name, expecteds in iteritems(self.allowed):
233            prefix = hash_then_or(hash_name)
234            lines.extend(('        Expected %s %s' % (next(prefix), e))
235                         for e in expecteds)
236            lines.append('             Got        %s\n' %
237                         self.gots[hash_name].hexdigest())
238            prefix = '    or'
239        return '\n'.join(lines)
240
241
242class UnsupportedPythonVersion(InstallationError):
243    """Unsupported python version according to Requires-Python package
244    metadata."""
245