req_command.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. """Contains the Command base classes that depend on PipSession.
  2. The classes in this module are in a separate module so the commands not
  3. needing download / PackageFinder capability don't unnecessarily import the
  4. PackageFinder machinery and all its vendored dependencies, etc.
  5. """
  6. import logging
  7. import os
  8. from functools import partial
  9. from pip._internal.cli import cmdoptions
  10. from pip._internal.cli.base_command import Command
  11. from pip._internal.cli.command_context import CommandContextMixIn
  12. from pip._internal.exceptions import CommandError, PreviousBuildDirError
  13. from pip._internal.index.collector import LinkCollector
  14. from pip._internal.index.package_finder import PackageFinder
  15. from pip._internal.models.selection_prefs import SelectionPreferences
  16. from pip._internal.network.download import Downloader
  17. from pip._internal.network.session import PipSession
  18. from pip._internal.operations.prepare import RequirementPreparer
  19. from pip._internal.req.constructors import (
  20. install_req_from_editable,
  21. install_req_from_line,
  22. install_req_from_parsed_requirement,
  23. install_req_from_req_string,
  24. )
  25. from pip._internal.req.req_file import parse_requirements
  26. from pip._internal.self_outdated_check import pip_self_version_check
  27. from pip._internal.utils.temp_dir import tempdir_kinds
  28. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  29. if MYPY_CHECK_RUNNING:
  30. from optparse import Values
  31. from typing import Any, List, Optional, Tuple
  32. from pip._internal.cache import WheelCache
  33. from pip._internal.models.target_python import TargetPython
  34. from pip._internal.req.req_install import InstallRequirement
  35. from pip._internal.req.req_tracker import RequirementTracker
  36. from pip._internal.resolution.base import BaseResolver
  37. from pip._internal.utils.temp_dir import (
  38. TempDirectory,
  39. TempDirectoryTypeRegistry,
  40. )
  41. logger = logging.getLogger(__name__)
  42. class SessionCommandMixin(CommandContextMixIn):
  43. """
  44. A class mixin for command classes needing _build_session().
  45. """
  46. def __init__(self):
  47. # type: () -> None
  48. super(SessionCommandMixin, self).__init__()
  49. self._session = None # Optional[PipSession]
  50. @classmethod
  51. def _get_index_urls(cls, options):
  52. # type: (Values) -> Optional[List[str]]
  53. """Return a list of index urls from user-provided options."""
  54. index_urls = []
  55. if not getattr(options, "no_index", False):
  56. url = getattr(options, "index_url", None)
  57. if url:
  58. index_urls.append(url)
  59. urls = getattr(options, "extra_index_urls", None)
  60. if urls:
  61. index_urls.extend(urls)
  62. # Return None rather than an empty list
  63. return index_urls or None
  64. def get_default_session(self, options):
  65. # type: (Values) -> PipSession
  66. """Get a default-managed session."""
  67. if self._session is None:
  68. self._session = self.enter_context(self._build_session(options))
  69. # there's no type annotation on requests.Session, so it's
  70. # automatically ContextManager[Any] and self._session becomes Any,
  71. # then https://github.com/python/mypy/issues/7696 kicks in
  72. assert self._session is not None
  73. return self._session
  74. def _build_session(self, options, retries=None, timeout=None):
  75. # type: (Values, Optional[int], Optional[int]) -> PipSession
  76. assert not options.cache_dir or os.path.isabs(options.cache_dir)
  77. session = PipSession(
  78. cache=(
  79. os.path.join(options.cache_dir, "http")
  80. if options.cache_dir else None
  81. ),
  82. retries=retries if retries is not None else options.retries,
  83. trusted_hosts=options.trusted_hosts,
  84. index_urls=self._get_index_urls(options),
  85. )
  86. # Handle custom ca-bundles from the user
  87. if options.cert:
  88. session.verify = options.cert
  89. # Handle SSL client certificate
  90. if options.client_cert:
  91. session.cert = options.client_cert
  92. # Handle timeouts
  93. if options.timeout or timeout:
  94. session.timeout = (
  95. timeout if timeout is not None else options.timeout
  96. )
  97. # Handle configured proxies
  98. if options.proxy:
  99. session.proxies = {
  100. "http": options.proxy,
  101. "https": options.proxy,
  102. }
  103. # Determine if we can prompt the user for authentication or not
  104. session.auth.prompting = not options.no_input
  105. return session
  106. class IndexGroupCommand(Command, SessionCommandMixin):
  107. """
  108. Abstract base class for commands with the index_group options.
  109. This also corresponds to the commands that permit the pip version check.
  110. """
  111. def handle_pip_version_check(self, options):
  112. # type: (Values) -> None
  113. """
  114. Do the pip version check if not disabled.
  115. This overrides the default behavior of not doing the check.
  116. """
  117. # Make sure the index_group options are present.
  118. assert hasattr(options, 'no_index')
  119. if options.disable_pip_version_check or options.no_index:
  120. return
  121. # Otherwise, check if we're using the latest version of pip available.
  122. session = self._build_session(
  123. options,
  124. retries=0,
  125. timeout=min(5, options.timeout)
  126. )
  127. with session:
  128. pip_self_version_check(session, options)
  129. KEEPABLE_TEMPDIR_TYPES = [
  130. tempdir_kinds.BUILD_ENV,
  131. tempdir_kinds.EPHEM_WHEEL_CACHE,
  132. tempdir_kinds.REQ_BUILD,
  133. ]
  134. def with_cleanup(func):
  135. # type: (Any) -> Any
  136. """Decorator for common logic related to managing temporary
  137. directories.
  138. """
  139. def configure_tempdir_registry(registry):
  140. # type: (TempDirectoryTypeRegistry) -> None
  141. for t in KEEPABLE_TEMPDIR_TYPES:
  142. registry.set_delete(t, False)
  143. def wrapper(self, options, args):
  144. # type: (RequirementCommand, Values, List[Any]) -> Optional[int]
  145. assert self.tempdir_registry is not None
  146. if options.no_clean:
  147. configure_tempdir_registry(self.tempdir_registry)
  148. try:
  149. return func(self, options, args)
  150. except PreviousBuildDirError:
  151. # This kind of conflict can occur when the user passes an explicit
  152. # build directory with a pre-existing folder. In that case we do
  153. # not want to accidentally remove it.
  154. configure_tempdir_registry(self.tempdir_registry)
  155. raise
  156. return wrapper
  157. class RequirementCommand(IndexGroupCommand):
  158. def __init__(self, *args, **kw):
  159. # type: (Any, Any) -> None
  160. super(RequirementCommand, self).__init__(*args, **kw)
  161. self.cmd_opts.add_option(cmdoptions.no_clean())
  162. @staticmethod
  163. def make_requirement_preparer(
  164. temp_build_dir, # type: TempDirectory
  165. options, # type: Values
  166. req_tracker, # type: RequirementTracker
  167. session, # type: PipSession
  168. finder, # type: PackageFinder
  169. use_user_site, # type: bool
  170. download_dir=None, # type: str
  171. wheel_download_dir=None, # type: str
  172. ):
  173. # type: (...) -> RequirementPreparer
  174. """
  175. Create a RequirementPreparer instance for the given parameters.
  176. """
  177. downloader = Downloader(session, progress_bar=options.progress_bar)
  178. temp_build_dir_path = temp_build_dir.path
  179. assert temp_build_dir_path is not None
  180. return RequirementPreparer(
  181. build_dir=temp_build_dir_path,
  182. src_dir=options.src_dir,
  183. download_dir=download_dir,
  184. wheel_download_dir=wheel_download_dir,
  185. build_isolation=options.build_isolation,
  186. req_tracker=req_tracker,
  187. downloader=downloader,
  188. finder=finder,
  189. require_hashes=options.require_hashes,
  190. use_user_site=use_user_site,
  191. )
  192. @staticmethod
  193. def make_resolver(
  194. preparer, # type: RequirementPreparer
  195. finder, # type: PackageFinder
  196. options, # type: Values
  197. wheel_cache=None, # type: Optional[WheelCache]
  198. use_user_site=False, # type: bool
  199. ignore_installed=True, # type: bool
  200. ignore_requires_python=False, # type: bool
  201. force_reinstall=False, # type: bool
  202. upgrade_strategy="to-satisfy-only", # type: str
  203. use_pep517=None, # type: Optional[bool]
  204. py_version_info=None # type: Optional[Tuple[int, ...]]
  205. ):
  206. # type: (...) -> BaseResolver
  207. """
  208. Create a Resolver instance for the given parameters.
  209. """
  210. make_install_req = partial(
  211. install_req_from_req_string,
  212. isolated=options.isolated_mode,
  213. use_pep517=use_pep517,
  214. )
  215. # The long import name and duplicated invocation is needed to convince
  216. # Mypy into correctly typechecking. Otherwise it would complain the
  217. # "Resolver" class being redefined.
  218. if '2020-resolver' in options.features_enabled:
  219. import pip._internal.resolution.resolvelib.resolver
  220. return pip._internal.resolution.resolvelib.resolver.Resolver(
  221. preparer=preparer,
  222. finder=finder,
  223. wheel_cache=wheel_cache,
  224. make_install_req=make_install_req,
  225. use_user_site=use_user_site,
  226. ignore_dependencies=options.ignore_dependencies,
  227. ignore_installed=ignore_installed,
  228. ignore_requires_python=ignore_requires_python,
  229. force_reinstall=force_reinstall,
  230. upgrade_strategy=upgrade_strategy,
  231. py_version_info=py_version_info,
  232. lazy_wheel='fast-deps' in options.features_enabled,
  233. )
  234. import pip._internal.resolution.legacy.resolver
  235. return pip._internal.resolution.legacy.resolver.Resolver(
  236. preparer=preparer,
  237. finder=finder,
  238. wheel_cache=wheel_cache,
  239. make_install_req=make_install_req,
  240. use_user_site=use_user_site,
  241. ignore_dependencies=options.ignore_dependencies,
  242. ignore_installed=ignore_installed,
  243. ignore_requires_python=ignore_requires_python,
  244. force_reinstall=force_reinstall,
  245. upgrade_strategy=upgrade_strategy,
  246. py_version_info=py_version_info,
  247. )
  248. def get_requirements(
  249. self,
  250. args, # type: List[str]
  251. options, # type: Values
  252. finder, # type: PackageFinder
  253. session, # type: PipSession
  254. ):
  255. # type: (...) -> List[InstallRequirement]
  256. """
  257. Parse command-line arguments into the corresponding requirements.
  258. """
  259. requirements = [] # type: List[InstallRequirement]
  260. for filename in options.constraints:
  261. for parsed_req in parse_requirements(
  262. filename,
  263. constraint=True, finder=finder, options=options,
  264. session=session):
  265. req_to_add = install_req_from_parsed_requirement(
  266. parsed_req,
  267. isolated=options.isolated_mode,
  268. user_supplied=False,
  269. )
  270. requirements.append(req_to_add)
  271. for req in args:
  272. req_to_add = install_req_from_line(
  273. req, None, isolated=options.isolated_mode,
  274. use_pep517=options.use_pep517,
  275. user_supplied=True,
  276. )
  277. requirements.append(req_to_add)
  278. for req in options.editables:
  279. req_to_add = install_req_from_editable(
  280. req,
  281. user_supplied=True,
  282. isolated=options.isolated_mode,
  283. use_pep517=options.use_pep517,
  284. )
  285. requirements.append(req_to_add)
  286. # NOTE: options.require_hashes may be set if --require-hashes is True
  287. for filename in options.requirements:
  288. for parsed_req in parse_requirements(
  289. filename,
  290. finder=finder, options=options, session=session):
  291. req_to_add = install_req_from_parsed_requirement(
  292. parsed_req,
  293. isolated=options.isolated_mode,
  294. use_pep517=options.use_pep517,
  295. user_supplied=True,
  296. )
  297. requirements.append(req_to_add)
  298. # If any requirement has hash options, enable hash checking.
  299. if any(req.has_hash_options for req in requirements):
  300. options.require_hashes = True
  301. if not (args or options.editables or options.requirements):
  302. opts = {'name': self.name}
  303. if options.find_links:
  304. raise CommandError(
  305. 'You must give at least one requirement to {name} '
  306. '(maybe you meant "pip {name} {links}"?)'.format(
  307. **dict(opts, links=' '.join(options.find_links))))
  308. else:
  309. raise CommandError(
  310. 'You must give at least one requirement to {name} '
  311. '(see "pip help {name}")'.format(**opts))
  312. return requirements
  313. @staticmethod
  314. def trace_basic_info(finder):
  315. # type: (PackageFinder) -> None
  316. """
  317. Trace basic information about the provided objects.
  318. """
  319. # Display where finder is looking for packages
  320. search_scope = finder.search_scope
  321. locations = search_scope.get_formatted_locations()
  322. if locations:
  323. logger.info(locations)
  324. def _build_package_finder(
  325. self,
  326. options, # type: Values
  327. session, # type: PipSession
  328. target_python=None, # type: Optional[TargetPython]
  329. ignore_requires_python=None, # type: Optional[bool]
  330. ):
  331. # type: (...) -> PackageFinder
  332. """
  333. Create a package finder appropriate to this requirement command.
  334. :param ignore_requires_python: Whether to ignore incompatible
  335. "Requires-Python" values in links. Defaults to False.
  336. """
  337. link_collector = LinkCollector.create(session, options=options)
  338. selection_prefs = SelectionPreferences(
  339. allow_yanked=True,
  340. format_control=options.format_control,
  341. allow_all_prereleases=options.pre,
  342. prefer_binary=options.prefer_binary,
  343. ignore_requires_python=ignore_requires_python,
  344. )
  345. return PackageFinder.create(
  346. link_collector=link_collector,
  347. selection_prefs=selection_prefs,
  348. target_python=target_python,
  349. )