From edc6d31533c9da64c8b9bce2d322487b2168fc18 Mon Sep 17 00:00:00 2001 From: Blazej Michalik Date: Mon, 4 Jan 2021 23:41:19 +0100 Subject: [PATCH] Add --debug flag to base command This flag makes the main subroutine (cli.base_command.Command.run) withold from intercepting unhandled exceptions. This means, that debugging via "python -m pdb -m pip" is now possible. --- news/9349.feature.rst | 1 + src/pip/_internal/cli/base_command.py | 97 ++++++++++++++++----------- src/pip/_internal/cli/cmdoptions.py | 13 ++++ 3 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 news/9349.feature.rst diff --git a/news/9349.feature.rst b/news/9349.feature.rst new file mode 100644 index 00000000000..2af97e357c3 --- /dev/null +++ b/news/9349.feature.rst @@ -0,0 +1 @@ +Add "--debug" flag. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 16a8faca82a..2f329338ea6 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -1,5 +1,6 @@ """Base Command class, and related routines""" +import functools import logging import logging.config import optparse @@ -7,7 +8,7 @@ import sys import traceback from optparse import Values -from typing import List, Optional, Tuple +from typing import Any, Callable, List, Optional, Tuple from pip._internal.cli import cmdoptions from pip._internal.cli.command_context import CommandContextMixIn @@ -169,46 +170,60 @@ def _main(self, args: List[str]) -> int: "This will become an error in pip 21.0." ) + def intercepts_unhandled_exc(run_func): + # type: (Callable[..., int]) -> Callable[..., int] + @functools.wraps(run_func) + def exc_logging_wrapper(*args): + # type: (*Any) -> int + try: + status = run_func(*args) + assert isinstance(status, int) + return status + except PreviousBuildDirError as exc: + logger.critical(str(exc)) + logger.debug("Exception information:", exc_info=True) + + return PREVIOUS_BUILD_DIR_ERROR + except ( + InstallationError, + UninstallationError, + BadCommand, + NetworkConnectionError, + ) as exc: + logger.critical(str(exc)) + logger.debug("Exception information:", exc_info=True) + + return ERROR + except CommandError as exc: + logger.critical("%s", exc) + logger.debug("Exception information:", exc_info=True) + + return ERROR + except BrokenStdoutLoggingError: + # Bypass our logger and write any remaining messages to + # stderr because stdout no longer works. + print("ERROR: Pipe to stdout was broken", file=sys.stderr) + if level_number <= logging.DEBUG: + traceback.print_exc(file=sys.stderr) + + return ERROR + except KeyboardInterrupt: + logger.critical("Operation cancelled by user") + logger.debug("Exception information:", exc_info=True) + + return ERROR + except BaseException: + logger.critical("Exception:", exc_info=True) + + return UNKNOWN_ERROR + + return exc_logging_wrapper + try: - status = self.run(options, args) - assert isinstance(status, int) - return status - except PreviousBuildDirError as exc: - logger.critical(str(exc)) - logger.debug("Exception information:", exc_info=True) - - return PREVIOUS_BUILD_DIR_ERROR - except ( - InstallationError, - UninstallationError, - BadCommand, - NetworkConnectionError, - ) as exc: - logger.critical(str(exc)) - logger.debug("Exception information:", exc_info=True) - - return ERROR - except CommandError as exc: - logger.critical("%s", exc) - logger.debug("Exception information:", exc_info=True) - - return ERROR - except BrokenStdoutLoggingError: - # Bypass our logger and write any remaining messages to stderr - # because stdout no longer works. - print("ERROR: Pipe to stdout was broken", file=sys.stderr) - if level_number <= logging.DEBUG: - traceback.print_exc(file=sys.stderr) - - return ERROR - except KeyboardInterrupt: - logger.critical("Operation cancelled by user") - logger.debug("Exception information:", exc_info=True) - - return ERROR - except BaseException: - logger.critical("Exception:", exc_info=True) - - return UNKNOWN_ERROR + if not options.debug_mode: + run = intercepts_unhandled_exc(self.run) + else: + run = self.run + return run(options, args) finally: self.handle_pip_version_check(options) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index b4f0f83c679..7c69fd4a768 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -151,6 +151,18 @@ class PipOption(Option): help="Show help.", ) +debug_mode = partial( + Option, + "--debug", + dest="debug_mode", + action="store_true", + default=False, + help=( + "Let unhandled exceptions propagate outside the main subroutine, " + "instead of logging them to stderr." + ), +) # type: Callable[..., Option] + isolated_mode: Callable[..., Option] = partial( Option, "--isolated", @@ -974,6 +986,7 @@ def check_list_path_option(options: Values) -> None: "name": "General Options", "options": [ help_, + debug_mode, isolated_mode, require_virtualenv, verbose,