1from __future__ import annotations
2
3import functools
4import logging
5import os
6import shutil
7import sys
8import uuid
9import zipfile
10from collections.abc import Collection, Iterable
11from optparse import Values
12from pathlib import Path
13from typing import Any
14
15from pip._vendor.packaging.markers import Marker
16from pip._vendor.packaging.requirements import Requirement
17from pip._vendor.packaging.specifiers import SpecifierSet
18from pip._vendor.packaging.utils import canonicalize_name
19from pip._vendor.packaging.version import Version
20from pip._vendor.packaging.version import parse as parse_version
21from pip._vendor.pyproject_hooks import BuildBackendHookCaller
22
23from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
24from pip._internal.exceptions import InstallationError, PreviousBuildDirError
25from pip._internal.locations import get_scheme
26from pip._internal.metadata import (
27 BaseDistribution,
28 get_default_environment,
29 get_directory_distribution,
30 get_wheel_distribution,
31)
32from pip._internal.metadata.base import FilesystemWheel
33from pip._internal.models.direct_url import DirectUrl
34from pip._internal.models.link import Link
35from pip._internal.operations.build.metadata import generate_metadata
36from pip._internal.operations.build.metadata_editable import generate_editable_metadata
37from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
38from pip._internal.req.req_uninstall import UninstallPathSet
39from pip._internal.utils.deprecation import deprecated
40from pip._internal.utils.hashes import Hashes
41from pip._internal.utils.misc import (
42 ConfiguredBuildBackendHookCaller,
43 ask_path_exists,
44 backup_dir,
45 display_path,
46 hide_url,
47 is_installable_dir,
48 redact_auth_from_requirement,
49 redact_auth_from_url,
50)
51from pip._internal.utils.packaging import get_requirement
52from pip._internal.utils.subprocess import runner_with_spinner_message
53from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
54from pip._internal.utils.unpacking import unpack_file
55from pip._internal.utils.virtualenv import running_under_virtualenv
56from pip._internal.vcs import vcs
57
58logger = logging.getLogger(__name__)
59
60
61class InstallRequirement:
62 """
63 Represents something that may be installed later on, may have information
64 about where to fetch the relevant requirement and also contains logic for
65 installing the said requirement.
66 """
67
68 def __init__(
69 self,
70 req: Requirement | None,
71 comes_from: str | InstallRequirement | None,
72 editable: bool = False,
73 link: Link | None = None,
74 markers: Marker | None = None,
75 isolated: bool = False,
76 *,
77 hash_options: dict[str, list[str]] | None = None,
78 config_settings: dict[str, str | list[str]] | None = None,
79 constraint: bool = False,
80 extras: Collection[str] = (),
81 user_supplied: bool = False,
82 ) -> None:
83 assert req is None or isinstance(req, Requirement), req
84 self.req = req
85 self.comes_from = comes_from
86 self.constraint = constraint
87 self.editable = editable
88
89 # source_dir is the local directory where the linked requirement is
90 # located, or unpacked. In case unpacking is needed, creating and
91 # populating source_dir is done by the RequirementPreparer. Note this
92 # is not necessarily the directory where pyproject.toml or setup.py is
93 # located - that one is obtained via unpacked_source_directory.
94 self.source_dir: str | None = None
95 if self.editable:
96 assert link
97 if link.is_file:
98 self.source_dir = os.path.normpath(os.path.abspath(link.file_path))
99
100 # original_link is the direct URL that was provided by the user for the
101 # requirement, either directly or via a constraints file.
102 if link is None and req and req.url:
103 # PEP 508 URL requirement
104 link = Link(req.url)
105 self.link = self.original_link = link
106
107 # When this InstallRequirement is a wheel obtained from the cache of locally
108 # built wheels, this is the source link corresponding to the cache entry, which
109 # was used to download and build the cached wheel.
110 self.cached_wheel_source_link: Link | None = None
111
112 # Information about the location of the artifact that was downloaded . This
113 # property is guaranteed to be set in resolver results.
114 self.download_info: DirectUrl | None = None
115
116 # Path to any downloaded or already-existing package.
117 self.local_file_path: str | None = None
118 if self.link and self.link.is_file:
119 self.local_file_path = self.link.file_path
120
121 if extras:
122 self.extras = extras
123 elif req:
124 self.extras = req.extras
125 else:
126 self.extras = set()
127 if markers is None and req:
128 markers = req.marker
129 self.markers = markers
130
131 # This holds the Distribution object if this requirement is already installed.
132 self.satisfied_by: BaseDistribution | None = None
133 # Whether the installation process should try to uninstall an existing
134 # distribution before installing this requirement.
135 self.should_reinstall = False
136 # Temporary build location
137 self._temp_build_dir: TempDirectory | None = None
138 # Set to True after successful installation
139 self.install_succeeded: bool | None = None
140 # Supplied options
141 self.hash_options = hash_options if hash_options else {}
142 self.config_settings = config_settings
143 # Set to True after successful preparation of this requirement
144 self.prepared = False
145 # User supplied requirement are explicitly requested for installation
146 # by the user via CLI arguments or requirements files, as opposed to,
147 # e.g. dependencies, extras or constraints.
148 self.user_supplied = user_supplied
149
150 self.isolated = isolated
151 self.build_env: BuildEnvironment = NoOpBuildEnvironment()
152
153 # For PEP 517, the directory where we request the project metadata
154 # gets stored. We need this to pass to build_wheel, so the backend
155 # can ensure that the wheel matches the metadata (see the PEP for
156 # details).
157 self.metadata_directory: str | None = None
158
159 # The cached metadata distribution that this requirement represents.
160 # See get_dist / set_dist.
161 self._distribution: BaseDistribution | None = None
162
163 # The static build requirements (from pyproject.toml)
164 self.pyproject_requires: list[str] | None = None
165
166 # Build requirements that we will check are available
167 self.requirements_to_check: list[str] = []
168
169 # The PEP 517 backend we should use to build the project
170 self._pep517_backend_spec: str
171 self._pep517_backend_path: str | None
172 self.pep517_backend: BuildBackendHookCaller | None = None
173
174 # This requirement needs more preparation before it can be built
175 self.needs_more_preparation = False
176
177 # This requirement needs to be unpacked before it can be installed.
178 self._archive_source: Path | None = None
179
180 def __str__(self) -> str:
181 if self.req:
182 s = redact_auth_from_requirement(self.req)
183 if self.link:
184 s += f" from {redact_auth_from_url(self.link.url)}"
185 elif self.link:
186 s = redact_auth_from_url(self.link.url)
187 else:
188 s = "<InstallRequirement>"
189 if self.satisfied_by is not None:
190 if self.satisfied_by.location is not None:
191 location = display_path(self.satisfied_by.location)
192 else:
193 location = "<memory>"
194 s += f" in {location}"
195 if self.comes_from:
196 if isinstance(self.comes_from, str):
197 comes_from: str | None = self.comes_from
198 else:
199 comes_from = self.comes_from.from_path()
200 if comes_from:
201 s += f" (from {comes_from})"
202 return s
203
204 def __repr__(self) -> str:
205 return (
206 f"<{self.__class__.__name__} object: "
207 f"{str(self)} editable={self.editable!r}>"
208 )
209
210 def format_debug(self) -> str:
211 """An un-tested helper for getting state, for debugging."""
212 attributes = vars(self)
213 names = sorted(attributes)
214
215 state = (f"{attr}={attributes[attr]!r}" for attr in sorted(names))
216 return "<{name} object: {{{state}}}>".format(
217 name=self.__class__.__name__,
218 state=", ".join(state),
219 )
220
221 # Things that are valid for all kinds of requirements?
222 @property
223 def name(self) -> str | None:
224 if self.req is None:
225 return None
226 return self.req.name
227
228 @functools.cached_property
229 def supports_pyproject_editable(self) -> bool:
230 assert self.pep517_backend
231 with self.build_env:
232 runner = runner_with_spinner_message(
233 "Checking if build backend supports build_editable"
234 )
235 with self.pep517_backend.subprocess_runner(runner):
236 return "build_editable" in self.pep517_backend._supported_features()
237
238 @property
239 def specifier(self) -> SpecifierSet:
240 assert self.req is not None
241 return self.req.specifier
242
243 @property
244 def is_direct(self) -> bool:
245 """Whether this requirement was specified as a direct URL."""
246 return self.original_link is not None
247
248 @property
249 def is_pinned(self) -> bool:
250 """Return whether I am pinned to an exact version.
251
252 For example, some-package==1.2 is pinned; some-package>1.2 is not.
253 """
254 assert self.req is not None
255 specifiers = self.req.specifier
256 return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
257
258 def match_markers(self, extras_requested: Iterable[str] | None = None) -> bool:
259 if not extras_requested:
260 # Provide an extra to safely evaluate the markers
261 # without matching any extra
262 extras_requested = ("",)
263 if self.markers is not None:
264 return any(
265 self.markers.evaluate({"extra": extra}) for extra in extras_requested
266 )
267 else:
268 return True
269
270 @property
271 def has_hash_options(self) -> bool:
272 """Return whether any known-good hashes are specified as options.
273
274 These activate --require-hashes mode; hashes specified as part of a
275 URL do not.
276
277 """
278 return bool(self.hash_options)
279
280 def hashes(self, trust_internet: bool = True) -> Hashes:
281 """Return a hash-comparer that considers my option- and URL-based
282 hashes to be known-good.
283
284 Hashes in URLs--ones embedded in the requirements file, not ones
285 downloaded from an index server--are almost peers with ones from
286 flags. They satisfy --require-hashes (whether it was implicitly or
287 explicitly activated) but do not activate it. md5 and sha224 are not
288 allowed in flags, which should nudge people toward good algos. We
289 always OR all hashes together, even ones from URLs.
290
291 :param trust_internet: Whether to trust URL-based (#md5=...) hashes
292 downloaded from the internet, as by populate_link()
293
294 """
295 good_hashes = self.hash_options.copy()
296 if trust_internet:
297 link = self.link
298 elif self.is_direct and self.user_supplied:
299 link = self.original_link
300 else:
301 link = None
302 if link and link.hash:
303 assert link.hash_name is not None
304 good_hashes.setdefault(link.hash_name, []).append(link.hash)
305 return Hashes(good_hashes)
306
307 def from_path(self) -> str | None:
308 """Format a nice indicator to show where this "comes from" """
309 if self.req is None:
310 return None
311 s = str(self.req)
312 if self.comes_from:
313 comes_from: str | None
314 if isinstance(self.comes_from, str):
315 comes_from = self.comes_from
316 else:
317 comes_from = self.comes_from.from_path()
318 if comes_from:
319 s += "->" + comes_from
320 return s
321
322 def ensure_build_location(
323 self, build_dir: str, autodelete: bool, parallel_builds: bool
324 ) -> str:
325 assert build_dir is not None
326 if self._temp_build_dir is not None:
327 assert self._temp_build_dir.path
328 return self._temp_build_dir.path
329 if self.req is None:
330 # Some systems have /tmp as a symlink which confuses custom
331 # builds (such as numpy). Thus, we ensure that the real path
332 # is returned.
333 self._temp_build_dir = TempDirectory(
334 kind=tempdir_kinds.REQ_BUILD, globally_managed=True
335 )
336
337 return self._temp_build_dir.path
338
339 # This is the only remaining place where we manually determine the path
340 # for the temporary directory. It is only needed for editables where
341 # it is the value of the --src option.
342
343 # When parallel builds are enabled, add a UUID to the build directory
344 # name so multiple builds do not interfere with each other.
345 dir_name: str = canonicalize_name(self.req.name)
346 if parallel_builds:
347 dir_name = f"{dir_name}_{uuid.uuid4().hex}"
348
349 # FIXME: Is there a better place to create the build_dir? (hg and bzr
350 # need this)
351 if not os.path.exists(build_dir):
352 logger.debug("Creating directory %s", build_dir)
353 os.makedirs(build_dir)
354 actual_build_dir = os.path.join(build_dir, dir_name)
355 # `None` indicates that we respect the globally-configured deletion
356 # settings, which is what we actually want when auto-deleting.
357 delete_arg = None if autodelete else False
358 return TempDirectory(
359 path=actual_build_dir,
360 delete=delete_arg,
361 kind=tempdir_kinds.REQ_BUILD,
362 globally_managed=True,
363 ).path
364
365 def _set_requirement(self) -> None:
366 """Set requirement after generating metadata."""
367 assert self.req is None
368 assert self.metadata is not None
369 assert self.source_dir is not None
370
371 # Construct a Requirement object from the generated metadata
372 if isinstance(parse_version(self.metadata["Version"]), Version):
373 op = "=="
374 else:
375 op = "==="
376
377 self.req = get_requirement(
378 "".join(
379 [
380 self.metadata["Name"],
381 op,
382 self.metadata["Version"],
383 ]
384 )
385 )
386
387 def warn_on_mismatching_name(self) -> None:
388 assert self.req is not None
389 metadata_name = canonicalize_name(self.metadata["Name"])
390 if canonicalize_name(self.req.name) == metadata_name:
391 # Everything is fine.
392 return
393
394 # If we're here, there's a mismatch. Log a warning about it.
395 logger.warning(
396 "Generating metadata for package %s "
397 "produced metadata for project name %s. Fix your "
398 "#egg=%s fragments.",
399 self.name,
400 metadata_name,
401 self.name,
402 )
403 self.req = get_requirement(metadata_name)
404
405 def check_if_exists(self, use_user_site: bool) -> None:
406 """Find an installed distribution that satisfies or conflicts
407 with this requirement, and set self.satisfied_by or
408 self.should_reinstall appropriately.
409 """
410 if self.req is None:
411 return
412 existing_dist = get_default_environment().get_distribution(self.req.name)
413 if not existing_dist:
414 return
415
416 version_compatible = self.req.specifier.contains(
417 existing_dist.version,
418 prereleases=True,
419 )
420 if not version_compatible:
421 self.satisfied_by = None
422 if use_user_site:
423 if existing_dist.in_usersite:
424 self.should_reinstall = True
425 elif running_under_virtualenv() and existing_dist.in_site_packages:
426 raise InstallationError(
427 f"Will not install to the user site because it will "
428 f"lack sys.path precedence to {existing_dist.raw_name} "
429 f"in {existing_dist.location}"
430 )
431 else:
432 self.should_reinstall = True
433 else:
434 if self.editable:
435 self.should_reinstall = True
436 # when installing editables, nothing pre-existing should ever
437 # satisfy
438 self.satisfied_by = None
439 else:
440 self.satisfied_by = existing_dist
441
442 # Things valid for wheels
443 @property
444 def is_wheel(self) -> bool:
445 if not self.link:
446 return False
447 return self.link.is_wheel
448
449 @property
450 def is_wheel_from_cache(self) -> bool:
451 # When True, it means that this InstallRequirement is a local wheel file in the
452 # cache of locally built wheels.
453 return self.cached_wheel_source_link is not None
454
455 # Things valid for sdists
456 @property
457 def unpacked_source_directory(self) -> str:
458 assert self.source_dir, f"No source dir for {self}"
459 return os.path.join(
460 self.source_dir, self.link and self.link.subdirectory_fragment or ""
461 )
462
463 @property
464 def setup_py_path(self) -> str:
465 assert self.source_dir, f"No source dir for {self}"
466 setup_py = os.path.join(self.unpacked_source_directory, "setup.py")
467
468 return setup_py
469
470 @property
471 def pyproject_toml_path(self) -> str:
472 assert self.source_dir, f"No source dir for {self}"
473 return make_pyproject_path(self.unpacked_source_directory)
474
475 def load_pyproject_toml(self) -> None:
476 """Load the pyproject.toml file.
477
478 After calling this routine, all of the attributes related to PEP 517
479 processing for this requirement have been set.
480 """
481 pyproject_toml_data = load_pyproject_toml(
482 self.pyproject_toml_path, self.setup_py_path, str(self)
483 )
484 assert pyproject_toml_data
485 requires, backend, check, backend_path = pyproject_toml_data
486 self.requirements_to_check = check
487 self.pyproject_requires = requires
488 self._pep517_backend_spec = backend
489 self._pep517_backend_path = backend_path
490
491 def configure_backend(self, python_executable: str) -> None:
492 """Set up the build backend hook caller.
493
494 This is done separately after pyproject.toml loading as the backend
495 need to be called with the build environment's Python executable,
496 which can vary."""
497 self.pep517_backend = ConfiguredBuildBackendHookCaller(
498 self,
499 self.unpacked_source_directory,
500 self._pep517_backend_spec,
501 backend_path=self._pep517_backend_path,
502 python_executable=python_executable,
503 )
504
505 def editable_sanity_check(self) -> None:
506 """Check that an editable requirement if valid for use with PEP 517/518.
507
508 This verifies that an editable has a build backend that supports PEP 660.
509 """
510 if self.editable and not self.supports_pyproject_editable:
511 raise InstallationError(
512 f"Project {self} uses a build backend "
513 f"that is missing the 'build_editable' hook, so "
514 f"it cannot be installed in editable mode. "
515 f"Consider using a build backend that supports PEP 660."
516 )
517
518 def prepare_metadata(self, allow_editables: bool) -> None:
519 """Ensure that project metadata is available.
520
521 Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
522 Under legacy processing, call setup.py egg-info.
523 """
524 assert self.source_dir, f"No source dir for {self}"
525 details = self.name or f"from {self.link}"
526
527 assert self.pep517_backend is not None
528 if self.editable and allow_editables and self.supports_pyproject_editable:
529 self.metadata_directory = generate_editable_metadata(
530 build_env=self.build_env,
531 backend=self.pep517_backend,
532 details=details,
533 )
534 else:
535 self.metadata_directory = generate_metadata(
536 build_env=self.build_env,
537 backend=self.pep517_backend,
538 details=details,
539 )
540
541 # Act on the newly generated metadata, based on the name and version.
542 if not self.name:
543 self._set_requirement()
544 else:
545 self.warn_on_mismatching_name()
546
547 self.assert_source_matches_version()
548
549 @property
550 def metadata(self) -> Any:
551 if not hasattr(self, "_metadata"):
552 self._metadata = self.get_dist().metadata
553
554 return self._metadata
555
556 def set_dist(self, distribution: BaseDistribution) -> None:
557 self._distribution = distribution
558
559 def get_dist(self) -> BaseDistribution:
560 if self._distribution is not None:
561 return self._distribution
562 elif self.metadata_directory:
563 return get_directory_distribution(self.metadata_directory)
564 elif self.local_file_path and self.is_wheel:
565 assert self.req is not None
566 return get_wheel_distribution(
567 FilesystemWheel(self.local_file_path),
568 canonicalize_name(self.req.name),
569 )
570 raise AssertionError(
571 f"InstallRequirement {self} has no metadata directory and no wheel: "
572 f"can't make a distribution."
573 )
574
575 def assert_source_matches_version(self) -> None:
576 assert self.source_dir, f"No source dir for {self}"
577 version = self.metadata["version"]
578 if self.req and self.req.specifier and version not in self.req.specifier:
579 logger.warning(
580 "Requested %s, but installing version %s",
581 self,
582 version,
583 )
584 else:
585 logger.debug(
586 "Source in %s has version %s, which satisfies requirement %s",
587 display_path(self.source_dir),
588 version,
589 self,
590 )
591
592 # For both source distributions and editables
593 def ensure_has_source_dir(
594 self,
595 parent_dir: str,
596 autodelete: bool = False,
597 parallel_builds: bool = False,
598 ) -> None:
599 """Ensure that a source_dir is set.
600
601 This will create a temporary build dir if the name of the requirement
602 isn't known yet.
603
604 :param parent_dir: The ideal pip parent_dir for the source_dir.
605 Generally src_dir for editables and build_dir for sdists.
606 :return: self.source_dir
607 """
608 if self.source_dir is None:
609 self.source_dir = self.ensure_build_location(
610 parent_dir,
611 autodelete=autodelete,
612 parallel_builds=parallel_builds,
613 )
614
615 def needs_unpacked_archive(self, archive_source: Path) -> None:
616 assert self._archive_source is None
617 self._archive_source = archive_source
618
619 def ensure_pristine_source_checkout(self) -> None:
620 """Ensure the source directory has not yet been built in."""
621 assert self.source_dir is not None
622 if self._archive_source is not None:
623 unpack_file(str(self._archive_source), self.source_dir)
624 elif is_installable_dir(self.source_dir):
625 # If a checkout exists, it's unwise to keep going.
626 # version inconsistencies are logged later, but do not fail
627 # the installation.
628 raise PreviousBuildDirError(
629 f"pip can't proceed with requirements '{self}' due to a "
630 f"pre-existing build directory ({self.source_dir}). This is likely "
631 "due to a previous installation that failed . pip is "
632 "being responsible and not assuming it can delete this. "
633 "Please delete it and try again."
634 )
635
636 # For editable installations
637 def update_editable(self) -> None:
638 if not self.link:
639 logger.debug(
640 "Cannot update repository at %s; repository location is unknown",
641 self.source_dir,
642 )
643 return
644 assert self.editable
645 assert self.source_dir
646 if self.link.scheme == "file":
647 # Static paths don't get updated
648 return
649 vcs_backend = vcs.get_backend_for_scheme(self.link.scheme)
650 # Editable requirements are validated in Requirement constructors.
651 # So here, if it's neither a path nor a valid VCS URL, it's a bug.
652 assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
653 hidden_url = hide_url(self.link.url)
654 vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
655
656 # Top-level Actions
657 def uninstall(
658 self, auto_confirm: bool = False, verbose: bool = False
659 ) -> UninstallPathSet | None:
660 """
661 Uninstall the distribution currently satisfying this requirement.
662
663 Prompts before removing or modifying files unless
664 ``auto_confirm`` is True.
665
666 Refuses to delete or modify files outside of ``sys.prefix`` -
667 thus uninstallation within a virtual environment can only
668 modify that virtual environment, even if the virtualenv is
669 linked to global site-packages.
670
671 """
672 assert self.req
673 dist = get_default_environment().get_distribution(self.req.name)
674 if not dist:
675 logger.warning("Skipping %s as it is not installed.", self.name)
676 return None
677 logger.info("Found existing installation: %s", dist)
678
679 uninstalled_pathset = UninstallPathSet.from_dist(dist)
680 uninstalled_pathset.remove(auto_confirm, verbose)
681 return uninstalled_pathset
682
683 def _get_archive_name(self, path: str, parentdir: str, rootdir: str) -> str:
684 def _clean_zip_name(name: str, prefix: str) -> str:
685 assert name.startswith(
686 prefix + os.path.sep
687 ), f"name {name!r} doesn't start with prefix {prefix!r}"
688 name = name[len(prefix) + 1 :]
689 name = name.replace(os.path.sep, "/")
690 return name
691
692 assert self.req is not None
693 path = os.path.join(parentdir, path)
694 name = _clean_zip_name(path, rootdir)
695 return self.req.name + "/" + name
696
697 def archive(self, build_dir: str | None) -> None:
698 """Saves archive to provided build_dir.
699
700 Used for saving downloaded VCS requirements as part of `pip download`.
701 """
702 assert self.source_dir
703 if build_dir is None:
704 return
705
706 create_archive = True
707 archive_name = "{}-{}.zip".format(self.name, self.metadata["version"])
708 archive_path = os.path.join(build_dir, archive_name)
709
710 if os.path.exists(archive_path):
711 response = ask_path_exists(
712 f"The file {display_path(archive_path)} exists. (i)gnore, (w)ipe, "
713 "(b)ackup, (a)bort ",
714 ("i", "w", "b", "a"),
715 )
716 if response == "i":
717 create_archive = False
718 elif response == "w":
719 logger.warning("Deleting %s", display_path(archive_path))
720 os.remove(archive_path)
721 elif response == "b":
722 dest_file = backup_dir(archive_path)
723 logger.warning(
724 "Backing up %s to %s",
725 display_path(archive_path),
726 display_path(dest_file),
727 )
728 shutil.move(archive_path, dest_file)
729 elif response == "a":
730 sys.exit(-1)
731
732 if not create_archive:
733 return
734
735 zip_output = zipfile.ZipFile(
736 archive_path,
737 "w",
738 zipfile.ZIP_DEFLATED,
739 allowZip64=True,
740 )
741 with zip_output:
742 dir = os.path.normcase(os.path.abspath(self.unpacked_source_directory))
743 for dirpath, dirnames, filenames in os.walk(dir):
744 for dirname in dirnames:
745 dir_arcname = self._get_archive_name(
746 dirname,
747 parentdir=dirpath,
748 rootdir=dir,
749 )
750 zipdir = zipfile.ZipInfo(dir_arcname + "/")
751 zipdir.external_attr = 0x1ED << 16 # 0o755
752 zip_output.writestr(zipdir, "")
753 for filename in filenames:
754 file_arcname = self._get_archive_name(
755 filename,
756 parentdir=dirpath,
757 rootdir=dir,
758 )
759 filename = os.path.join(dirpath, filename)
760 zip_output.write(filename, file_arcname)
761
762 logger.info("Saved %s", display_path(archive_path))
763
764 def install(
765 self,
766 root: str | None = None,
767 home: str | None = None,
768 prefix: str | None = None,
769 warn_script_location: bool = True,
770 use_user_site: bool = False,
771 pycompile: bool = True,
772 script_executable: str | None = None,
773 ) -> None:
774 # Lazy import to avoid transitively importing `_vendor.distlib.compat`
775 # which in turn imports `urllib.request` which is slow.
776 # During an actual installation, `urllib.request` will end up imported anyway,
777 # but `req.req_install` (this module) is also imported from commands that
778 # don't actually install anything (e.g. `pip freeze` or `pip show`).
779 from pip._internal.operations.install.wheel import install_wheel
780
781 assert self.req is not None
782 scheme = get_scheme(
783 self.req.name,
784 user=use_user_site,
785 home=home,
786 root=root,
787 isolated=self.isolated,
788 prefix=prefix,
789 )
790
791 assert self.is_wheel
792 assert self.local_file_path
793
794 install_wheel(
795 self.req.name,
796 self.local_file_path,
797 scheme=scheme,
798 req_description=str(self.req),
799 pycompile=pycompile,
800 warn_script_location=warn_script_location,
801 direct_url=self.download_info if self.is_direct else None,
802 requested=self.user_supplied,
803 script_executable=script_executable,
804 )
805 self.install_succeeded = True
806
807
808def check_invalid_constraint_type(req: InstallRequirement) -> str:
809 # Check for unsupported forms
810 problem = ""
811 if not req.name:
812 problem = "Unnamed requirements are not allowed as constraints"
813 elif req.editable:
814 problem = "Editable requirements are not allowed as constraints"
815 elif req.extras:
816 problem = "Constraints cannot have extras"
817
818 if problem:
819 deprecated(
820 reason=(
821 "Constraints are only allowed to take the form of a package "
822 "name and a version specifier. Other forms were originally "
823 "permitted as an accident of the implementation, but were "
824 "undocumented. The new implementation of the resolver no "
825 "longer supports these forms."
826 ),
827 replacement="replacing the constraint with a requirement",
828 # No plan yet for when the new resolver becomes default
829 gone_in=None,
830 issue=8210,
831 )
832
833 return problem
834
835
836def _has_option(options: Values, reqs: list[InstallRequirement], option: str) -> bool:
837 if getattr(options, option, None):
838 return True
839 for req in reqs:
840 if getattr(req, option, None):
841 return True
842 return False