req_command.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. """Contains the RequirementCommand base class.
  2. This class is in a separate module so the commands that do not always
  3. need PackageFinder capability don't unnecessarily import the
  4. PackageFinder machinery and all its vendored dependencies, etc.
  5. """
  6. import logging
  7. from functools import partial
  8. from optparse import Values
  9. from typing import Any, List, Optional, Tuple
  10. from pip._internal.cache import WheelCache
  11. from pip._internal.cli import cmdoptions
  12. from pip._internal.cli.index_command import IndexGroupCommand
  13. from pip._internal.cli.index_command import SessionCommandMixin as SessionCommandMixin
  14. from pip._internal.exceptions import CommandError, PreviousBuildDirError
  15. from pip._internal.index.collector import LinkCollector
  16. from pip._internal.index.package_finder import PackageFinder
  17. from pip._internal.models.selection_prefs import SelectionPreferences
  18. from pip._internal.models.target_python import TargetPython
  19. from pip._internal.network.session import PipSession
  20. from pip._internal.operations.build.build_tracker import BuildTracker
  21. from pip._internal.operations.prepare import RequirementPreparer
  22. from pip._internal.req.constructors import (
  23. install_req_from_editable,
  24. install_req_from_line,
  25. install_req_from_parsed_requirement,
  26. install_req_from_req_string,
  27. )
  28. from pip._internal.req.req_file import parse_requirements
  29. from pip._internal.req.req_install import InstallRequirement
  30. from pip._internal.resolution.base import BaseResolver
  31. from pip._internal.utils.temp_dir import (
  32. TempDirectory,
  33. TempDirectoryTypeRegistry,
  34. tempdir_kinds,
  35. )
  36. logger = logging.getLogger(__name__)
  37. KEEPABLE_TEMPDIR_TYPES = [
  38. tempdir_kinds.BUILD_ENV,
  39. tempdir_kinds.EPHEM_WHEEL_CACHE,
  40. tempdir_kinds.REQ_BUILD,
  41. ]
  42. def with_cleanup(func: Any) -> Any:
  43. """Decorator for common logic related to managing temporary
  44. directories.
  45. """
  46. def configure_tempdir_registry(registry: TempDirectoryTypeRegistry) -> None:
  47. for t in KEEPABLE_TEMPDIR_TYPES:
  48. registry.set_delete(t, False)
  49. def wrapper(
  50. self: RequirementCommand, options: Values, args: List[Any]
  51. ) -> Optional[int]:
  52. assert self.tempdir_registry is not None
  53. if options.no_clean:
  54. configure_tempdir_registry(self.tempdir_registry)
  55. try:
  56. return func(self, options, args)
  57. except PreviousBuildDirError:
  58. # This kind of conflict can occur when the user passes an explicit
  59. # build directory with a pre-existing folder. In that case we do
  60. # not want to accidentally remove it.
  61. configure_tempdir_registry(self.tempdir_registry)
  62. raise
  63. return wrapper
  64. class RequirementCommand(IndexGroupCommand):
  65. def __init__(self, *args: Any, **kw: Any) -> None:
  66. super().__init__(*args, **kw)
  67. self.cmd_opts.add_option(cmdoptions.no_clean())
  68. @staticmethod
  69. def determine_resolver_variant(options: Values) -> str:
  70. """Determines which resolver should be used, based on the given options."""
  71. if "legacy-resolver" in options.deprecated_features_enabled:
  72. return "legacy"
  73. return "resolvelib"
  74. @classmethod
  75. def make_requirement_preparer(
  76. cls,
  77. temp_build_dir: TempDirectory,
  78. options: Values,
  79. build_tracker: BuildTracker,
  80. session: PipSession,
  81. finder: PackageFinder,
  82. use_user_site: bool,
  83. download_dir: Optional[str] = None,
  84. verbosity: int = 0,
  85. ) -> RequirementPreparer:
  86. """
  87. Create a RequirementPreparer instance for the given parameters.
  88. """
  89. temp_build_dir_path = temp_build_dir.path
  90. assert temp_build_dir_path is not None
  91. legacy_resolver = False
  92. resolver_variant = cls.determine_resolver_variant(options)
  93. if resolver_variant == "resolvelib":
  94. lazy_wheel = "fast-deps" in options.features_enabled
  95. if lazy_wheel:
  96. logger.warning(
  97. "pip is using lazily downloaded wheels using HTTP "
  98. "range requests to obtain dependency information. "
  99. "This experimental feature is enabled through "
  100. "--use-feature=fast-deps and it is not ready for "
  101. "production."
  102. )
  103. else:
  104. legacy_resolver = True
  105. lazy_wheel = False
  106. if "fast-deps" in options.features_enabled:
  107. logger.warning(
  108. "fast-deps has no effect when used with the legacy resolver."
  109. )
  110. return RequirementPreparer(
  111. build_dir=temp_build_dir_path,
  112. src_dir=options.src_dir,
  113. download_dir=download_dir,
  114. build_isolation=options.build_isolation,
  115. check_build_deps=options.check_build_deps,
  116. build_tracker=build_tracker,
  117. session=session,
  118. progress_bar=options.progress_bar,
  119. finder=finder,
  120. require_hashes=options.require_hashes,
  121. use_user_site=use_user_site,
  122. lazy_wheel=lazy_wheel,
  123. verbosity=verbosity,
  124. legacy_resolver=legacy_resolver,
  125. )
  126. @classmethod
  127. def make_resolver(
  128. cls,
  129. preparer: RequirementPreparer,
  130. finder: PackageFinder,
  131. options: Values,
  132. wheel_cache: Optional[WheelCache] = None,
  133. use_user_site: bool = False,
  134. ignore_installed: bool = True,
  135. ignore_requires_python: bool = False,
  136. force_reinstall: bool = False,
  137. upgrade_strategy: str = "to-satisfy-only",
  138. use_pep517: Optional[bool] = None,
  139. py_version_info: Optional[Tuple[int, ...]] = None,
  140. ) -> BaseResolver:
  141. """
  142. Create a Resolver instance for the given parameters.
  143. """
  144. make_install_req = partial(
  145. install_req_from_req_string,
  146. isolated=options.isolated_mode,
  147. use_pep517=use_pep517,
  148. )
  149. resolver_variant = cls.determine_resolver_variant(options)
  150. # The long import name and duplicated invocation is needed to convince
  151. # Mypy into correctly typechecking. Otherwise it would complain the
  152. # "Resolver" class being redefined.
  153. if resolver_variant == "resolvelib":
  154. import pip._internal.resolution.resolvelib.resolver
  155. return pip._internal.resolution.resolvelib.resolver.Resolver(
  156. preparer=preparer,
  157. finder=finder,
  158. wheel_cache=wheel_cache,
  159. make_install_req=make_install_req,
  160. use_user_site=use_user_site,
  161. ignore_dependencies=options.ignore_dependencies,
  162. ignore_installed=ignore_installed,
  163. ignore_requires_python=ignore_requires_python,
  164. force_reinstall=force_reinstall,
  165. upgrade_strategy=upgrade_strategy,
  166. py_version_info=py_version_info,
  167. )
  168. import pip._internal.resolution.legacy.resolver
  169. return pip._internal.resolution.legacy.resolver.Resolver(
  170. preparer=preparer,
  171. finder=finder,
  172. wheel_cache=wheel_cache,
  173. make_install_req=make_install_req,
  174. use_user_site=use_user_site,
  175. ignore_dependencies=options.ignore_dependencies,
  176. ignore_installed=ignore_installed,
  177. ignore_requires_python=ignore_requires_python,
  178. force_reinstall=force_reinstall,
  179. upgrade_strategy=upgrade_strategy,
  180. py_version_info=py_version_info,
  181. )
  182. def get_requirements(
  183. self,
  184. args: List[str],
  185. options: Values,
  186. finder: PackageFinder,
  187. session: PipSession,
  188. ) -> List[InstallRequirement]:
  189. """
  190. Parse command-line arguments into the corresponding requirements.
  191. """
  192. requirements: List[InstallRequirement] = []
  193. for filename in options.constraints:
  194. for parsed_req in parse_requirements(
  195. filename,
  196. constraint=True,
  197. finder=finder,
  198. options=options,
  199. session=session,
  200. ):
  201. req_to_add = install_req_from_parsed_requirement(
  202. parsed_req,
  203. isolated=options.isolated_mode,
  204. user_supplied=False,
  205. )
  206. requirements.append(req_to_add)
  207. for req in args:
  208. req_to_add = install_req_from_line(
  209. req,
  210. comes_from=None,
  211. isolated=options.isolated_mode,
  212. use_pep517=options.use_pep517,
  213. user_supplied=True,
  214. config_settings=getattr(options, "config_settings", None),
  215. )
  216. requirements.append(req_to_add)
  217. for req in options.editables:
  218. req_to_add = install_req_from_editable(
  219. req,
  220. user_supplied=True,
  221. isolated=options.isolated_mode,
  222. use_pep517=options.use_pep517,
  223. config_settings=getattr(options, "config_settings", None),
  224. )
  225. requirements.append(req_to_add)
  226. # NOTE: options.require_hashes may be set if --require-hashes is True
  227. for filename in options.requirements:
  228. for parsed_req in parse_requirements(
  229. filename, finder=finder, options=options, session=session
  230. ):
  231. req_to_add = install_req_from_parsed_requirement(
  232. parsed_req,
  233. isolated=options.isolated_mode,
  234. use_pep517=options.use_pep517,
  235. user_supplied=True,
  236. config_settings=(
  237. parsed_req.options.get("config_settings")
  238. if parsed_req.options
  239. else None
  240. ),
  241. )
  242. requirements.append(req_to_add)
  243. # If any requirement has hash options, enable hash checking.
  244. if any(req.has_hash_options for req in requirements):
  245. options.require_hashes = True
  246. if not (args or options.editables or options.requirements):
  247. opts = {"name": self.name}
  248. if options.find_links:
  249. raise CommandError(
  250. "You must give at least one requirement to {name} "
  251. '(maybe you meant "pip {name} {links}"?)'.format(
  252. **dict(opts, links=" ".join(options.find_links))
  253. )
  254. )
  255. else:
  256. raise CommandError(
  257. "You must give at least one requirement to {name} "
  258. '(see "pip help {name}")'.format(**opts)
  259. )
  260. return requirements
  261. @staticmethod
  262. def trace_basic_info(finder: PackageFinder) -> None:
  263. """
  264. Trace basic information about the provided objects.
  265. """
  266. # Display where finder is looking for packages
  267. search_scope = finder.search_scope
  268. locations = search_scope.get_formatted_locations()
  269. if locations:
  270. logger.info(locations)
  271. def _build_package_finder(
  272. self,
  273. options: Values,
  274. session: PipSession,
  275. target_python: Optional[TargetPython] = None,
  276. ignore_requires_python: Optional[bool] = None,
  277. ) -> PackageFinder:
  278. """
  279. Create a package finder appropriate to this requirement command.
  280. :param ignore_requires_python: Whether to ignore incompatible
  281. "Requires-Python" values in links. Defaults to False.
  282. """
  283. link_collector = LinkCollector.create(session, options=options)
  284. selection_prefs = SelectionPreferences(
  285. allow_yanked=True,
  286. format_control=options.format_control,
  287. allow_all_prereleases=options.pre,
  288. prefer_binary=options.prefer_binary,
  289. ignore_requires_python=ignore_requires_python,
  290. )
  291. return PackageFinder.create(
  292. link_collector=link_collector,
  293. selection_prefs=selection_prefs,
  294. target_python=target_python,
  295. )