mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 06:46:15 +00:00
9a3b3ce706
* Add .pre-commit-config.yaml to the project and exclude tests (for now). This does not include the MyPy linting that pip does but does include everything else.
463 lines
17 KiB
Python
463 lines
17 KiB
Python
import itertools
|
|
import re
|
|
import sys
|
|
from collections import namedtuple
|
|
from traceback import format_tb
|
|
|
|
from pipenv import environments
|
|
from pipenv._compat import decode_for_output
|
|
from pipenv.patched import crayons
|
|
from pipenv.vendor import vistir
|
|
from pipenv.vendor.click.exceptions import ClickException, FileError, UsageError
|
|
from pipenv.vendor.vistir.misc import echo as click_echo
|
|
|
|
ANSI_REMOVAL_RE = re.compile(r"\033\[((?:\d|;)*)([a-zA-Z])", re.MULTILINE)
|
|
STRING_TYPES = ((str,), crayons.ColoredString)
|
|
|
|
if sys.version_info[:2] >= (3, 7):
|
|
KnownException = namedtuple(
|
|
"KnownException",
|
|
["exception_name", "match_string", "show_from_string", "prefix"],
|
|
defaults=[None, None, None, ""],
|
|
)
|
|
else:
|
|
KnownException = namedtuple(
|
|
"KnownException",
|
|
["exception_name", "match_string", "show_from_string", "prefix"],
|
|
)
|
|
KnownException.__new__.__defaults__ = (None, None, None, "")
|
|
|
|
KNOWN_EXCEPTIONS = [
|
|
KnownException("PermissionError", prefix="Permission Denied:"),
|
|
KnownException(
|
|
"VirtualenvCreationException",
|
|
match_string="do_create_virtualenv",
|
|
show_from_string=None,
|
|
),
|
|
]
|
|
|
|
|
|
def handle_exception(exc_type, exception, traceback, hook=sys.excepthook):
|
|
if environments.Setting().is_verbose() or not issubclass(exc_type, ClickException):
|
|
hook(exc_type, exception, traceback)
|
|
else:
|
|
tb = format_tb(traceback, limit=-6)
|
|
lines = itertools.chain.from_iterable([frame.splitlines() for frame in tb])
|
|
formatted_lines = []
|
|
for line in lines:
|
|
line = line.strip("'").strip('"').strip("\n").strip()
|
|
if not line.startswith("File"):
|
|
line = f" {line}"
|
|
else:
|
|
line = f" {line}"
|
|
line = "[{!s}]: {}".format(exception.__class__.__name__, line)
|
|
formatted_lines.append(line)
|
|
# use new exception prettification rules to format exceptions according to
|
|
# UX rules
|
|
click_echo(decode_for_output(prettify_exc("\n".join(formatted_lines))), err=True)
|
|
exception.show()
|
|
|
|
|
|
sys.excepthook = handle_exception
|
|
|
|
|
|
class PipenvException(ClickException):
|
|
message = "{0}: {{0}}".format(crayons.red("ERROR", bold=True))
|
|
|
|
def __init__(self, message=None, **kwargs):
|
|
if not message:
|
|
message = "Pipenv encountered a problem and had to exit."
|
|
extra = kwargs.pop("extra", [])
|
|
message = self.message.format(message)
|
|
ClickException.__init__(self, message)
|
|
self.extra = extra
|
|
|
|
def show(self, file=None):
|
|
if file is None:
|
|
file = vistir.misc.get_text_stderr()
|
|
if self.extra:
|
|
if isinstance(self.extra, STRING_TYPES):
|
|
self.extra = [self.extra]
|
|
for extra in self.extra:
|
|
extra = "[pipenv.exceptions.{!s}]: {}".format(
|
|
self.__class__.__name__, extra
|
|
)
|
|
extra = decode_for_output(extra, file)
|
|
click_echo(extra, file=file)
|
|
click_echo(decode_for_output(f"{self.message}", file), file=file)
|
|
|
|
|
|
class PipenvCmdError(PipenvException):
|
|
def __init__(self, cmd, out="", err="", exit_code=1):
|
|
self.cmd = cmd
|
|
self.out = out
|
|
self.err = err
|
|
self.exit_code = exit_code
|
|
message = f"Error running command: {cmd}"
|
|
PipenvException.__init__(self, message)
|
|
|
|
def show(self, file=None):
|
|
if file is None:
|
|
file = vistir.misc.get_text_stderr()
|
|
click_echo(
|
|
"{} {}".format(
|
|
crayons.red("Error running command: "),
|
|
crayons.normal(decode_for_output(f"$ {self.cmd}", file), bold=True),
|
|
),
|
|
err=True,
|
|
)
|
|
if self.out:
|
|
click_echo(
|
|
"{} {}".format(
|
|
crayons.normal("OUTPUT: "), decode_for_output(self.out, file)
|
|
),
|
|
err=True,
|
|
)
|
|
if self.err:
|
|
click_echo(
|
|
"{} {}".format(
|
|
crayons.normal("STDERR: "), decode_for_output(self.err, file)
|
|
),
|
|
err=True,
|
|
)
|
|
|
|
|
|
class JSONParseError(PipenvException):
|
|
def __init__(self, contents="", error_text=""):
|
|
self.error_text = error_text
|
|
PipenvException.__init__(self, contents)
|
|
|
|
def show(self, file=None):
|
|
if file is None:
|
|
file = vistir.misc.get_text_stderr()
|
|
message = "{}\n{}".format(
|
|
crayons.normal("Failed parsing JSON results:", bold=True),
|
|
decode_for_output(self.message.strip(), file),
|
|
)
|
|
click_echo(message, err=True)
|
|
if self.error_text:
|
|
click_echo(
|
|
"{} {}".format(
|
|
crayons.normal("ERROR TEXT:", bold=True),
|
|
decode_for_output(self.error_text, file),
|
|
),
|
|
err=True,
|
|
)
|
|
|
|
|
|
class PipenvUsageError(UsageError):
|
|
def __init__(self, message=None, ctx=None, **kwargs):
|
|
formatted_message = "{0}: {1}"
|
|
msg_prefix = crayons.red("ERROR:", bold=True)
|
|
if not message:
|
|
message = "Pipenv encountered a problem and had to exit."
|
|
message = formatted_message.format(msg_prefix, crayons.normal(message, bold=True))
|
|
self.message = message
|
|
extra = kwargs.pop("extra", [])
|
|
UsageError.__init__(self, decode_for_output(message), ctx)
|
|
self.extra = extra
|
|
|
|
def show(self, file=None):
|
|
if file is None:
|
|
file = vistir.misc.get_text_stderr()
|
|
color = None
|
|
if self.ctx is not None:
|
|
color = self.ctx.color
|
|
if self.extra:
|
|
if isinstance(self.extra, STRING_TYPES):
|
|
self.extra = [self.extra]
|
|
for extra in self.extra:
|
|
if color:
|
|
extra = getattr(crayons, color, "blue")(extra)
|
|
click_echo(decode_for_output(extra, file), file=file)
|
|
hint = ""
|
|
if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:
|
|
hint = 'Try "%s %s" for help.\n' % (
|
|
self.ctx.command_path,
|
|
self.ctx.help_option_names[0],
|
|
)
|
|
if self.ctx is not None:
|
|
click_echo(self.ctx.get_usage() + "\n%s" % hint, file=file, color=color)
|
|
click_echo(self.message, file=file)
|
|
|
|
|
|
class PipenvFileError(FileError):
|
|
formatted_message = "{0} {{0}} {{1}}".format(crayons.red("ERROR:", bold=True))
|
|
|
|
def __init__(self, filename, message=None, **kwargs):
|
|
extra = kwargs.pop("extra", [])
|
|
if not message:
|
|
message = crayons.normal("Please ensure that the file exists!", bold=True)
|
|
message = self.formatted_message.format(
|
|
crayons.normal(f"{filename} not found!", bold=True), message
|
|
)
|
|
FileError.__init__(
|
|
self, filename=filename, hint=decode_for_output(message), **kwargs
|
|
)
|
|
self.extra = extra
|
|
|
|
def show(self, file=None):
|
|
if file is None:
|
|
file = vistir.misc.get_text_stderr()
|
|
if self.extra:
|
|
if isinstance(self.extra, STRING_TYPES):
|
|
self.extra = [self.extra]
|
|
for extra in self.extra:
|
|
click_echo(decode_for_output(extra, file), file=file)
|
|
click_echo(self.message, file=file)
|
|
|
|
|
|
class PipfileNotFound(PipenvFileError):
|
|
def __init__(self, filename="Pipfile", extra=None, **kwargs):
|
|
extra = kwargs.pop("extra", [])
|
|
message = "{} {}".format(
|
|
crayons.red("Aborting!", bold=True),
|
|
crayons.normal(
|
|
"Please ensure that the file exists and is located in your"
|
|
" project root directory.",
|
|
bold=True,
|
|
),
|
|
)
|
|
super().__init__(filename, message=message, extra=extra, **kwargs)
|
|
|
|
|
|
class LockfileNotFound(PipenvFileError):
|
|
def __init__(self, filename="Pipfile.lock", extra=None, **kwargs):
|
|
extra = kwargs.pop("extra", [])
|
|
message = "{} {} {}".format(
|
|
crayons.normal("You need to run", bold=True),
|
|
crayons.red("$ pipenv lock", bold=True),
|
|
crayons.normal("before you can continue.", bold=True),
|
|
)
|
|
super().__init__(filename, message=message, extra=extra, **kwargs)
|
|
|
|
|
|
class DeployException(PipenvUsageError):
|
|
def __init__(self, message=None, **kwargs):
|
|
if not message:
|
|
message = str(crayons.normal("Aborting deploy", bold=True))
|
|
extra = kwargs.pop("extra", [])
|
|
PipenvUsageError.__init__(self, message=message, extra=extra, **kwargs)
|
|
|
|
|
|
class PipenvOptionsError(PipenvUsageError):
|
|
def __init__(self, option_name, message=None, ctx=None, **kwargs):
|
|
extra = kwargs.pop("extra", [])
|
|
PipenvUsageError.__init__(self, message=message, ctx=ctx, **kwargs)
|
|
self.extra = extra
|
|
self.option_name = option_name
|
|
|
|
|
|
class SystemUsageError(PipenvOptionsError):
|
|
def __init__(self, option_name="system", message=None, ctx=None, **kwargs):
|
|
extra = kwargs.pop("extra", [])
|
|
extra += [
|
|
"{}: --system is intended to be used for Pipfile installation, "
|
|
"not installation of specific packages. Aborting.".format(
|
|
crayons.red("Warning", bold=True)
|
|
),
|
|
]
|
|
if message is None:
|
|
message = str(
|
|
crayons.cyan("See also: {}".format(crayons.normal("--deploy flag.")))
|
|
)
|
|
super().__init__(option_name, message=message, ctx=ctx, extra=extra, **kwargs)
|
|
|
|
|
|
class SetupException(PipenvException):
|
|
def __init__(self, message=None, **kwargs):
|
|
PipenvException.__init__(self, message, **kwargs)
|
|
|
|
|
|
class VirtualenvException(PipenvException):
|
|
def __init__(self, message=None, **kwargs):
|
|
if not message:
|
|
message = (
|
|
"There was an unexpected error while activating your virtualenv. "
|
|
"Continuing anyway..."
|
|
)
|
|
PipenvException.__init__(self, message, **kwargs)
|
|
|
|
|
|
class VirtualenvActivationException(VirtualenvException):
|
|
def __init__(self, message=None, **kwargs):
|
|
if not message:
|
|
message = (
|
|
"activate_this.py not found. Your environment is most certainly "
|
|
"not activated. Continuing anyway..."
|
|
)
|
|
self.message = message
|
|
VirtualenvException.__init__(self, message, **kwargs)
|
|
|
|
|
|
class VirtualenvCreationException(VirtualenvException):
|
|
def __init__(self, message=None, **kwargs):
|
|
if not message:
|
|
message = "Failed to create virtual environment."
|
|
self.message = message
|
|
extra = kwargs.pop("extra", None)
|
|
if extra is not None and isinstance(extra, STRING_TYPES):
|
|
# note we need the format interpolation because ``crayons.ColoredString``
|
|
# is not an actual string type but is only a preparation for interpolation
|
|
# so replacement or parsing requires this step
|
|
extra = ANSI_REMOVAL_RE.sub("", f"{extra}")
|
|
if "KeyboardInterrupt" in extra:
|
|
extra = str(
|
|
crayons.red("Virtualenv creation interrupted by user", bold=True)
|
|
)
|
|
self.extra = extra = [extra]
|
|
VirtualenvException.__init__(self, message, extra=extra)
|
|
|
|
|
|
class UninstallError(PipenvException):
|
|
def __init__(self, package, command, return_values, return_code, **kwargs):
|
|
extra = [
|
|
"{} {}".format(
|
|
crayons.cyan("Attempted to run command: "),
|
|
crayons.yellow(f"$ {command!r}", bold=True),
|
|
)
|
|
]
|
|
extra.extend([crayons.cyan(line.strip()) for line in return_values.splitlines()])
|
|
if isinstance(package, (tuple, list, set)):
|
|
package = " ".join(package)
|
|
message = "{!s} {!s}...".format(
|
|
crayons.normal("Failed to uninstall package(s)"),
|
|
crayons.yellow(f"{package}!s", bold=True),
|
|
)
|
|
self.exit_code = return_code
|
|
PipenvException.__init__(self, message=message, extra=extra)
|
|
self.extra = extra
|
|
|
|
|
|
class InstallError(PipenvException):
|
|
def __init__(self, package, **kwargs):
|
|
package_message = ""
|
|
if package is not None:
|
|
package_message = "Couldn't install package: {}\n".format(
|
|
crayons.normal(f"{package!s}", bold=True)
|
|
)
|
|
message = "{} {}".format(
|
|
f"{package_message}", crayons.yellow("Package installation failed...")
|
|
)
|
|
extra = kwargs.pop("extra", [])
|
|
PipenvException.__init__(self, message=message, extra=extra, **kwargs)
|
|
|
|
|
|
class CacheError(PipenvException):
|
|
def __init__(self, path, **kwargs):
|
|
message = "{} {}\n{}".format(
|
|
crayons.cyan("Corrupt cache file"),
|
|
crayons.normal(f"{path!s}"),
|
|
crayons.normal('Consider trying "pipenv lock --clear" to clear the cache.'),
|
|
)
|
|
PipenvException.__init__(self, message=message)
|
|
|
|
|
|
class DependencyConflict(PipenvException):
|
|
def __init__(self, message):
|
|
extra = [
|
|
"{} {}".format(
|
|
crayons.red("The operation failed...", bold=True),
|
|
crayons.red(
|
|
"A dependency conflict was detected and could not be resolved."
|
|
),
|
|
)
|
|
]
|
|
PipenvException.__init__(self, message, extra=extra)
|
|
|
|
|
|
class ResolutionFailure(PipenvException):
|
|
def __init__(self, message, no_version_found=False):
|
|
extra = (
|
|
"{}: Your dependencies could not be resolved. You likely have a "
|
|
"mismatch in your sub-dependencies.\n "
|
|
"You can use {} to bypass this mechanism, then run "
|
|
"{} to inspect the situation.\n "
|
|
"Hint: try {} if it is a pre-release dependency."
|
|
"".format(
|
|
crayons.red("Warning", bold=True),
|
|
crayons.yellow("$ pipenv install --skip-lock"),
|
|
crayons.yellow("$ pipenv graph"),
|
|
crayons.yellow("$ pipenv lock --pre"),
|
|
),
|
|
)
|
|
if "no version found at all" in message:
|
|
no_version_found = True
|
|
message = crayons.yellow(f"{message}")
|
|
if no_version_found:
|
|
message = "{}\n{}".format(
|
|
message,
|
|
crayons.cyan(
|
|
"Please check your version specifier and version number. "
|
|
"See PEP440 for more information."
|
|
),
|
|
)
|
|
PipenvException.__init__(self, message, extra=extra)
|
|
|
|
|
|
class RequirementError(PipenvException):
|
|
def __init__(self, req=None):
|
|
from .utils import VCS_LIST
|
|
|
|
keys = (
|
|
(
|
|
"name",
|
|
"path",
|
|
)
|
|
+ VCS_LIST
|
|
+ ("line", "uri", "url", "relpath")
|
|
)
|
|
if req is not None:
|
|
possible_display_values = [getattr(req, value, None) for value in keys]
|
|
req_value = next(
|
|
iter(val for val in possible_display_values if val is not None), None
|
|
)
|
|
if not req_value:
|
|
getstate_fn = getattr(req, "__getstate__", None)
|
|
slots = getattr(req, "__slots__", None)
|
|
keys_fn = getattr(req, "keys", None)
|
|
if getstate_fn:
|
|
req_value = getstate_fn()
|
|
elif slots:
|
|
slot_vals = [
|
|
(k, getattr(req, k, None)) for k in slots if getattr(req, k, None)
|
|
]
|
|
req_value = "\n".join([f" {k}: {v}" for k, v in slot_vals])
|
|
elif keys_fn:
|
|
values = [(k, req.get(k)) for k in keys_fn() if req.get(k)]
|
|
req_value = "\n".join([f" {k}: {v}" for k, v in values])
|
|
else:
|
|
req_value = getattr(req.line_instance, "line", None)
|
|
message = "{} {}".format(
|
|
crayons.normal(decode_for_output("Failed creating requirement instance")),
|
|
crayons.normal(decode_for_output(f"{req_value!r}")),
|
|
)
|
|
extra = [str(req)]
|
|
PipenvException.__init__(self, message, extra=extra)
|
|
|
|
|
|
def prettify_exc(error):
|
|
"""Catch known errors and prettify them instead of showing the
|
|
entire traceback, for better UX"""
|
|
errors = []
|
|
for exc in KNOWN_EXCEPTIONS:
|
|
search_string = exc.match_string if exc.match_string else exc.exception_name
|
|
split_string = (
|
|
exc.show_from_string if exc.show_from_string else exc.exception_name
|
|
)
|
|
if search_string in error:
|
|
# for known exceptions with no display rules and no prefix
|
|
# we should simply show nothing
|
|
if not exc.show_from_string and not exc.prefix:
|
|
errors.append("")
|
|
continue
|
|
elif exc.prefix and exc.prefix in error:
|
|
_, error, info = error.rpartition(exc.prefix)
|
|
else:
|
|
_, error, info = error.rpartition(split_string)
|
|
errors.append(f"{error} {info}")
|
|
if not errors:
|
|
return f"{vistir.misc.decode_for_output(error)}"
|
|
|
|
return "\n".join(errors)
|