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.

base_command.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. """Base Command class, and related routines"""
  2. from __future__ import absolute_import, print_function
  3. import logging
  4. import logging.config
  5. import optparse
  6. import os
  7. import platform
  8. import sys
  9. import traceback
  10. from pip._internal.cli import cmdoptions
  11. from pip._internal.cli.parser import (
  12. ConfigOptionParser, UpdatingDefaultsHelpFormatter,
  13. )
  14. from pip._internal.cli.status_codes import (
  15. ERROR, PREVIOUS_BUILD_DIR_ERROR, SUCCESS, UNKNOWN_ERROR,
  16. VIRTUALENV_NOT_FOUND,
  17. )
  18. from pip._internal.download import PipSession
  19. from pip._internal.exceptions import (
  20. BadCommand, CommandError, InstallationError, PreviousBuildDirError,
  21. UninstallationError,
  22. )
  23. from pip._internal.index import PackageFinder
  24. from pip._internal.locations import running_under_virtualenv
  25. from pip._internal.req.constructors import (
  26. install_req_from_editable, install_req_from_line,
  27. )
  28. from pip._internal.req.req_file import parse_requirements
  29. from pip._internal.utils.deprecation import deprecated
  30. from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
  31. from pip._internal.utils.misc import (
  32. get_prog, normalize_path, redact_password_from_url,
  33. )
  34. from pip._internal.utils.outdated import pip_version_check
  35. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  36. if MYPY_CHECK_RUNNING:
  37. from typing import Optional, List, Tuple, Any # noqa: F401
  38. from optparse import Values # noqa: F401
  39. from pip._internal.cache import WheelCache # noqa: F401
  40. from pip._internal.req.req_set import RequirementSet # noqa: F401
  41. __all__ = ['Command']
  42. logger = logging.getLogger(__name__)
  43. class Command(object):
  44. name = None # type: Optional[str]
  45. usage = None # type: Optional[str]
  46. hidden = False # type: bool
  47. ignore_require_venv = False # type: bool
  48. def __init__(self, isolated=False):
  49. # type: (bool) -> None
  50. parser_kw = {
  51. 'usage': self.usage,
  52. 'prog': '%s %s' % (get_prog(), self.name),
  53. 'formatter': UpdatingDefaultsHelpFormatter(),
  54. 'add_help_option': False,
  55. 'name': self.name,
  56. 'description': self.__doc__,
  57. 'isolated': isolated,
  58. }
  59. self.parser = ConfigOptionParser(**parser_kw)
  60. # Commands should add options to this option group
  61. optgroup_name = '%s Options' % self.name.capitalize()
  62. self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name)
  63. # Add the general options
  64. gen_opts = cmdoptions.make_option_group(
  65. cmdoptions.general_group,
  66. self.parser,
  67. )
  68. self.parser.add_option_group(gen_opts)
  69. def run(self, options, args):
  70. # type: (Values, List[Any]) -> Any
  71. raise NotImplementedError
  72. def _build_session(self, options, retries=None, timeout=None):
  73. # type: (Values, Optional[int], Optional[int]) -> PipSession
  74. session = PipSession(
  75. cache=(
  76. normalize_path(os.path.join(options.cache_dir, "http"))
  77. if options.cache_dir else None
  78. ),
  79. retries=retries if retries is not None else options.retries,
  80. insecure_hosts=options.trusted_hosts,
  81. )
  82. # Handle custom ca-bundles from the user
  83. if options.cert:
  84. session.verify = options.cert
  85. # Handle SSL client certificate
  86. if options.client_cert:
  87. session.cert = options.client_cert
  88. # Handle timeouts
  89. if options.timeout or timeout:
  90. session.timeout = (
  91. timeout if timeout is not None else options.timeout
  92. )
  93. # Handle configured proxies
  94. if options.proxy:
  95. session.proxies = {
  96. "http": options.proxy,
  97. "https": options.proxy,
  98. }
  99. # Determine if we can prompt the user for authentication or not
  100. session.auth.prompting = not options.no_input
  101. return session
  102. def parse_args(self, args):
  103. # type: (List[str]) -> Tuple
  104. # factored out for testability
  105. return self.parser.parse_args(args)
  106. def main(self, args):
  107. # type: (List[str]) -> int
  108. options, args = self.parse_args(args)
  109. # Set verbosity so that it can be used elsewhere.
  110. self.verbosity = options.verbose - options.quiet
  111. level_number = setup_logging(
  112. verbosity=self.verbosity,
  113. no_color=options.no_color,
  114. user_log_file=options.log,
  115. )
  116. if sys.version_info[:2] == (3, 4):
  117. deprecated(
  118. "Python 3.4 support has been deprecated. pip 19.1 will be the "
  119. "last one supporting it. Please upgrade your Python as Python "
  120. "3.4 won't be maintained after March 2019 (cf PEP 429).",
  121. replacement=None,
  122. gone_in='19.2',
  123. )
  124. elif sys.version_info[:2] == (2, 7):
  125. message = (
  126. "A future version of pip will drop support for Python 2.7."
  127. )
  128. if platform.python_implementation() == "CPython":
  129. message = (
  130. "Python 2.7 will reach the end of its life on January "
  131. "1st, 2020. Please upgrade your Python as Python 2.7 "
  132. "won't be maintained after that date. "
  133. ) + message
  134. deprecated(message, replacement=None, gone_in=None)
  135. # TODO: Try to get these passing down from the command?
  136. # without resorting to os.environ to hold these.
  137. # This also affects isolated builds and it should.
  138. if options.no_input:
  139. os.environ['PIP_NO_INPUT'] = '1'
  140. if options.exists_action:
  141. os.environ['PIP_EXISTS_ACTION'] = ' '.join(options.exists_action)
  142. if options.require_venv and not self.ignore_require_venv:
  143. # If a venv is required check if it can really be found
  144. if not running_under_virtualenv():
  145. logger.critical(
  146. 'Could not find an activated virtualenv (required).'
  147. )
  148. sys.exit(VIRTUALENV_NOT_FOUND)
  149. try:
  150. status = self.run(options, args)
  151. # FIXME: all commands should return an exit status
  152. # and when it is done, isinstance is not needed anymore
  153. if isinstance(status, int):
  154. return status
  155. except PreviousBuildDirError as exc:
  156. logger.critical(str(exc))
  157. logger.debug('Exception information:', exc_info=True)
  158. return PREVIOUS_BUILD_DIR_ERROR
  159. except (InstallationError, UninstallationError, BadCommand) as exc:
  160. logger.critical(str(exc))
  161. logger.debug('Exception information:', exc_info=True)
  162. return ERROR
  163. except CommandError as exc:
  164. logger.critical('ERROR: %s', exc)
  165. logger.debug('Exception information:', exc_info=True)
  166. return ERROR
  167. except BrokenStdoutLoggingError:
  168. # Bypass our logger and write any remaining messages to stderr
  169. # because stdout no longer works.
  170. print('ERROR: Pipe to stdout was broken', file=sys.stderr)
  171. if level_number <= logging.DEBUG:
  172. traceback.print_exc(file=sys.stderr)
  173. return ERROR
  174. except KeyboardInterrupt:
  175. logger.critical('Operation cancelled by user')
  176. logger.debug('Exception information:', exc_info=True)
  177. return ERROR
  178. except BaseException:
  179. logger.critical('Exception:', exc_info=True)
  180. return UNKNOWN_ERROR
  181. finally:
  182. allow_version_check = (
  183. # Does this command have the index_group options?
  184. hasattr(options, "no_index") and
  185. # Is this command allowed to perform this check?
  186. not (options.disable_pip_version_check or options.no_index)
  187. )
  188. # Check if we're using the latest version of pip available
  189. if allow_version_check:
  190. session = self._build_session(
  191. options,
  192. retries=0,
  193. timeout=min(5, options.timeout)
  194. )
  195. with session:
  196. pip_version_check(session, options)
  197. # Shutdown the logging module
  198. logging.shutdown()
  199. return SUCCESS
  200. class RequirementCommand(Command):
  201. @staticmethod
  202. def populate_requirement_set(requirement_set, # type: RequirementSet
  203. args, # type: List[str]
  204. options, # type: Values
  205. finder, # type: PackageFinder
  206. session, # type: PipSession
  207. name, # type: str
  208. wheel_cache # type: Optional[WheelCache]
  209. ):
  210. # type: (...) -> None
  211. """
  212. Marshal cmd line args into a requirement set.
  213. """
  214. # NOTE: As a side-effect, options.require_hashes and
  215. # requirement_set.require_hashes may be updated
  216. for filename in options.constraints:
  217. for req_to_add in parse_requirements(
  218. filename,
  219. constraint=True, finder=finder, options=options,
  220. session=session, wheel_cache=wheel_cache):
  221. req_to_add.is_direct = True
  222. requirement_set.add_requirement(req_to_add)
  223. for req in args:
  224. req_to_add = install_req_from_line(
  225. req, None, isolated=options.isolated_mode,
  226. use_pep517=options.use_pep517,
  227. wheel_cache=wheel_cache
  228. )
  229. req_to_add.is_direct = True
  230. requirement_set.add_requirement(req_to_add)
  231. for req in options.editables:
  232. req_to_add = install_req_from_editable(
  233. req,
  234. isolated=options.isolated_mode,
  235. use_pep517=options.use_pep517,
  236. wheel_cache=wheel_cache
  237. )
  238. req_to_add.is_direct = True
  239. requirement_set.add_requirement(req_to_add)
  240. for filename in options.requirements:
  241. for req_to_add in parse_requirements(
  242. filename,
  243. finder=finder, options=options, session=session,
  244. wheel_cache=wheel_cache,
  245. use_pep517=options.use_pep517):
  246. req_to_add.is_direct = True
  247. requirement_set.add_requirement(req_to_add)
  248. # If --require-hashes was a line in a requirements file, tell
  249. # RequirementSet about it:
  250. requirement_set.require_hashes = options.require_hashes
  251. if not (args or options.editables or options.requirements):
  252. opts = {'name': name}
  253. if options.find_links:
  254. raise CommandError(
  255. 'You must give at least one requirement to %(name)s '
  256. '(maybe you meant "pip %(name)s %(links)s"?)' %
  257. dict(opts, links=' '.join(options.find_links)))
  258. else:
  259. raise CommandError(
  260. 'You must give at least one requirement to %(name)s '
  261. '(see "pip help %(name)s")' % opts)
  262. def _build_package_finder(
  263. self,
  264. options, # type: Values
  265. session, # type: PipSession
  266. platform=None, # type: Optional[str]
  267. python_versions=None, # type: Optional[List[str]]
  268. abi=None, # type: Optional[str]
  269. implementation=None # type: Optional[str]
  270. ):
  271. # type: (...) -> PackageFinder
  272. """
  273. Create a package finder appropriate to this requirement command.
  274. """
  275. index_urls = [options.index_url] + options.extra_index_urls
  276. if options.no_index:
  277. logger.debug(
  278. 'Ignoring indexes: %s',
  279. ','.join(redact_password_from_url(url) for url in index_urls),
  280. )
  281. index_urls = []
  282. return PackageFinder(
  283. find_links=options.find_links,
  284. format_control=options.format_control,
  285. index_urls=index_urls,
  286. trusted_hosts=options.trusted_hosts,
  287. allow_all_prereleases=options.pre,
  288. session=session,
  289. platform=platform,
  290. versions=python_versions,
  291. abi=abi,
  292. implementation=implementation,
  293. prefer_binary=options.prefer_binary,
  294. )