1# -*- coding=utf-8 -*-
2
3import itertools
4import re
5import sys
6
7from collections import namedtuple
8from traceback import format_tb
9
10import six
11
12from . import environments
13from ._compat import decode_for_output
14from .patched import crayons
15from .vendor.click.exceptions import (
16    ClickException, FileError, UsageError
17)
18from .vendor.vistir.misc import echo as click_echo
19import vistir
20
21ANSI_REMOVAL_RE = re.compile(r"\033\[((?:\d|;)*)([a-zA-Z])", re.MULTILINE)
22STRING_TYPES = (six.string_types, crayons.ColoredString)
23
24if sys.version_info[:2] >= (3, 7):
25    KnownException = namedtuple(
26        'KnownException', ['exception_name', 'match_string', 'show_from_string', 'prefix'],
27        defaults=[None, None, None, ""]
28    )
29else:
30    KnownException = namedtuple(
31        'KnownException', ['exception_name', 'match_string', 'show_from_string', 'prefix'],
32    )
33    KnownException.__new__.__defaults__ = (None, None, None, "")
34
35KNOWN_EXCEPTIONS = [
36    KnownException("PermissionError", prefix="Permission Denied:"),
37    KnownException(
38        "VirtualenvCreationException",
39        match_string="do_create_virtualenv",
40        show_from_string=None
41    )
42]
43
44
45def handle_exception(exc_type, exception, traceback, hook=sys.excepthook):
46    if environments.is_verbose() or not issubclass(exc_type, ClickException):
47        hook(exc_type, exception, traceback)
48    else:
49        tb = format_tb(traceback, limit=-6)
50        lines = itertools.chain.from_iterable([frame.splitlines() for frame in tb])
51        formatted_lines = []
52        for line in lines:
53            line = line.strip("'").strip('"').strip("\n").strip()
54            if not line.startswith("File"):
55                line = "      {0}".format(line)
56            else:
57                line = "  {0}".format(line)
58            line = "[{0!s}]: {1}".format(
59                exception.__class__.__name__, line
60            )
61            formatted_lines.append(line)
62        # use new exception prettification rules to format exceptions according to
63        # UX rules
64        click_echo(decode_for_output(prettify_exc("\n".join(formatted_lines))), err=True)
65        exception.show()
66
67
68sys.excepthook = handle_exception
69
70
71class PipenvException(ClickException):
72    message = "{0}: {{0}}".format(crayons.red("ERROR", bold=True))
73
74    def __init__(self, message=None, **kwargs):
75        if not message:
76            message = "Pipenv encountered a problem and had to exit."
77        extra = kwargs.pop("extra", [])
78        message = self.message.format(message)
79        ClickException.__init__(self, message)
80        self.extra = extra
81
82    def show(self, file=None):
83        if file is None:
84            file = vistir.misc.get_text_stderr()
85        if self.extra:
86            if isinstance(self.extra, STRING_TYPES):
87                self.extra = [self.extra]
88            for extra in self.extra:
89                extra = "[pipenv.exceptions.{0!s}]: {1}".format(
90                    self.__class__.__name__, extra
91                )
92                extra = decode_for_output(extra, file)
93                click_echo(extra, file=file)
94        click_echo(decode_for_output("{0}".format(self.message), file), file=file)
95
96
97class PipenvCmdError(PipenvException):
98    def __init__(self, cmd, out="", err="", exit_code=1):
99        self.cmd = cmd
100        self.out = out
101        self.err = err
102        self.exit_code = exit_code
103        message = "Error running command: {0}".format(cmd)
104        PipenvException.__init__(self, message)
105
106    def show(self, file=None):
107        if file is None:
108            file = vistir.misc.get_text_stderr()
109        click_echo("{0} {1}".format(
110            crayons.red("Error running command: "),
111            crayons.normal(decode_for_output("$ {0}".format(self.cmd), file), bold=True)
112        ), err=True)
113        if self.out:
114            click_echo("{0} {1}".format(
115                crayons.normal("OUTPUT: "),
116                decode_for_output(self.out, file)
117            ), err=True)
118        if self.err:
119            click_echo("{0} {1}".format(
120                crayons.normal("STDERR: "),
121                decode_for_output(self.err, file)
122            ), err=True)
123
124
125class JSONParseError(PipenvException):
126    def __init__(self, contents="", error_text=""):
127        self.error_text = error_text
128        PipenvException.__init__(self, contents)
129
130    def show(self, file=None):
131        if file is None:
132            file = vistir.misc.get_text_stderr()
133        message = "{0}\n{1}".format(
134            crayons.normal("Failed parsing JSON results:", bold=True),
135            decode_for_output(self.message.strip(), file)
136        )
137        click_echo(message, err=True)
138        if self.error_text:
139            click_echo("{0} {1}".format(
140                crayons.normal("ERROR TEXT:", bold=True),
141                decode_for_output(self.error_text, file)
142            ), err=True)
143
144
145class PipenvUsageError(UsageError):
146
147    def __init__(self, message=None, ctx=None, **kwargs):
148        formatted_message = "{0}: {1}"
149        msg_prefix = crayons.red("ERROR:", bold=True)
150        if not message:
151            message = "Pipenv encountered a problem and had to exit."
152        message = formatted_message.format(msg_prefix, crayons.normal(message, bold=True))
153        self.message = message
154        extra = kwargs.pop("extra", [])
155        UsageError.__init__(self, decode_for_output(message), ctx)
156        self.extra = extra
157
158    def show(self, file=None):
159        if file is None:
160            file = vistir.misc.get_text_stderr()
161        color = None
162        if self.ctx is not None:
163            color = self.ctx.color
164        if self.extra:
165            if isinstance(self.extra, STRING_TYPES):
166                self.extra = [self.extra]
167            for extra in self.extra:
168                if color:
169                    extra = getattr(crayons, color, "blue")(extra)
170                click_echo(decode_for_output(extra, file), file=file)
171        hint = ''
172        if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:
173            hint = ('Try "%s %s" for help.\n'
174                    % (self.ctx.command_path, self.ctx.help_option_names[0]))
175        if self.ctx is not None:
176            click_echo(self.ctx.get_usage() + '\n%s' % hint, file=file, color=color)
177        click_echo(self.message, file=file)
178
179
180class PipenvFileError(FileError):
181    formatted_message = "{0} {{0}} {{1}}".format(
182        crayons.red("ERROR:", bold=True)
183    )
184
185    def __init__(self, filename, message=None, **kwargs):
186        extra = kwargs.pop("extra", [])
187        if not message:
188            message = crayons.normal("Please ensure that the file exists!", bold=True)
189        message = self.formatted_message.format(
190            crayons.normal("{0} not found!".format(filename), bold=True),
191            message
192        )
193        FileError.__init__(self, filename=filename, hint=decode_for_output(message), **kwargs)
194        self.extra = extra
195
196    def show(self, file=None):
197        if file is None:
198            file = vistir.misc.get_text_stderr()
199        if self.extra:
200            if isinstance(self.extra, STRING_TYPES):
201                self.extra = [self.extra]
202            for extra in self.extra:
203                click_echo(decode_for_output(extra, file), file=file)
204        click_echo(self.message, file=file)
205
206
207class PipfileNotFound(PipenvFileError):
208    def __init__(self, filename="Pipfile", extra=None, **kwargs):
209        extra = kwargs.pop("extra", [])
210        message = (
211            "{0} {1}".format(
212                crayons.red("Aborting!", bold=True),
213                crayons.normal(
214                    "Please ensure that the file exists and is located in your"
215                    " project root directory.", bold=True
216                )
217            )
218        )
219        super(PipfileNotFound, self).__init__(filename, message=message, extra=extra, **kwargs)
220
221
222class LockfileNotFound(PipenvFileError):
223    def __init__(self, filename="Pipfile.lock", extra=None, **kwargs):
224        extra = kwargs.pop("extra", [])
225        message = "{0} {1} {2}".format(
226            crayons.normal("You need to run", bold=True),
227            crayons.red("$ pipenv lock", bold=True),
228            crayons.normal("before you can continue.", bold=True)
229        )
230        super(LockfileNotFound, self).__init__(filename, message=message, extra=extra, **kwargs)
231
232
233class DeployException(PipenvUsageError):
234    def __init__(self, message=None, **kwargs):
235        if not message:
236            message = str(crayons.normal("Aborting deploy", bold=True))
237        extra = kwargs.pop("extra", [])
238        PipenvUsageError.__init__(self, message=message, extra=extra, **kwargs)
239
240
241class PipenvOptionsError(PipenvUsageError):
242    def __init__(self, option_name, message=None, ctx=None, **kwargs):
243        extra = kwargs.pop("extra", [])
244        PipenvUsageError.__init__(self, message=message, ctx=ctx, **kwargs)
245        self.extra = extra
246        self.option_name = option_name
247
248
249class SystemUsageError(PipenvOptionsError):
250    def __init__(self, option_name="system", message=None, ctx=None, **kwargs):
251        extra = kwargs.pop("extra", [])
252        extra += [
253            "{0}: --system is intended to be used for Pipfile installation, "
254            "not installation of specific packages. Aborting.".format(
255                crayons.red("Warning", bold=True)
256            ),
257        ]
258        if message is None:
259            message = str(
260                crayons.cyan("See also: {0}".format(crayons.normal("--deploy flag.")))
261            )
262        super(SystemUsageError, self).__init__(option_name, message=message, ctx=ctx, extra=extra, **kwargs)
263
264
265class PipfileException(PipenvFileError):
266    def __init__(self, hint=None, **kwargs):
267        from .core import project
268
269        if not hint:
270            hint = "{0} {1}".format(crayons.red("ERROR (PACKAGE NOT INSTALLED):"), hint)
271        filename = project.pipfile_location
272        extra = kwargs.pop("extra", [])
273        PipenvFileError.__init__(self, filename, hint, extra=extra, **kwargs)
274
275
276class SetupException(PipenvException):
277    def __init__(self, message=None, **kwargs):
278        PipenvException.__init__(self, message, **kwargs)
279
280
281class VirtualenvException(PipenvException):
282
283    def __init__(self, message=None, **kwargs):
284        if not message:
285            message = (
286                "There was an unexpected error while activating your virtualenv. "
287                "Continuing anyway..."
288            )
289        PipenvException.__init__(self, message, **kwargs)
290
291
292class VirtualenvActivationException(VirtualenvException):
293    def __init__(self, message=None, **kwargs):
294        if not message:
295            message = (
296                "activate_this.py not found. Your environment is most certainly "
297                "not activated. Continuing anyway..."
298            )
299        self.message = message
300        VirtualenvException.__init__(self, message, **kwargs)
301
302
303class VirtualenvCreationException(VirtualenvException):
304    def __init__(self, message=None, **kwargs):
305        if not message:
306            message = "Failed to create virtual environment."
307        self.message = message
308        extra = kwargs.pop("extra", None)
309        if extra is not None and isinstance(extra, STRING_TYPES):
310            # note we need the format interpolation because ``crayons.ColoredString``
311            # is not an actual string type but is only a preparation for interpolation
312            # so replacement or parsing requires this step
313            extra = ANSI_REMOVAL_RE.sub("", "{0}".format(extra))
314            if "KeyboardInterrupt" in extra:
315                extra = str(
316                    crayons.red("Virtualenv creation interrupted by user", bold=True)
317                )
318            self.extra = extra = [extra]
319        VirtualenvException.__init__(self, message, extra=extra)
320
321
322class UninstallError(PipenvException):
323    def __init__(self, package, command, return_values, return_code, **kwargs):
324        extra = [
325            "{0} {1}".format(
326                crayons.cyan("Attempted to run command: "),
327                crayons.yellow("$ {0!r}".format(command), bold=True)
328            )
329        ]
330        extra.extend([crayons.cyan(line.strip()) for line in return_values.splitlines()])
331        if isinstance(package, (tuple, list, set)):
332            package = " ".join(package)
333        message = "{0!s} {1!s}...".format(
334            crayons.normal("Failed to uninstall package(s)"),
335            crayons.yellow("{0}!s".format(package), bold=True)
336        )
337        self.exit_code = return_code
338        PipenvException.__init__(self, message=message, extra=extra)
339        self.extra = extra
340
341
342class InstallError(PipenvException):
343    def __init__(self, package, **kwargs):
344        package_message = ""
345        if package is not None:
346            package_message = "Couldn't install package: {0}\n".format(
347                crayons.normal("{0!s}".format(package), bold=True)
348            )
349        message = "{0} {1}".format(
350            "{0}".format(package_message),
351            crayons.yellow("Package installation failed...")
352        )
353        extra = kwargs.pop("extra", [])
354        PipenvException.__init__(self, message=message, extra=extra, **kwargs)
355
356
357class CacheError(PipenvException):
358    def __init__(self, path, **kwargs):
359        message = "{0} {1}\n{2}".format(
360            crayons.cyan("Corrupt cache file"),
361            crayons.normal("{0!s}".format(path)),
362            crayons.normal('Consider trying "pipenv lock --clear" to clear the cache.')
363        )
364        PipenvException.__init__(self, message=message)
365
366
367class DependencyConflict(PipenvException):
368    def __init__(self, message):
369        extra = ["{0} {1}".format(
370            crayons.red("The operation failed...", bold=True),
371            crayons.red("A dependency conflict was detected and could not be resolved."),
372        )]
373        PipenvException.__init__(self, message, extra=extra)
374
375
376class ResolutionFailure(PipenvException):
377    def __init__(self, message, no_version_found=False):
378        extra = (
379            "{0}: Your dependencies could not be resolved. You likely have a "
380            "mismatch in your sub-dependencies.\n  "
381            "First try clearing your dependency cache with {1}, then try the original command again.\n "
382            "Alternatively, you can use {2} to bypass this mechanism, then run "
383            "{3} to inspect the situation.\n  "
384            "Hint: try {4} if it is a pre-release dependency."
385            "".format(
386                crayons.red("Warning", bold=True),
387                crayons.yellow("$ pipenv lock --clear"),
388                crayons.yellow("$ pipenv install --skip-lock"),
389                crayons.yellow("$ pipenv graph"),
390                crayons.yellow("$ pipenv lock --pre"),
391            ),
392        )
393        if "no version found at all" in message:
394            no_version_found = True
395        message = crayons.yellow("{0}".format(message))
396        if no_version_found:
397            message = "{0}\n{1}".format(
398                message,
399                crayons.cyan(
400                    "Please check your version specifier and version number. "
401                    "See PEP440 for more information."
402                )
403            )
404        PipenvException.__init__(self, message, extra=extra)
405
406
407class RequirementError(PipenvException):
408
409    def __init__(self, req=None):
410        from .utils import VCS_LIST
411        keys = ("name", "path",) + VCS_LIST + ("line", "uri", "url", "relpath")
412        if req is not None:
413            possible_display_values = [getattr(req, value, None) for value in keys]
414            req_value = next(iter(
415                val for val in possible_display_values if val is not None
416            ), None)
417            if not req_value:
418                getstate_fn = getattr(req, "__getstate__", None)
419                slots = getattr(req, "__slots__", None)
420                keys_fn = getattr(req, "keys", None)
421                if getstate_fn:
422                    req_value = getstate_fn()
423                elif slots:
424                    slot_vals = [
425                        (k, getattr(req, k, None)) for k in slots
426                        if getattr(req, k, None)
427                    ]
428                    req_value = "\n".join([
429                        "    {0}: {1}".format(k, v) for k, v in slot_vals
430                    ])
431                elif keys_fn:
432                    values = [(k, req.get(k)) for k in keys_fn() if req.get(k)]
433                    req_value = "\n".join([
434                        "    {0}: {1}".format(k, v) for k, v in values
435                    ])
436                else:
437                    req_value = getattr(req.line_instance, "line", None)
438        message = "{0} {1}".format(
439            crayons.normal(decode_for_output("Failed creating requirement instance")),
440            crayons.normal(decode_for_output("{0!r}".format(req_value)))
441        )
442        extra = [str(req)]
443        PipenvException.__init__(self, message, extra=extra)
444
445
446def prettify_exc(error):
447    """Catch known errors and prettify them instead of showing the
448    entire traceback, for better UX"""
449    errors = []
450    for exc in KNOWN_EXCEPTIONS:
451        search_string = exc.match_string if exc.match_string else exc.exception_name
452        split_string = exc.show_from_string if exc.show_from_string else exc.exception_name
453        if search_string in error:
454            # for known exceptions with no display rules and no prefix
455            # we should simply show nothing
456            if not exc.show_from_string and not exc.prefix:
457                errors.append("")
458                continue
459            elif exc.prefix and exc.prefix in error:
460                _, error, info = error.rpartition(exc.prefix)
461            else:
462                _, error, info = error.rpartition(split_string)
463            errors.append("{0} {1}".format(error, info))
464    if not errors:
465        return "{}".format(vistir.misc.decode_for_output(error))
466
467    return "\n".join(errors)
468