req_command.py 16 KB

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