You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

basecommand.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. """Base Command class, and related routines"""
  2. from __future__ import absolute_import
  3. import logging
  4. import logging.config
  5. import optparse
  6. import os
  7. import sys
  8. import warnings
  9. from pip._internal import cmdoptions
  10. from pip._internal.baseparser import (
  11. ConfigOptionParser, UpdatingDefaultsHelpFormatter,
  12. )
  13. from pip._internal.compat import WINDOWS
  14. from pip._internal.download import PipSession
  15. from pip._internal.exceptions import (
  16. BadCommand, CommandError, InstallationError, PreviousBuildDirError,
  17. UninstallationError,
  18. )
  19. from pip._internal.index import PackageFinder
  20. from pip._internal.locations import running_under_virtualenv
  21. from pip._internal.req.req_file import parse_requirements
  22. from pip._internal.req.req_install import InstallRequirement
  23. from pip._internal.status_codes import (
  24. ERROR, PREVIOUS_BUILD_DIR_ERROR, SUCCESS, UNKNOWN_ERROR,
  25. VIRTUALENV_NOT_FOUND,
  26. )
  27. from pip._internal.utils import deprecation
  28. from pip._internal.utils.logging import IndentingFormatter
  29. from pip._internal.utils.misc import get_prog, normalize_path
  30. from pip._internal.utils.outdated import pip_version_check
  31. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  32. if MYPY_CHECK_RUNNING:
  33. from typing import Optional
  34. __all__ = ['Command']
  35. logger = logging.getLogger(__name__)
  36. class Command(object):
  37. name = None # type: Optional[str]
  38. usage = None # type: Optional[str]
  39. hidden = False # type: bool
  40. ignore_require_venv = False # type: bool
  41. log_streams = ("ext://sys.stdout", "ext://sys.stderr")
  42. def __init__(self, isolated=False):
  43. parser_kw = {
  44. 'usage': self.usage,
  45. 'prog': '%s %s' % (get_prog(), self.name),
  46. 'formatter': UpdatingDefaultsHelpFormatter(),
  47. 'add_help_option': False,
  48. 'name': self.name,
  49. 'description': self.__doc__,
  50. 'isolated': isolated,
  51. }
  52. self.parser = ConfigOptionParser(**parser_kw)
  53. # Commands should add options to this option group
  54. optgroup_name = '%s Options' % self.name.capitalize()
  55. self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name)
  56. # Add the general options
  57. gen_opts = cmdoptions.make_option_group(
  58. cmdoptions.general_group,
  59. self.parser,
  60. )
  61. self.parser.add_option_group(gen_opts)
  62. def _build_session(self, options, retries=None, timeout=None):
  63. session = PipSession(
  64. cache=(
  65. normalize_path(os.path.join(options.cache_dir, "http"))
  66. if options.cache_dir else None
  67. ),
  68. retries=retries if retries is not None else options.retries,
  69. insecure_hosts=options.trusted_hosts,
  70. )
  71. # Handle custom ca-bundles from the user
  72. if options.cert:
  73. session.verify = options.cert
  74. # Handle SSL client certificate
  75. if options.client_cert:
  76. session.cert = options.client_cert
  77. # Handle timeouts
  78. if options.timeout or timeout:
  79. session.timeout = (
  80. timeout if timeout is not None else options.timeout
  81. )
  82. # Handle configured proxies
  83. if options.proxy:
  84. session.proxies = {
  85. "http": options.proxy,
  86. "https": options.proxy,
  87. }
  88. # Determine if we can prompt the user for authentication or not
  89. session.auth.prompting = not options.no_input
  90. return session
  91. def parse_args(self, args):
  92. # factored out for testability
  93. return self.parser.parse_args(args)
  94. def main(self, args):
  95. options, args = self.parse_args(args)
  96. # Set verbosity so that it can be used elsewhere.
  97. self.verbosity = options.verbose - options.quiet
  98. if self.verbosity >= 1:
  99. level = "DEBUG"
  100. elif self.verbosity == -1:
  101. level = "WARNING"
  102. elif self.verbosity == -2:
  103. level = "ERROR"
  104. elif self.verbosity <= -3:
  105. level = "CRITICAL"
  106. else:
  107. level = "INFO"
  108. # The root logger should match the "console" level *unless* we
  109. # specified "--log" to send debug logs to a file.
  110. root_level = level
  111. if options.log:
  112. root_level = "DEBUG"
  113. logger_class = "pip._internal.utils.logging.ColorizedStreamHandler"
  114. handler_class = "pip._internal.utils.logging.BetterRotatingFileHandler"
  115. logging.config.dictConfig({
  116. "version": 1,
  117. "disable_existing_loggers": False,
  118. "filters": {
  119. "exclude_warnings": {
  120. "()": "pip._internal.utils.logging.MaxLevelFilter",
  121. "level": logging.WARNING,
  122. },
  123. },
  124. "formatters": {
  125. "indent": {
  126. "()": IndentingFormatter,
  127. "format": "%(message)s",
  128. },
  129. },
  130. "handlers": {
  131. "console": {
  132. "level": level,
  133. "class": logger_class,
  134. "no_color": options.no_color,
  135. "stream": self.log_streams[0],
  136. "filters": ["exclude_warnings"],
  137. "formatter": "indent",
  138. },
  139. "console_errors": {
  140. "level": "WARNING",
  141. "class": logger_class,
  142. "no_color": options.no_color,
  143. "stream": self.log_streams[1],
  144. "formatter": "indent",
  145. },
  146. "user_log": {
  147. "level": "DEBUG",
  148. "class": handler_class,
  149. "filename": options.log or "/dev/null",
  150. "delay": True,
  151. "formatter": "indent",
  152. },
  153. },
  154. "root": {
  155. "level": root_level,
  156. "handlers": list(filter(None, [
  157. "console",
  158. "console_errors",
  159. "user_log" if options.log else None,
  160. ])),
  161. },
  162. # Disable any logging besides WARNING unless we have DEBUG level
  163. # logging enabled. These use both pip._vendor and the bare names
  164. # for the case where someone unbundles our libraries.
  165. "loggers": {
  166. name: {
  167. "level": (
  168. "WARNING" if level in ["INFO", "ERROR"] else "DEBUG"
  169. )
  170. } for name in [
  171. "pip._vendor", "distlib", "requests", "urllib3"
  172. ]
  173. },
  174. })
  175. if sys.version_info[:2] == (3, 3):
  176. warnings.warn(
  177. "Python 3.3 supported has been deprecated and support for it "
  178. "will be dropped in the future. Please upgrade your Python.",
  179. deprecation.RemovedInPip11Warning,
  180. )
  181. # TODO: try to get these passing down from the command?
  182. # without resorting to os.environ to hold these.
  183. if options.no_input:
  184. os.environ['PIP_NO_INPUT'] = '1'
  185. if options.exists_action:
  186. os.environ['PIP_EXISTS_ACTION'] = ' '.join(options.exists_action)
  187. if options.require_venv and not self.ignore_require_venv:
  188. # If a venv is required check if it can really be found
  189. if not running_under_virtualenv():
  190. logger.critical(
  191. 'Could not find an activated virtualenv (required).'
  192. )
  193. sys.exit(VIRTUALENV_NOT_FOUND)
  194. original_root_handlers = set(logging.root.handlers)
  195. try:
  196. status = self.run(options, args)
  197. # FIXME: all commands should return an exit status
  198. # and when it is done, isinstance is not needed anymore
  199. if isinstance(status, int):
  200. return status
  201. except PreviousBuildDirError as exc:
  202. logger.critical(str(exc))
  203. logger.debug('Exception information:', exc_info=True)
  204. return PREVIOUS_BUILD_DIR_ERROR
  205. except (InstallationError, UninstallationError, BadCommand) as exc:
  206. logger.critical(str(exc))
  207. logger.debug('Exception information:', exc_info=True)
  208. return ERROR
  209. except CommandError as exc:
  210. logger.critical('ERROR: %s', exc)
  211. logger.debug('Exception information:', exc_info=True)
  212. return ERROR
  213. except KeyboardInterrupt:
  214. logger.critical('Operation cancelled by user')
  215. logger.debug('Exception information:', exc_info=True)
  216. return ERROR
  217. except:
  218. logger.critical('Exception:', exc_info=True)
  219. return UNKNOWN_ERROR
  220. finally:
  221. # Check if we're using the latest version of pip available
  222. if (not options.disable_pip_version_check and not
  223. getattr(options, "no_index", False)):
  224. with self._build_session(
  225. options,
  226. retries=0,
  227. timeout=min(5, options.timeout)) as session:
  228. pip_version_check(session, options)
  229. # Avoid leaking loggers
  230. for handler in set(logging.root.handlers) - original_root_handlers:
  231. # this method benefit from the Logger class internal lock
  232. logging.root.removeHandler(handler)
  233. return SUCCESS
  234. class RequirementCommand(Command):
  235. @staticmethod
  236. def populate_requirement_set(requirement_set, args, options, finder,
  237. session, name, wheel_cache):
  238. """
  239. Marshal cmd line args into a requirement set.
  240. """
  241. # NOTE: As a side-effect, options.require_hashes and
  242. # requirement_set.require_hashes may be updated
  243. for filename in options.constraints:
  244. for req_to_add in parse_requirements(
  245. filename,
  246. constraint=True, finder=finder, options=options,
  247. session=session, wheel_cache=wheel_cache):
  248. req_to_add.is_direct = True
  249. requirement_set.add_requirement(req_to_add)
  250. for req in args:
  251. req_to_add = InstallRequirement.from_line(
  252. req, None, isolated=options.isolated_mode,
  253. wheel_cache=wheel_cache
  254. )
  255. req_to_add.is_direct = True
  256. requirement_set.add_requirement(req_to_add)
  257. for req in options.editables:
  258. req_to_add = InstallRequirement.from_editable(
  259. req,
  260. isolated=options.isolated_mode,
  261. wheel_cache=wheel_cache
  262. )
  263. req_to_add.is_direct = True
  264. requirement_set.add_requirement(req_to_add)
  265. for filename in options.requirements:
  266. for req_to_add in parse_requirements(
  267. filename,
  268. finder=finder, options=options, session=session,
  269. wheel_cache=wheel_cache):
  270. req_to_add.is_direct = True
  271. requirement_set.add_requirement(req_to_add)
  272. # If --require-hashes was a line in a requirements file, tell
  273. # RequirementSet about it:
  274. requirement_set.require_hashes = options.require_hashes
  275. if not (args or options.editables or options.requirements):
  276. opts = {'name': name}
  277. if options.find_links:
  278. raise CommandError(
  279. 'You must give at least one requirement to %(name)s '
  280. '(maybe you meant "pip %(name)s %(links)s"?)' %
  281. dict(opts, links=' '.join(options.find_links)))
  282. else:
  283. raise CommandError(
  284. 'You must give at least one requirement to %(name)s '
  285. '(see "pip help %(name)s")' % opts)
  286. # On Windows, any operation modifying pip should be run as:
  287. # python -m pip ...
  288. # See https://github.com/pypa/pip/issues/1299 for more discussion
  289. should_show_use_python_msg = (
  290. WINDOWS and
  291. requirement_set.has_requirement("pip") and
  292. os.path.basename(sys.argv[0]).startswith("pip")
  293. )
  294. if should_show_use_python_msg:
  295. new_command = [
  296. sys.executable, "-m", "pip"
  297. ] + sys.argv[1:]
  298. raise CommandError(
  299. 'To modify pip, please run the following command:\n{}'
  300. .format(" ".join(new_command))
  301. )
  302. def _build_package_finder(self, options, session,
  303. platform=None, python_versions=None,
  304. abi=None, implementation=None):
  305. """
  306. Create a package finder appropriate to this requirement command.
  307. """
  308. index_urls = [options.index_url] + options.extra_index_urls
  309. if options.no_index:
  310. logger.debug('Ignoring indexes: %s', ','.join(index_urls))
  311. index_urls = []
  312. return PackageFinder(
  313. find_links=options.find_links,
  314. format_control=options.format_control,
  315. index_urls=index_urls,
  316. trusted_hosts=options.trusted_hosts,
  317. allow_all_prereleases=options.pre,
  318. process_dependency_links=options.process_dependency_links,
  319. session=session,
  320. platform=platform,
  321. versions=python_versions,
  322. abi=abi,
  323. implementation=implementation,
  324. )