basecommand.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. """Base Command class, and related routines"""
  2. from __future__ import absolute_import
  3. import logging
  4. import os
  5. import sys
  6. import traceback
  7. import optparse
  8. import warnings
  9. from pip._vendor.six import StringIO
  10. from pip import cmdoptions
  11. from pip.locations import running_under_virtualenv
  12. from pip.download import PipSession
  13. from pip.exceptions import (BadCommand, InstallationError, UninstallationError,
  14. CommandError, PreviousBuildDirError)
  15. from pip.compat import logging_dictConfig
  16. from pip.baseparser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
  17. from pip.status_codes import (
  18. SUCCESS, ERROR, UNKNOWN_ERROR, VIRTUALENV_NOT_FOUND,
  19. PREVIOUS_BUILD_DIR_ERROR,
  20. )
  21. from pip.utils import appdirs, get_prog, normalize_path
  22. from pip.utils.deprecation import RemovedInPip8Warning
  23. from pip.utils.filesystem import check_path_owner
  24. from pip.utils.logging import IndentingFormatter
  25. from pip.utils.outdated import pip_version_check
  26. __all__ = ['Command']
  27. logger = logging.getLogger(__name__)
  28. class Command(object):
  29. name = None
  30. usage = None
  31. hidden = False
  32. log_streams = ("ext://sys.stdout", "ext://sys.stderr")
  33. def __init__(self, isolated=False):
  34. parser_kw = {
  35. 'usage': self.usage,
  36. 'prog': '%s %s' % (get_prog(), self.name),
  37. 'formatter': UpdatingDefaultsHelpFormatter(),
  38. 'add_help_option': False,
  39. 'name': self.name,
  40. 'description': self.__doc__,
  41. 'isolated': isolated,
  42. }
  43. self.parser = ConfigOptionParser(**parser_kw)
  44. # Commands should add options to this option group
  45. optgroup_name = '%s Options' % self.name.capitalize()
  46. self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name)
  47. # Add the general options
  48. gen_opts = cmdoptions.make_option_group(
  49. cmdoptions.general_group,
  50. self.parser,
  51. )
  52. self.parser.add_option_group(gen_opts)
  53. def _build_session(self, options, retries=None, timeout=None):
  54. session = PipSession(
  55. cache=(
  56. normalize_path(os.path.join(options.cache_dir, "http"))
  57. if options.cache_dir else None
  58. ),
  59. retries=retries if retries is not None else options.retries,
  60. insecure_hosts=options.trusted_hosts,
  61. )
  62. # Handle custom ca-bundles from the user
  63. if options.cert:
  64. session.verify = options.cert
  65. # Handle SSL client certificate
  66. if options.client_cert:
  67. session.cert = options.client_cert
  68. # Handle timeouts
  69. if options.timeout or timeout:
  70. session.timeout = (
  71. timeout if timeout is not None else options.timeout
  72. )
  73. # Handle configured proxies
  74. if options.proxy:
  75. session.proxies = {
  76. "http": options.proxy,
  77. "https": options.proxy,
  78. }
  79. # Determine if we can prompt the user for authentication or not
  80. session.auth.prompting = not options.no_input
  81. return session
  82. def parse_args(self, args):
  83. # factored out for testability
  84. return self.parser.parse_args(args)
  85. def main(self, args):
  86. options, args = self.parse_args(args)
  87. if options.quiet:
  88. level = "WARNING"
  89. elif options.verbose:
  90. level = "DEBUG"
  91. else:
  92. level = "INFO"
  93. # Compute the path for our debug log.
  94. debug_log_path = os.path.join(appdirs.user_log_dir("pip"), "debug.log")
  95. # Ensure that the path for our debug log is owned by the current user
  96. # and if it is not, disable the debug log.
  97. write_debug_log = check_path_owner(debug_log_path)
  98. logging_dictConfig({
  99. "version": 1,
  100. "disable_existing_loggers": False,
  101. "filters": {
  102. "exclude_warnings": {
  103. "()": "pip.utils.logging.MaxLevelFilter",
  104. "level": logging.WARNING,
  105. },
  106. },
  107. "formatters": {
  108. "indent": {
  109. "()": IndentingFormatter,
  110. "format": (
  111. "%(message)s"
  112. if not options.log_explicit_levels
  113. else "[%(levelname)s] %(message)s"
  114. ),
  115. },
  116. },
  117. "handlers": {
  118. "console": {
  119. "level": level,
  120. "class": "pip.utils.logging.ColorizedStreamHandler",
  121. "stream": self.log_streams[0],
  122. "filters": ["exclude_warnings"],
  123. "formatter": "indent",
  124. },
  125. "console_errors": {
  126. "level": "WARNING",
  127. "class": "pip.utils.logging.ColorizedStreamHandler",
  128. "stream": self.log_streams[1],
  129. "formatter": "indent",
  130. },
  131. "debug_log": {
  132. "level": "DEBUG",
  133. "class": "pip.utils.logging.BetterRotatingFileHandler",
  134. "filename": debug_log_path,
  135. "maxBytes": 10 * 1000 * 1000, # 10 MB
  136. "backupCount": 1,
  137. "delay": True,
  138. "formatter": "indent",
  139. },
  140. "user_log": {
  141. "level": "DEBUG",
  142. "class": "pip.utils.logging.BetterRotatingFileHandler",
  143. "filename": options.log or "/dev/null",
  144. "delay": True,
  145. "formatter": "indent",
  146. },
  147. },
  148. "root": {
  149. "level": level,
  150. "handlers": list(filter(None, [
  151. "console",
  152. "console_errors",
  153. "debug_log" if write_debug_log else None,
  154. "user_log" if options.log else None,
  155. ])),
  156. },
  157. # Disable any logging besides WARNING unless we have DEBUG level
  158. # logging enabled. These use both pip._vendor and the bare names
  159. # for the case where someone unbundles our libraries.
  160. "loggers": dict(
  161. (
  162. name,
  163. {
  164. "level": (
  165. "WARNING"
  166. if level in ["INFO", "ERROR"]
  167. else "DEBUG"
  168. ),
  169. },
  170. )
  171. for name in ["pip._vendor", "distlib", "requests", "urllib3"]
  172. ),
  173. })
  174. # We add this warning here instead of up above, because the logger
  175. # hasn't been configured until just now.
  176. if not write_debug_log:
  177. logger.warning(
  178. "The directory '%s' or its parent directory is not owned by "
  179. "the current user and the debug log has been disabled. Please "
  180. "check the permissions and owner of that directory. If "
  181. "executing pip with sudo, you may want sudo's -H flag.",
  182. os.path.dirname(debug_log_path),
  183. )
  184. if options.log_explicit_levels:
  185. warnings.warn(
  186. "--log-explicit-levels has been deprecated and will be removed"
  187. " in a future version.",
  188. RemovedInPip8Warning,
  189. )
  190. # TODO: try to get these passing down from the command?
  191. # without resorting to os.environ to hold these.
  192. if options.no_input:
  193. os.environ['PIP_NO_INPUT'] = '1'
  194. if options.exists_action:
  195. os.environ['PIP_EXISTS_ACTION'] = ' '.join(options.exists_action)
  196. if options.require_venv:
  197. # If a venv is required check if it can really be found
  198. if not running_under_virtualenv():
  199. logger.critical(
  200. 'Could not find an activated virtualenv (required).'
  201. )
  202. sys.exit(VIRTUALENV_NOT_FOUND)
  203. # Check if we're using the latest version of pip available
  204. if (not options.disable_pip_version_check and not
  205. getattr(options, "no_index", False)):
  206. with self._build_session(
  207. options,
  208. retries=0,
  209. timeout=min(5, options.timeout)) as session:
  210. pip_version_check(session)
  211. try:
  212. status = self.run(options, args)
  213. # FIXME: all commands should return an exit status
  214. # and when it is done, isinstance is not needed anymore
  215. if isinstance(status, int):
  216. return status
  217. except PreviousBuildDirError as exc:
  218. logger.critical(str(exc))
  219. logger.debug('Exception information:\n%s', format_exc())
  220. return PREVIOUS_BUILD_DIR_ERROR
  221. except (InstallationError, UninstallationError, BadCommand) as exc:
  222. logger.critical(str(exc))
  223. logger.debug('Exception information:\n%s', format_exc())
  224. return ERROR
  225. except CommandError as exc:
  226. logger.critical('ERROR: %s', exc)
  227. logger.debug('Exception information:\n%s', format_exc())
  228. return ERROR
  229. except KeyboardInterrupt:
  230. logger.critical('Operation cancelled by user')
  231. logger.debug('Exception information:\n%s', format_exc())
  232. return ERROR
  233. except:
  234. logger.critical('Exception:\n%s', format_exc())
  235. return UNKNOWN_ERROR
  236. return SUCCESS
  237. def format_exc(exc_info=None):
  238. if exc_info is None:
  239. exc_info = sys.exc_info()
  240. out = StringIO()
  241. traceback.print_exception(*exc_info, **dict(file=out))
  242. return out.getvalue()