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