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 assert isinstance(self.pep517_backend, ConfiguredBuildBackendHookCaller)
232 with self.build_env:
233 runner = runner_with_spinner_message(
234 "Checking if build backend supports build_editable"
235 )
236 with self.pep517_backend.subprocess_runner(runner):
237 return self.pep517_backend.supports_feature("build_editable")
238
239 @property
240 def specifier(self) -> SpecifierSet:
241 assert self.req is not None
242 return self.req.specifier
243
244 @property
245 def is_direct(self) -> bool:
246 """Whether this requirement was specified as a direct URL."""
247 return self.original_link is not None
248
249 @property
250 def is_pinned(self) -> bool:
251 """Return whether I am pinned to an exact version.
252
253 For example, some-package==1.2 is pinned; some-package>1.2 is not.
254 """
255 assert self.req is not None
256 specifiers = self.req.specifier
257 return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
258
259 def match_markers(self, extras_requested: Iterable[str] | None = None) -> bool:
260 if not extras_requested:
261 # Provide an extra to safely evaluate the markers
262 # without matching any extra
263 extras_requested = ("",)
264 if self.markers is not None:
265 return any(
266 self.markers.evaluate({"extra": extra}) for extra in extras_requested
267 )
268 else:
269 return True
270
271 @property
272 def has_hash_options(self) -> bool:
273 """Return whether any known-good hashes are specified as options.
274
275 These activate --require-hashes mode; hashes specified as part of a
276 URL do not.
277
278 """
279 return bool(self.hash_options)
280
281 def hashes(self, trust_internet: bool = True) -> Hashes:
282 """Return a hash-comparer that considers my option- and URL-based
283 hashes to be known-good.
284
285 Hashes in URLs--ones embedded in the requirements file, not ones
286 downloaded from an index server--are almost peers with ones from
287 flags. They satisfy --require-hashes (whether it was implicitly or
288 explicitly activated) but do not activate it. md5 and sha224 are not
289 allowed in flags, which should nudge people toward good algos. We
290 always OR all hashes together, even ones from URLs.
291
292 :param trust_internet: Whether to trust URL-based (#md5=...) hashes
293 downloaded from the internet, as by populate_link()
294
295 """
296 good_hashes = self.hash_options.copy()
297 if trust_internet:
298 link = self.link
299 elif self.is_direct and self.user_supplied:
300 link = self.original_link
301 else:
302 link = None
303 if link and link.hash:
304 assert link.hash_name is not None
305 good_hashes.setdefault(link.hash_name, []).append(link.hash)
306 return Hashes(good_hashes)
307
308 def from_path(self) -> str | None:
309 """Format a nice indicator to show where this "comes from" """
310 if self.req is None:
311 return None
312 s = str(self.req)
313 if self.comes_from:
314 comes_from: str | None
315 if isinstance(self.comes_from, str):
316 comes_from = self.comes_from
317 else:
318 comes_from = self.comes_from.from_path()
319 if comes_from:
320 s += "->" + comes_from
321 return s
322
323 def ensure_build_location(
324 self, build_dir: str, autodelete: bool, parallel_builds: bool
325 ) -> str:
326 assert build_dir is not None
327 if self._temp_build_dir is not None:
328 assert self._temp_build_dir.path
329 return self._temp_build_dir.path
330 if self.req is None:
331 # Some systems have /tmp as a symlink which confuses custom
332 # builds (such as numpy). Thus, we ensure that the real path
333 # is returned.
334 self._temp_build_dir = TempDirectory(
335 kind=tempdir_kinds.REQ_BUILD, globally_managed=True
336 )
337
338 return self._temp_build_dir.path
339
340 # This is the only remaining place where we manually determine the path
341 # for the temporary directory. It is only needed for editables where
342 # it is the value of the --src option.
343
344 # When parallel builds are enabled, add a UUID to the build directory
345 # name so multiple builds do not interfere with each other.
346 dir_name: str = canonicalize_name(self.req.name)
347 if parallel_builds:
348 dir_name = f"{dir_name}_{uuid.uuid4().hex}"
349
350 # FIXME: Is there a better place to create the build_dir? (hg and bzr
351 # need this)
352 if not os.path.exists(build_dir):
353 logger.debug("Creating directory %s", build_dir)
354 os.makedirs(build_dir)
355 actual_build_dir = os.path.join(build_dir, dir_name)
356 # `None` indicates that we respect the globally-configured deletion
357 # settings, which is what we actually want when auto-deleting.
358 delete_arg = None if autodelete else False
359 return TempDirectory(
360 path=actual_build_dir,
361 delete=delete_arg,
362 kind=tempdir_kinds.REQ_BUILD,
363 globally_managed=True,
364 ).path
365
366 def _set_requirement(self) -> None:
367 """Set requirement after generating metadata."""
368 assert self.req is None
369 assert self.metadata is not None
370 assert self.source_dir is not None
371
372 # Construct a Requirement object from the generated metadata
373 if isinstance(parse_version(self.metadata["Version"]), Version):
374 op = "=="
375 else:
376 op = "==="
377
378 self.req = get_requirement(
379 "".join(
380 [
381 self.metadata["Name"],
382 op,
383 self.metadata["Version"],
384 ]
385 )
386 )
387
388 def warn_on_mismatching_name(self) -> None:
389 assert self.req is not None
390 metadata_name = canonicalize_name(self.metadata["Name"])
391 if canonicalize_name(self.req.name) == metadata_name:
392 # Everything is fine.
393 return
394
395 # If we're here, there's a mismatch. Log a warning about it.
396 logger.warning(
397 "Generating metadata for package %s "
398 "produced metadata for project name %s. Fix your "
399 "#egg=%s fragments.",
400 self.name,
401 metadata_name,
402 self.name,
403 )
404 self.req = get_requirement(metadata_name)
405
406 def check_if_exists(self, use_user_site: bool) -> None:
407 """Find an installed distribution that satisfies or conflicts
408 with this requirement, and set self.satisfied_by or
409 self.should_reinstall appropriately.
410 """
411 if self.req is None:
412 return
413 existing_dist = get_default_environment().get_distribution(self.req.name)
414 if not existing_dist:
415 return
416
417 version_compatible = self.req.specifier.contains(
418 existing_dist.version,
419 prereleases=True,
420 )
421 if not version_compatible:
422 self.satisfied_by = None
423 if use_user_site:
424 if existing_dist.in_usersite:
425 self.should_reinstall = True
426 elif running_under_virtualenv() and existing_dist.in_site_packages:
427 raise InstallationError(
428 f"Will not install to the user site because it will "
429 f"lack sys.path precedence to {existing_dist.raw_name} "
430 f"in {existing_dist.location}"
431 )
432 else:
433 self.should_reinstall = True
434 else:
435 if self.editable:
436 self.should_reinstall = True
437 # when installing editables, nothing pre-existing should ever
438 # satisfy
439 self.satisfied_by = None
440 else:
441 self.satisfied_by = existing_dist
442
443 # Things valid for wheels
444 @property
445 def is_wheel(self) -> bool:
446 if not self.link:
447 return False
448 return self.link.is_wheel
449
450 @property
451 def is_wheel_from_cache(self) -> bool:
452 # When True, it means that this InstallRequirement is a local wheel file in the
453 # cache of locally built wheels.
454 return self.cached_wheel_source_link is not None
455
456 # Things valid for sdists
457 @property
458 def unpacked_source_directory(self) -> str:
459 assert self.source_dir, f"No source dir for {self}"
460 return os.path.join(
461 self.source_dir, self.link and self.link.subdirectory_fragment or ""
462 )
463
464 @property
465 def setup_py_path(self) -> str:
466 assert self.source_dir, f"No source dir for {self}"
467 setup_py = os.path.join(self.unpacked_source_directory, "setup.py")
468
469 return setup_py
470
471 @property
472 def pyproject_toml_path(self) -> str:
473 assert self.source_dir, f"No source dir for {self}"
474 return make_pyproject_path(self.unpacked_source_directory)
475
476 def load_pyproject_toml(self) -> None:
477 """Load the pyproject.toml file.
478
479 After calling this routine, all of the attributes related to PEP 517
480 processing for this requirement have been set.
481 """
482 pyproject_toml_data = load_pyproject_toml(
483 self.pyproject_toml_path, self.setup_py_path, str(self)
484 )
485 assert pyproject_toml_data
486 requires, backend, check, backend_path = pyproject_toml_data
487 self.requirements_to_check = check
488 self.pyproject_requires = requires
489 self._pep517_backend_spec = backend
490 self._pep517_backend_path = backend_path
491
492 def configure_backend(self, python_executable: str) -> None:
493 """Set up the build backend hook caller.
494
495 This is done separately after pyproject.toml loading as the backend
496 need to be called with the build environment's Python executable,
497 which can vary."""
498 self.pep517_backend = ConfiguredBuildBackendHookCaller(
499 self,
500 self.unpacked_source_directory,
501 self._pep517_backend_spec,
502 backend_path=self._pep517_backend_path,
503 python_executable=python_executable,
504 )
505
506 def editable_sanity_check(self) -> None:
507 """Check that an editable requirement if valid for use with PEP 517/518.
508
509 This verifies that an editable has a build backend that supports PEP 660.
510 """
511 if self.editable and not self.supports_pyproject_editable:
512 raise InstallationError(
513 f"Project {self} uses a build backend "
514 f"that is missing the 'build_editable' hook, so "
515 f"it cannot be installed in editable mode. "
516 f"Consider using a build backend that supports PEP 660."
517 )
518
519 def prepare_metadata(self, allow_editables: bool) -> None:
520 """Ensure that project metadata is available.
521
522 Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
523 Under legacy processing, call setup.py egg-info.
524 """
525 assert self.source_dir, f"No source dir for {self}"
526 details = self.name or f"from {self.link}"
527
528 assert self.pep517_backend is not None
529 if self.editable and allow_editables and self.supports_pyproject_editable:
530 self.metadata_directory = generate_editable_metadata(
531 build_env=self.build_env,
532 backend=self.pep517_backend,
533 details=details,
534 )
535 else:
536 self.metadata_directory = generate_metadata(
537 build_env=self.build_env,
538 backend=self.pep517_backend,
539 details=details,
540 )
541
542 # Act on the newly generated metadata, based on the name and version.
543 if not self.name:
544 self._set_requirement()
545 else:
546 self.warn_on_mismatching_name()
547
548 self.assert_source_matches_version()
549
550 @property
551 def metadata(self) -> Any:
552 if not hasattr(self, "_metadata"):
553 self._metadata = self.get_dist().metadata
554
555 return self._metadata
556
557 def set_dist(self, distribution: BaseDistribution) -> None:
558 self._distribution = distribution
559
560 def get_dist(self) -> BaseDistribution:
561 if self._distribution is not None:
562 return self._distribution
563 elif self.metadata_directory:
564 return get_directory_distribution(self.metadata_directory)
565 elif self.local_file_path and self.is_wheel:
566 assert self.req is not None
567 return get_wheel_distribution(
568 FilesystemWheel(self.local_file_path),
569 canonicalize_name(self.req.name),
570 )
571 raise AssertionError(
572 f"InstallRequirement {self} has no metadata directory and no wheel: "
573 f"can't make a distribution."
574 )
575
576 def assert_source_matches_version(self) -> None:
577 assert self.source_dir, f"No source dir for {self}"
578 version = self.metadata["version"]
579 if self.req and self.req.specifier and version not in self.req.specifier:
580 logger.warning(
581 "Requested %s, but installing version %s",
582 self,
583 version,
584 )
585 else:
586 logger.debug(
587 "Source in %s has version %s, which satisfies requirement %s",
588 display_path(self.source_dir),
589 version,
590 self,
591 )
592
593 # For both source distributions and editables
594 def ensure_has_source_dir(
595 self,
596 parent_dir: str,
597 autodelete: bool = False,
598 parallel_builds: bool = False,
599 ) -> None:
600 """Ensure that a source_dir is set.
601
602 This will create a temporary build dir if the name of the requirement
603 isn't known yet.
604
605 :param parent_dir: The ideal pip parent_dir for the source_dir.
606 Generally src_dir for editables and build_dir for sdists.
607 :return: self.source_dir
608 """
609 if self.source_dir is None:
610 self.source_dir = self.ensure_build_location(
611 parent_dir,
612 autodelete=autodelete,
613 parallel_builds=parallel_builds,
614 )
615
616 def needs_unpacked_archive(self, archive_source: Path) -> None:
617 assert self._archive_source is None
618 self._archive_source = archive_source
619
620 def ensure_pristine_source_checkout(self) -> None:
621 """Ensure the source directory has not yet been built in."""
622 assert self.source_dir is not None
623 if self._archive_source is not None:
624 unpack_file(str(self._archive_source), self.source_dir)
625 elif is_installable_dir(self.source_dir):
626 # If a checkout exists, it's unwise to keep going.
627 # version inconsistencies are logged later, but do not fail
628 # the installation.
629 raise PreviousBuildDirError(
630 f"pip can't proceed with requirements '{self}' due to a "
631 f"pre-existing build directory ({self.source_dir}). This is likely "
632 "due to a previous installation that failed . pip is "
633 "being responsible and not assuming it can delete this. "
634 "Please delete it and try again."
635 )
636
637 # For editable installations
638 def update_editable(self) -> None:
639 if not self.link:
640 logger.debug(
641 "Cannot update repository at %s; repository location is unknown",
642 self.source_dir,
643 )
644 return
645 assert self.editable
646 assert self.source_dir
647 if self.link.scheme == "file":
648 # Static paths don't get updated
649 return
650 vcs_backend = vcs.get_backend_for_scheme(self.link.scheme)
651 # Editable requirements are validated in Requirement constructors.
652 # So here, if it's neither a path nor a valid VCS URL, it's a bug.
653 assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
654 hidden_url = hide_url(self.link.url)
655 vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
656
657 # Top-level Actions
658 def uninstall(
659 self, auto_confirm: bool = False, verbose: bool = False
660 ) -> UninstallPathSet | None:
661 """
662 Uninstall the distribution currently satisfying this requirement.
663
664 Prompts before removing or modifying files unless
665 ``auto_confirm`` is True.
666
667 Refuses to delete or modify files outside of ``sys.prefix`` -
668 thus uninstallation within a virtual environment can only
669 modify that virtual environment, even if the virtualenv is
670 linked to global site-packages.
671
672 """
673 assert self.req
674 dist = get_default_environment().get_distribution(self.req.name)
675 if not dist:
676 logger.warning("Skipping %s as it is not installed.", self.name)
677 return None
678 logger.info("Found existing installation: %s", dist)
679
680 uninstalled_pathset = UninstallPathSet.from_dist(dist)
681 uninstalled_pathset.remove(auto_confirm, verbose)
682 return uninstalled_pathset
683
684 def _get_archive_name(self, path: str, parentdir: str, rootdir: str) -> str:
685 def _clean_zip_name(name: str, prefix: str) -> str:
686 assert name.startswith(
687 prefix + os.path.sep
688 ), f"name {name!r} doesn't start with prefix {prefix!r}"
689 name = name[len(prefix) + 1 :]
690 name = name.replace(os.path.sep, "/")
691 return name
692
693 assert self.req is not None
694 path = os.path.join(parentdir, path)
695 name = _clean_zip_name(path, rootdir)
696 return self.req.name + "/" + name
697
698 def archive(self, build_dir: str | None) -> None:
699 """Saves archive to provided build_dir.
700
701 Used for saving downloaded VCS requirements as part of `pip download`.
702 """
703 assert self.source_dir
704 if build_dir is None:
705 return
706
707 create_archive = True
708 archive_name = "{}-{}.zip".format(self.name, self.metadata["version"])
709 archive_path = os.path.join(build_dir, archive_name)
710
711 if os.path.exists(archive_path):
712 response = ask_path_exists(
713 f"The file {display_path(archive_path)} exists. (i)gnore, (w)ipe, "
714 "(b)ackup, (a)bort ",
715 ("i", "w", "b", "a"),
716 )
717 if response == "i":
718 create_archive = False
719 elif response == "w":
720 logger.warning("Deleting %s", display_path(archive_path))
721 os.remove(archive_path)
722 elif response == "b":
723 dest_file = backup_dir(archive_path)
724 logger.warning(
725 "Backing up %s to %s",
726 display_path(archive_path),
727 display_path(dest_file),
728 )
729 shutil.move(archive_path, dest_file)
730 elif response == "a":
731 sys.exit(-1)
732
733 if not create_archive:
734 return
735
736 zip_output = zipfile.ZipFile(
737 archive_path,
738 "w",
739 zipfile.ZIP_DEFLATED,
740 allowZip64=True,
741 )
742 with zip_output:
743 dir = os.path.normcase(os.path.abspath(self.unpacked_source_directory))
744 for dirpath, dirnames, filenames in os.walk(dir):
745 for dirname in dirnames:
746 dir_arcname = self._get_archive_name(
747 dirname,
748 parentdir=dirpath,
749 rootdir=dir,
750 )
751 zipdir = zipfile.ZipInfo(dir_arcname + "/")
752 zipdir.external_attr = 0x1ED << 16 # 0o755
753 zip_output.writestr(zipdir, "")
754 for filename in filenames:
755 file_arcname = self._get_archive_name(
756 filename,
757 parentdir=dirpath,
758 rootdir=dir,
759 )
760 filename = os.path.join(dirpath, filename)
761 zip_output.write(filename, file_arcname)
762
763 logger.info("Saved %s", display_path(archive_path))
764
765 def install(
766 self,
767 root: str | None = None,
768 home: str | None = None,
769 prefix: str | None = None,
770 warn_script_location: bool = True,
771 use_user_site: bool = False,
772 pycompile: bool = True,
773 script_executable: str | None = None,
774 ) -> None:
775 # Lazy import to avoid transitively importing `_vendor.distlib.compat`
776 # which in turn imports `urllib.request` which is slow.
777 # During an actual installation, `urllib.request` will end up imported anyway,
778 # but `req.req_install` (this module) is also imported from commands that
779 # don't actually install anything (e.g. `pip freeze` or `pip show`).
780 from pip._internal.operations.install.wheel import install_wheel
781
782 assert self.req is not None
783 scheme = get_scheme(
784 self.req.name,
785 user=use_user_site,
786 home=home,
787 root=root,
788 isolated=self.isolated,
789 prefix=prefix,
790 )
791
792 assert self.is_wheel
793 assert self.local_file_path
794
795 install_wheel(
796 self.req.name,
797 self.local_file_path,
798 scheme=scheme,
799 req_description=str(self.req),
800 pycompile=pycompile,
801 warn_script_location=warn_script_location,
802 direct_url=self.download_info if self.is_direct else None,
803 requested=self.user_supplied,
804 script_executable=script_executable,
805 )
806 self.install_succeeded = True
807
808
809def check_invalid_constraint_type(req: InstallRequirement) -> str:
810 # Check for unsupported forms
811 problem = ""
812 if not req.name:
813 problem = "Unnamed requirements are not allowed as constraints"
814 elif req.editable:
815 problem = "Editable requirements are not allowed as constraints"
816 elif req.extras:
817 problem = "Constraints cannot have extras"
818
819 if problem:
820 deprecated(
821 reason=(
822 "Constraints are only allowed to take the form of a package "
823 "name and a version specifier. Other forms were originally "
824 "permitted as an accident of the implementation, but were "
825 "undocumented. The new implementation of the resolver no "
826 "longer supports these forms."
827 ),
828 replacement="replacing the constraint with a requirement",
829 # No plan yet for when the new resolver becomes default
830 gone_in=None,
831 issue=8210,
832 )
833
834 return problem
835
836
837def _has_option(options: Values, reqs: list[InstallRequirement], option: str) -> bool:
838 if getattr(options, option, None):
839 return True
840 for req in reqs:
841 if getattr(req, option, None):
842 return True
843 return False