req_command.py 13 KB

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