base_command.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. """Base Command class, and related routines"""
  2. from __future__ import annotations
  3. import logging
  4. import logging.config
  5. import optparse
  6. import os
  7. import sys
  8. import traceback
  9. from optparse import Values
  10. from typing import Callable
  11. from pip._vendor.rich import reconfigure
  12. from pip._vendor.rich import traceback as rich_traceback
  13. from pip._internal.cli import cmdoptions
  14. from pip._internal.cli.command_context import CommandContextMixIn
  15. from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
  16. from pip._internal.cli.status_codes import (
  17. ERROR,
  18. PREVIOUS_BUILD_DIR_ERROR,
  19. UNKNOWN_ERROR,
  20. VIRTUALENV_NOT_FOUND,
  21. )
  22. from pip._internal.exceptions import (
  23. BadCommand,
  24. CommandError,
  25. DiagnosticPipError,
  26. InstallationError,
  27. NetworkConnectionError,
  28. PreviousBuildDirError,
  29. )
  30. from pip._internal.utils.filesystem import check_path_owner
  31. from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
  32. from pip._internal.utils.misc import get_prog, normalize_path
  33. from pip._internal.utils.temp_dir import TempDirectoryTypeRegistry as TempDirRegistry
  34. from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry
  35. from pip._internal.utils.virtualenv import running_under_virtualenv
  36. __all__ = ["Command"]
  37. logger = logging.getLogger(__name__)
  38. class Command(CommandContextMixIn):
  39. usage: str = ""
  40. ignore_require_venv: bool = False
  41. def __init__(self, name: str, summary: str, isolated: bool = False) -> None:
  42. super().__init__()
  43. self.name = name
  44. self.summary = summary
  45. self.parser = ConfigOptionParser(
  46. usage=self.usage,
  47. prog=f"{get_prog()} {name}",
  48. formatter=UpdatingDefaultsHelpFormatter(),
  49. add_help_option=False,
  50. name=name,
  51. description=self.__doc__,
  52. isolated=isolated,
  53. )
  54. self.tempdir_registry: TempDirRegistry | None = None
  55. # Commands should add options to this option group
  56. optgroup_name = f"{self.name.capitalize()} Options"
  57. self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name)
  58. # Add the general options
  59. gen_opts = cmdoptions.make_option_group(
  60. cmdoptions.general_group,
  61. self.parser,
  62. )
  63. self.parser.add_option_group(gen_opts)
  64. self.add_options()
  65. def add_options(self) -> None:
  66. pass
  67. def handle_pip_version_check(self, options: Values) -> None:
  68. """
  69. This is a no-op so that commands by default do not do the pip version
  70. check.
  71. """
  72. # Make sure we do the pip version check if the index_group options
  73. # are present.
  74. assert not hasattr(options, "no_index")
  75. def run(self, options: Values, args: list[str]) -> int:
  76. raise NotImplementedError
  77. def _run_wrapper(self, level_number: int, options: Values, args: list[str]) -> int:
  78. def _inner_run() -> int:
  79. try:
  80. return self.run(options, args)
  81. finally:
  82. self.handle_pip_version_check(options)
  83. if options.debug_mode:
  84. rich_traceback.install(show_locals=True)
  85. return _inner_run()
  86. try:
  87. status = _inner_run()
  88. assert isinstance(status, int)
  89. return status
  90. except DiagnosticPipError as exc:
  91. logger.error("%s", exc, extra={"rich": True})
  92. logger.debug("Exception information:", exc_info=True)
  93. return ERROR
  94. except PreviousBuildDirError as exc:
  95. logger.critical(str(exc))
  96. logger.debug("Exception information:", exc_info=True)
  97. return PREVIOUS_BUILD_DIR_ERROR
  98. except (
  99. InstallationError,
  100. BadCommand,
  101. NetworkConnectionError,
  102. ) as exc:
  103. logger.critical(str(exc))
  104. logger.debug("Exception information:", exc_info=True)
  105. return ERROR
  106. except CommandError as exc:
  107. logger.critical("%s", exc)
  108. logger.debug("Exception information:", exc_info=True)
  109. return ERROR
  110. except BrokenStdoutLoggingError:
  111. # Bypass our logger and write any remaining messages to
  112. # stderr because stdout no longer works.
  113. print("ERROR: Pipe to stdout was broken", file=sys.stderr)
  114. if level_number <= logging.DEBUG:
  115. traceback.print_exc(file=sys.stderr)
  116. return ERROR
  117. except KeyboardInterrupt:
  118. logger.critical("Operation cancelled by user")
  119. logger.debug("Exception information:", exc_info=True)
  120. return ERROR
  121. except BaseException:
  122. logger.critical("Exception:", exc_info=True)
  123. return UNKNOWN_ERROR
  124. def parse_args(self, args: list[str]) -> tuple[Values, list[str]]:
  125. # factored out for testability
  126. return self.parser.parse_args(args)
  127. def main(self, args: list[str]) -> int:
  128. try:
  129. with self.main_context():
  130. return self._main(args)
  131. finally:
  132. logging.shutdown()
  133. def _main(self, args: list[str]) -> int:
  134. # We must initialize this before the tempdir manager, otherwise the
  135. # configuration would not be accessible by the time we clean up the
  136. # tempdir manager.
  137. self.tempdir_registry = self.enter_context(tempdir_registry())
  138. # Intentionally set as early as possible so globally-managed temporary
  139. # directories are available to the rest of the code.
  140. self.enter_context(global_tempdir_manager())
  141. options, args = self.parse_args(args)
  142. # Set verbosity so that it can be used elsewhere.
  143. self.verbosity = options.verbose - options.quiet
  144. if options.debug_mode:
  145. self.verbosity = 2
  146. if hasattr(options, "progress_bar") and options.progress_bar == "auto":
  147. options.progress_bar = "on" if self.verbosity >= 0 else "off"
  148. reconfigure(no_color=options.no_color)
  149. level_number = setup_logging(
  150. verbosity=self.verbosity,
  151. no_color=options.no_color,
  152. user_log_file=options.log,
  153. )
  154. always_enabled_features = set(options.features_enabled) & set(
  155. cmdoptions.ALWAYS_ENABLED_FEATURES
  156. )
  157. if always_enabled_features:
  158. logger.warning(
  159. "The following features are always enabled: %s. ",
  160. ", ".join(sorted(always_enabled_features)),
  161. )
  162. # Make sure that the --python argument isn't specified after the
  163. # subcommand. We can tell, because if --python was specified,
  164. # we should only reach this point if we're running in the created
  165. # subprocess, which has the _PIP_RUNNING_IN_SUBPROCESS environment
  166. # variable set.
  167. if options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
  168. logger.critical(
  169. "The --python option must be placed before the pip subcommand name"
  170. )
  171. sys.exit(ERROR)
  172. # TODO: Try to get these passing down from the command?
  173. # without resorting to os.environ to hold these.
  174. # This also affects isolated builds and it should.
  175. if options.no_input:
  176. os.environ["PIP_NO_INPUT"] = "1"
  177. if options.exists_action:
  178. os.environ["PIP_EXISTS_ACTION"] = " ".join(options.exists_action)
  179. if options.require_venv and not self.ignore_require_venv:
  180. # If a venv is required check if it can really be found
  181. if not running_under_virtualenv():
  182. logger.critical("Could not find an activated virtualenv (required).")
  183. sys.exit(VIRTUALENV_NOT_FOUND)
  184. if options.cache_dir:
  185. options.cache_dir = normalize_path(options.cache_dir)
  186. if not check_path_owner(options.cache_dir):
  187. logger.warning(
  188. "The directory '%s' or its parent directory is not owned "
  189. "or is not writable by the current user. The cache "
  190. "has been disabled. Check the permissions and owner of "
  191. "that directory. If executing pip with sudo, you should "
  192. "use sudo's -H flag.",
  193. options.cache_dir,
  194. )
  195. options.cache_dir = None
  196. if (
  197. "inprocess-build-deps" in options.features_enabled
  198. and os.environ.get("PIP_CONSTRAINT", "")
  199. and "build-constraint" not in options.features_enabled
  200. ):
  201. logger.warning(
  202. "In-process build dependencies are enabled, "
  203. "PIP_CONSTRAINT will have no effect for build dependencies"
  204. )
  205. options.features_enabled.append("build-constraint")
  206. return self._run_wrapper(level_number, options, args)
  207. def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
  208. """
  209. map of names to handler actions for commands with sub-actions
  210. """
  211. return {}