mirror of
				https://forge.chapril.org/tykayn/orgmode-to-gemini-blog
				synced 2025-10-09 17:02:45 +02:00 
			
		
		
		
	move on index build and linking previous and next articles
This commit is contained in:
		
							parent
							
								
									7d221d970a
								
							
						
					
					
						commit
						16b93f380e
					
				
					 1711 changed files with 231792 additions and 838 deletions
				
			
		|  | @ -0,0 +1,138 @@ | |||
| import contextlib | ||||
| import hashlib | ||||
| import logging | ||||
| import os | ||||
| from types import TracebackType | ||||
| from typing import Dict, Generator, Optional, Type, Union | ||||
| 
 | ||||
| from pip._internal.req.req_install import InstallRequirement | ||||
| from pip._internal.utils.temp_dir import TempDirectory | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| @contextlib.contextmanager | ||||
| def update_env_context_manager(**changes: str) -> Generator[None, None, None]: | ||||
|     target = os.environ | ||||
| 
 | ||||
|     # Save values from the target and change them. | ||||
|     non_existent_marker = object() | ||||
|     saved_values: Dict[str, Union[object, str]] = {} | ||||
|     for name, new_value in changes.items(): | ||||
|         try: | ||||
|             saved_values[name] = target[name] | ||||
|         except KeyError: | ||||
|             saved_values[name] = non_existent_marker | ||||
|         target[name] = new_value | ||||
| 
 | ||||
|     try: | ||||
|         yield | ||||
|     finally: | ||||
|         # Restore original values in the target. | ||||
|         for name, original_value in saved_values.items(): | ||||
|             if original_value is non_existent_marker: | ||||
|                 del target[name] | ||||
|             else: | ||||
|                 assert isinstance(original_value, str)  # for mypy | ||||
|                 target[name] = original_value | ||||
| 
 | ||||
| 
 | ||||
| @contextlib.contextmanager | ||||
| def get_build_tracker() -> Generator["BuildTracker", None, None]: | ||||
|     root = os.environ.get("PIP_BUILD_TRACKER") | ||||
|     with contextlib.ExitStack() as ctx: | ||||
|         if root is None: | ||||
|             root = ctx.enter_context(TempDirectory(kind="build-tracker")).path | ||||
|             ctx.enter_context(update_env_context_manager(PIP_BUILD_TRACKER=root)) | ||||
|             logger.debug("Initialized build tracking at %s", root) | ||||
| 
 | ||||
|         with BuildTracker(root) as tracker: | ||||
|             yield tracker | ||||
| 
 | ||||
| 
 | ||||
| class TrackerId(str): | ||||
|     """Uniquely identifying string provided to the build tracker.""" | ||||
| 
 | ||||
| 
 | ||||
| class BuildTracker: | ||||
|     """Ensure that an sdist cannot request itself as a setup requirement. | ||||
| 
 | ||||
|     When an sdist is prepared, it identifies its setup requirements in the | ||||
|     context of ``BuildTracker.track()``. If a requirement shows up recursively, this | ||||
|     raises an exception. | ||||
| 
 | ||||
|     This stops fork bombs embedded in malicious packages.""" | ||||
| 
 | ||||
|     def __init__(self, root: str) -> None: | ||||
|         self._root = root | ||||
|         self._entries: Dict[TrackerId, InstallRequirement] = {} | ||||
|         logger.debug("Created build tracker: %s", self._root) | ||||
| 
 | ||||
|     def __enter__(self) -> "BuildTracker": | ||||
|         logger.debug("Entered build tracker: %s", self._root) | ||||
|         return self | ||||
| 
 | ||||
|     def __exit__( | ||||
|         self, | ||||
|         exc_type: Optional[Type[BaseException]], | ||||
|         exc_val: Optional[BaseException], | ||||
|         exc_tb: Optional[TracebackType], | ||||
|     ) -> None: | ||||
|         self.cleanup() | ||||
| 
 | ||||
|     def _entry_path(self, key: TrackerId) -> str: | ||||
|         hashed = hashlib.sha224(key.encode()).hexdigest() | ||||
|         return os.path.join(self._root, hashed) | ||||
| 
 | ||||
|     def add(self, req: InstallRequirement, key: TrackerId) -> None: | ||||
|         """Add an InstallRequirement to build tracking.""" | ||||
| 
 | ||||
|         # Get the file to write information about this requirement. | ||||
|         entry_path = self._entry_path(key) | ||||
| 
 | ||||
|         # Try reading from the file. If it exists and can be read from, a build | ||||
|         # is already in progress, so a LookupError is raised. | ||||
|         try: | ||||
|             with open(entry_path) as fp: | ||||
|                 contents = fp.read() | ||||
|         except FileNotFoundError: | ||||
|             pass | ||||
|         else: | ||||
|             message = f"{req.link} is already being built: {contents}" | ||||
|             raise LookupError(message) | ||||
| 
 | ||||
|         # If we're here, req should really not be building already. | ||||
|         assert key not in self._entries | ||||
| 
 | ||||
|         # Start tracking this requirement. | ||||
|         with open(entry_path, "w", encoding="utf-8") as fp: | ||||
|             fp.write(str(req)) | ||||
|         self._entries[key] = req | ||||
| 
 | ||||
|         logger.debug("Added %s to build tracker %r", req, self._root) | ||||
| 
 | ||||
|     def remove(self, req: InstallRequirement, key: TrackerId) -> None: | ||||
|         """Remove an InstallRequirement from build tracking.""" | ||||
| 
 | ||||
|         # Delete the created file and the corresponding entry. | ||||
|         os.unlink(self._entry_path(key)) | ||||
|         del self._entries[key] | ||||
| 
 | ||||
|         logger.debug("Removed %s from build tracker %r", req, self._root) | ||||
| 
 | ||||
|     def cleanup(self) -> None: | ||||
|         for key, req in list(self._entries.items()): | ||||
|             self.remove(req, key) | ||||
| 
 | ||||
|         logger.debug("Removed build tracker: %r", self._root) | ||||
| 
 | ||||
|     @contextlib.contextmanager | ||||
|     def track(self, req: InstallRequirement, key: str) -> Generator[None, None, None]: | ||||
|         """Ensure that `key` cannot install itself as a setup requirement. | ||||
| 
 | ||||
|         :raises LookupError: If `key` was already provided in a parent invocation of | ||||
|                              the context introduced by this method.""" | ||||
|         tracker_id = TrackerId(key) | ||||
|         self.add(req, tracker_id) | ||||
|         yield | ||||
|         self.remove(req, tracker_id) | ||||
|  | @ -0,0 +1,39 @@ | |||
| """Metadata generation logic for source distributions. | ||||
| """ | ||||
| 
 | ||||
| import os | ||||
| 
 | ||||
| from pip._vendor.pyproject_hooks import BuildBackendHookCaller | ||||
| 
 | ||||
| from pip._internal.build_env import BuildEnvironment | ||||
| from pip._internal.exceptions import ( | ||||
|     InstallationSubprocessError, | ||||
|     MetadataGenerationFailed, | ||||
| ) | ||||
| from pip._internal.utils.subprocess import runner_with_spinner_message | ||||
| from pip._internal.utils.temp_dir import TempDirectory | ||||
| 
 | ||||
| 
 | ||||
| def generate_metadata( | ||||
|     build_env: BuildEnvironment, backend: BuildBackendHookCaller, details: str | ||||
| ) -> str: | ||||
|     """Generate metadata using mechanisms described in PEP 517. | ||||
| 
 | ||||
|     Returns the generated metadata directory. | ||||
|     """ | ||||
|     metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True) | ||||
| 
 | ||||
|     metadata_dir = metadata_tmpdir.path | ||||
| 
 | ||||
|     with build_env: | ||||
|         # Note that BuildBackendHookCaller implements a fallback for | ||||
|         # prepare_metadata_for_build_wheel, so we don't have to | ||||
|         # consider the possibility that this hook doesn't exist. | ||||
|         runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)") | ||||
|         with backend.subprocess_runner(runner): | ||||
|             try: | ||||
|                 distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir) | ||||
|             except InstallationSubprocessError as error: | ||||
|                 raise MetadataGenerationFailed(package_details=details) from error | ||||
| 
 | ||||
|     return os.path.join(metadata_dir, distinfo_dir) | ||||
|  | @ -0,0 +1,42 @@ | |||
| """Metadata generation logic for source distributions. | ||||
| """ | ||||
| 
 | ||||
| import os | ||||
| 
 | ||||
| from pip._vendor.pyproject_hooks import BuildBackendHookCaller | ||||
| 
 | ||||
| from pip._internal.build_env import BuildEnvironment | ||||
| from pip._internal.exceptions import ( | ||||
|     InstallationSubprocessError, | ||||
|     MetadataGenerationFailed, | ||||
| ) | ||||
| from pip._internal.utils.subprocess import runner_with_spinner_message | ||||
| from pip._internal.utils.temp_dir import TempDirectory | ||||
| 
 | ||||
| 
 | ||||
| def generate_editable_metadata( | ||||
|     build_env: BuildEnvironment, backend: BuildBackendHookCaller, details: str | ||||
| ) -> str: | ||||
|     """Generate metadata using mechanisms described in PEP 660. | ||||
| 
 | ||||
|     Returns the generated metadata directory. | ||||
|     """ | ||||
|     metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True) | ||||
| 
 | ||||
|     metadata_dir = metadata_tmpdir.path | ||||
| 
 | ||||
|     with build_env: | ||||
|         # Note that BuildBackendHookCaller implements a fallback for | ||||
|         # prepare_metadata_for_build_wheel/editable, so we don't have to | ||||
|         # consider the possibility that this hook doesn't exist. | ||||
|         runner = runner_with_spinner_message( | ||||
|             "Preparing editable metadata (pyproject.toml)" | ||||
|         ) | ||||
|         with backend.subprocess_runner(runner): | ||||
|             try: | ||||
|                 distinfo_dir = backend.prepare_metadata_for_build_editable(metadata_dir) | ||||
|             except InstallationSubprocessError as error: | ||||
|                 raise MetadataGenerationFailed(package_details=details) from error | ||||
| 
 | ||||
|     assert distinfo_dir is not None | ||||
|     return os.path.join(metadata_dir, distinfo_dir) | ||||
|  | @ -0,0 +1,74 @@ | |||
| """Metadata generation logic for legacy source distributions. | ||||
| """ | ||||
| 
 | ||||
| import logging | ||||
| import os | ||||
| 
 | ||||
| from pip._internal.build_env import BuildEnvironment | ||||
| from pip._internal.cli.spinners import open_spinner | ||||
| from pip._internal.exceptions import ( | ||||
|     InstallationError, | ||||
|     InstallationSubprocessError, | ||||
|     MetadataGenerationFailed, | ||||
| ) | ||||
| from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args | ||||
| from pip._internal.utils.subprocess import call_subprocess | ||||
| from pip._internal.utils.temp_dir import TempDirectory | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def _find_egg_info(directory: str) -> str: | ||||
|     """Find an .egg-info subdirectory in `directory`.""" | ||||
|     filenames = [f for f in os.listdir(directory) if f.endswith(".egg-info")] | ||||
| 
 | ||||
|     if not filenames: | ||||
|         raise InstallationError(f"No .egg-info directory found in {directory}") | ||||
| 
 | ||||
|     if len(filenames) > 1: | ||||
|         raise InstallationError( | ||||
|             f"More than one .egg-info directory found in {directory}" | ||||
|         ) | ||||
| 
 | ||||
|     return os.path.join(directory, filenames[0]) | ||||
| 
 | ||||
| 
 | ||||
| def generate_metadata( | ||||
|     build_env: BuildEnvironment, | ||||
|     setup_py_path: str, | ||||
|     source_dir: str, | ||||
|     isolated: bool, | ||||
|     details: str, | ||||
| ) -> str: | ||||
|     """Generate metadata using setup.py-based defacto mechanisms. | ||||
| 
 | ||||
|     Returns the generated metadata directory. | ||||
|     """ | ||||
|     logger.debug( | ||||
|         "Running setup.py (path:%s) egg_info for package %s", | ||||
|         setup_py_path, | ||||
|         details, | ||||
|     ) | ||||
| 
 | ||||
|     egg_info_dir = TempDirectory(kind="pip-egg-info", globally_managed=True).path | ||||
| 
 | ||||
|     args = make_setuptools_egg_info_args( | ||||
|         setup_py_path, | ||||
|         egg_info_dir=egg_info_dir, | ||||
|         no_user_config=isolated, | ||||
|     ) | ||||
| 
 | ||||
|     with build_env: | ||||
|         with open_spinner("Preparing metadata (setup.py)") as spinner: | ||||
|             try: | ||||
|                 call_subprocess( | ||||
|                     args, | ||||
|                     cwd=source_dir, | ||||
|                     command_desc="python setup.py egg_info", | ||||
|                     spinner=spinner, | ||||
|                 ) | ||||
|             except InstallationSubprocessError as error: | ||||
|                 raise MetadataGenerationFailed(package_details=details) from error | ||||
| 
 | ||||
|     # Return the .egg-info directory. | ||||
|     return _find_egg_info(egg_info_dir) | ||||
|  | @ -0,0 +1,37 @@ | |||
| import logging | ||||
| import os | ||||
| from typing import Optional | ||||
| 
 | ||||
| from pip._vendor.pyproject_hooks import BuildBackendHookCaller | ||||
| 
 | ||||
| from pip._internal.utils.subprocess import runner_with_spinner_message | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def build_wheel_pep517( | ||||
|     name: str, | ||||
|     backend: BuildBackendHookCaller, | ||||
|     metadata_directory: str, | ||||
|     tempd: str, | ||||
| ) -> Optional[str]: | ||||
|     """Build one InstallRequirement using the PEP 517 build process. | ||||
| 
 | ||||
|     Returns path to wheel if successfully built. Otherwise, returns None. | ||||
|     """ | ||||
|     assert metadata_directory is not None | ||||
|     try: | ||||
|         logger.debug("Destination directory: %s", tempd) | ||||
| 
 | ||||
|         runner = runner_with_spinner_message( | ||||
|             f"Building wheel for {name} (pyproject.toml)" | ||||
|         ) | ||||
|         with backend.subprocess_runner(runner): | ||||
|             wheel_name = backend.build_wheel( | ||||
|                 tempd, | ||||
|                 metadata_directory=metadata_directory, | ||||
|             ) | ||||
|     except Exception: | ||||
|         logger.error("Failed building wheel for %s", name) | ||||
|         return None | ||||
|     return os.path.join(tempd, wheel_name) | ||||
|  | @ -0,0 +1,46 @@ | |||
| import logging | ||||
| import os | ||||
| from typing import Optional | ||||
| 
 | ||||
| from pip._vendor.pyproject_hooks import BuildBackendHookCaller, HookMissing | ||||
| 
 | ||||
| from pip._internal.utils.subprocess import runner_with_spinner_message | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def build_wheel_editable( | ||||
|     name: str, | ||||
|     backend: BuildBackendHookCaller, | ||||
|     metadata_directory: str, | ||||
|     tempd: str, | ||||
| ) -> Optional[str]: | ||||
|     """Build one InstallRequirement using the PEP 660 build process. | ||||
| 
 | ||||
|     Returns path to wheel if successfully built. Otherwise, returns None. | ||||
|     """ | ||||
|     assert metadata_directory is not None | ||||
|     try: | ||||
|         logger.debug("Destination directory: %s", tempd) | ||||
| 
 | ||||
|         runner = runner_with_spinner_message( | ||||
|             f"Building editable for {name} (pyproject.toml)" | ||||
|         ) | ||||
|         with backend.subprocess_runner(runner): | ||||
|             try: | ||||
|                 wheel_name = backend.build_editable( | ||||
|                     tempd, | ||||
|                     metadata_directory=metadata_directory, | ||||
|                 ) | ||||
|             except HookMissing as e: | ||||
|                 logger.error( | ||||
|                     "Cannot build editable %s because the build " | ||||
|                     "backend does not have the %s hook", | ||||
|                     name, | ||||
|                     e, | ||||
|                 ) | ||||
|                 return None | ||||
|     except Exception: | ||||
|         logger.error("Failed building editable for %s", name) | ||||
|         return None | ||||
|     return os.path.join(tempd, wheel_name) | ||||
|  | @ -0,0 +1,102 @@ | |||
| import logging | ||||
| import os.path | ||||
| from typing import List, Optional | ||||
| 
 | ||||
| from pip._internal.cli.spinners import open_spinner | ||||
| from pip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args | ||||
| from pip._internal.utils.subprocess import call_subprocess, format_command_args | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def format_command_result( | ||||
|     command_args: List[str], | ||||
|     command_output: str, | ||||
| ) -> str: | ||||
|     """Format command information for logging.""" | ||||
|     command_desc = format_command_args(command_args) | ||||
|     text = f"Command arguments: {command_desc}\n" | ||||
| 
 | ||||
|     if not command_output: | ||||
|         text += "Command output: None" | ||||
|     elif logger.getEffectiveLevel() > logging.DEBUG: | ||||
|         text += "Command output: [use --verbose to show]" | ||||
|     else: | ||||
|         if not command_output.endswith("\n"): | ||||
|             command_output += "\n" | ||||
|         text += f"Command output:\n{command_output}" | ||||
| 
 | ||||
|     return text | ||||
| 
 | ||||
| 
 | ||||
| def get_legacy_build_wheel_path( | ||||
|     names: List[str], | ||||
|     temp_dir: str, | ||||
|     name: str, | ||||
|     command_args: List[str], | ||||
|     command_output: str, | ||||
| ) -> Optional[str]: | ||||
|     """Return the path to the wheel in the temporary build directory.""" | ||||
|     # Sort for determinism. | ||||
|     names = sorted(names) | ||||
|     if not names: | ||||
|         msg = f"Legacy build of wheel for {name!r} created no files.\n" | ||||
|         msg += format_command_result(command_args, command_output) | ||||
|         logger.warning(msg) | ||||
|         return None | ||||
| 
 | ||||
|     if len(names) > 1: | ||||
|         msg = ( | ||||
|             f"Legacy build of wheel for {name!r} created more than one file.\n" | ||||
|             f"Filenames (choosing first): {names}\n" | ||||
|         ) | ||||
|         msg += format_command_result(command_args, command_output) | ||||
|         logger.warning(msg) | ||||
| 
 | ||||
|     return os.path.join(temp_dir, names[0]) | ||||
| 
 | ||||
| 
 | ||||
| def build_wheel_legacy( | ||||
|     name: str, | ||||
|     setup_py_path: str, | ||||
|     source_dir: str, | ||||
|     global_options: List[str], | ||||
|     build_options: List[str], | ||||
|     tempd: str, | ||||
| ) -> Optional[str]: | ||||
|     """Build one unpacked package using the "legacy" build process. | ||||
| 
 | ||||
|     Returns path to wheel if successfully built. Otherwise, returns None. | ||||
|     """ | ||||
|     wheel_args = make_setuptools_bdist_wheel_args( | ||||
|         setup_py_path, | ||||
|         global_options=global_options, | ||||
|         build_options=build_options, | ||||
|         destination_dir=tempd, | ||||
|     ) | ||||
| 
 | ||||
|     spin_message = f"Building wheel for {name} (setup.py)" | ||||
|     with open_spinner(spin_message) as spinner: | ||||
|         logger.debug("Destination directory: %s", tempd) | ||||
| 
 | ||||
|         try: | ||||
|             output = call_subprocess( | ||||
|                 wheel_args, | ||||
|                 command_desc="python setup.py bdist_wheel", | ||||
|                 cwd=source_dir, | ||||
|                 spinner=spinner, | ||||
|             ) | ||||
|         except Exception: | ||||
|             spinner.finish("error") | ||||
|             logger.error("Failed building wheel for %s", name) | ||||
|             return None | ||||
| 
 | ||||
|         names = os.listdir(tempd) | ||||
|         wheel_path = get_legacy_build_wheel_path( | ||||
|             names=names, | ||||
|             temp_dir=tempd, | ||||
|             name=name, | ||||
|             command_args=wheel_args, | ||||
|             command_output=output, | ||||
|         ) | ||||
|         return wheel_path | ||||
|  | @ -0,0 +1,181 @@ | |||
| """Validation of dependencies of packages | ||||
| """ | ||||
| 
 | ||||
| import logging | ||||
| from contextlib import suppress | ||||
| from email.parser import Parser | ||||
| from functools import reduce | ||||
| from typing import ( | ||||
|     Callable, | ||||
|     Dict, | ||||
|     FrozenSet, | ||||
|     Generator, | ||||
|     Iterable, | ||||
|     List, | ||||
|     NamedTuple, | ||||
|     Optional, | ||||
|     Set, | ||||
|     Tuple, | ||||
| ) | ||||
| 
 | ||||
| from pip._vendor.packaging.requirements import Requirement | ||||
| from pip._vendor.packaging.tags import Tag, parse_tag | ||||
| from pip._vendor.packaging.utils import NormalizedName, canonicalize_name | ||||
| from pip._vendor.packaging.version import Version | ||||
| 
 | ||||
| from pip._internal.distributions import make_distribution_for_install_requirement | ||||
| from pip._internal.metadata import get_default_environment | ||||
| from pip._internal.metadata.base import BaseDistribution | ||||
| from pip._internal.req.req_install import InstallRequirement | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class PackageDetails(NamedTuple): | ||||
|     version: Version | ||||
|     dependencies: List[Requirement] | ||||
| 
 | ||||
| 
 | ||||
| # Shorthands | ||||
| PackageSet = Dict[NormalizedName, PackageDetails] | ||||
| Missing = Tuple[NormalizedName, Requirement] | ||||
| Conflicting = Tuple[NormalizedName, Version, Requirement] | ||||
| 
 | ||||
| MissingDict = Dict[NormalizedName, List[Missing]] | ||||
| ConflictingDict = Dict[NormalizedName, List[Conflicting]] | ||||
| CheckResult = Tuple[MissingDict, ConflictingDict] | ||||
| ConflictDetails = Tuple[PackageSet, CheckResult] | ||||
| 
 | ||||
| 
 | ||||
| def create_package_set_from_installed() -> Tuple[PackageSet, bool]: | ||||
|     """Converts a list of distributions into a PackageSet.""" | ||||
|     package_set = {} | ||||
|     problems = False | ||||
|     env = get_default_environment() | ||||
|     for dist in env.iter_installed_distributions(local_only=False, skip=()): | ||||
|         name = dist.canonical_name | ||||
|         try: | ||||
|             dependencies = list(dist.iter_dependencies()) | ||||
|             package_set[name] = PackageDetails(dist.version, dependencies) | ||||
|         except (OSError, ValueError) as e: | ||||
|             # Don't crash on unreadable or broken metadata. | ||||
|             logger.warning("Error parsing dependencies of %s: %s", name, e) | ||||
|             problems = True | ||||
|     return package_set, problems | ||||
| 
 | ||||
| 
 | ||||
| def check_package_set( | ||||
|     package_set: PackageSet, should_ignore: Optional[Callable[[str], bool]] = None | ||||
| ) -> CheckResult: | ||||
|     """Check if a package set is consistent | ||||
| 
 | ||||
|     If should_ignore is passed, it should be a callable that takes a | ||||
|     package name and returns a boolean. | ||||
|     """ | ||||
| 
 | ||||
|     missing = {} | ||||
|     conflicting = {} | ||||
| 
 | ||||
|     for package_name, package_detail in package_set.items(): | ||||
|         # Info about dependencies of package_name | ||||
|         missing_deps: Set[Missing] = set() | ||||
|         conflicting_deps: Set[Conflicting] = set() | ||||
| 
 | ||||
|         if should_ignore and should_ignore(package_name): | ||||
|             continue | ||||
| 
 | ||||
|         for req in package_detail.dependencies: | ||||
|             name = canonicalize_name(req.name) | ||||
| 
 | ||||
|             # Check if it's missing | ||||
|             if name not in package_set: | ||||
|                 missed = True | ||||
|                 if req.marker is not None: | ||||
|                     missed = req.marker.evaluate({"extra": ""}) | ||||
|                 if missed: | ||||
|                     missing_deps.add((name, req)) | ||||
|                 continue | ||||
| 
 | ||||
|             # Check if there's a conflict | ||||
|             version = package_set[name].version | ||||
|             if not req.specifier.contains(version, prereleases=True): | ||||
|                 conflicting_deps.add((name, version, req)) | ||||
| 
 | ||||
|         if missing_deps: | ||||
|             missing[package_name] = sorted(missing_deps, key=str) | ||||
|         if conflicting_deps: | ||||
|             conflicting[package_name] = sorted(conflicting_deps, key=str) | ||||
| 
 | ||||
|     return missing, conflicting | ||||
| 
 | ||||
| 
 | ||||
| def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDetails: | ||||
|     """For checking if the dependency graph would be consistent after \ | ||||
|     installing given requirements | ||||
|     """ | ||||
|     # Start from the current state | ||||
|     package_set, _ = create_package_set_from_installed() | ||||
|     # Install packages | ||||
|     would_be_installed = _simulate_installation_of(to_install, package_set) | ||||
| 
 | ||||
|     # Only warn about directly-dependent packages; create a whitelist of them | ||||
|     whitelist = _create_whitelist(would_be_installed, package_set) | ||||
| 
 | ||||
|     return ( | ||||
|         package_set, | ||||
|         check_package_set( | ||||
|             package_set, should_ignore=lambda name: name not in whitelist | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def check_unsupported( | ||||
|     packages: Iterable[BaseDistribution], | ||||
|     supported_tags: Iterable[Tag], | ||||
| ) -> Generator[BaseDistribution, None, None]: | ||||
|     for p in packages: | ||||
|         with suppress(FileNotFoundError): | ||||
|             wheel_file = p.read_text("WHEEL") | ||||
|             wheel_tags: FrozenSet[Tag] = reduce( | ||||
|                 frozenset.union, | ||||
|                 map(parse_tag, Parser().parsestr(wheel_file).get_all("Tag", [])), | ||||
|                 frozenset(), | ||||
|             ) | ||||
|             if wheel_tags.isdisjoint(supported_tags): | ||||
|                 yield p | ||||
| 
 | ||||
| 
 | ||||
| def _simulate_installation_of( | ||||
|     to_install: List[InstallRequirement], package_set: PackageSet | ||||
| ) -> Set[NormalizedName]: | ||||
|     """Computes the version of packages after installing to_install.""" | ||||
|     # Keep track of packages that were installed | ||||
|     installed = set() | ||||
| 
 | ||||
|     # Modify it as installing requirement_set would (assuming no errors) | ||||
|     for inst_req in to_install: | ||||
|         abstract_dist = make_distribution_for_install_requirement(inst_req) | ||||
|         dist = abstract_dist.get_metadata_distribution() | ||||
|         name = dist.canonical_name | ||||
|         package_set[name] = PackageDetails(dist.version, list(dist.iter_dependencies())) | ||||
| 
 | ||||
|         installed.add(name) | ||||
| 
 | ||||
|     return installed | ||||
| 
 | ||||
| 
 | ||||
| def _create_whitelist( | ||||
|     would_be_installed: Set[NormalizedName], package_set: PackageSet | ||||
| ) -> Set[NormalizedName]: | ||||
|     packages_affected = set(would_be_installed) | ||||
| 
 | ||||
|     for package_name in package_set: | ||||
|         if package_name in packages_affected: | ||||
|             continue | ||||
| 
 | ||||
|         for req in package_set[package_name].dependencies: | ||||
|             if canonicalize_name(req.name) in packages_affected: | ||||
|                 packages_affected.add(package_name) | ||||
|                 break | ||||
| 
 | ||||
|     return packages_affected | ||||
|  | @ -0,0 +1,256 @@ | |||
| import collections | ||||
| import logging | ||||
| import os | ||||
| from dataclasses import dataclass, field | ||||
| from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set | ||||
| 
 | ||||
| from pip._vendor.packaging.utils import NormalizedName, canonicalize_name | ||||
| from pip._vendor.packaging.version import InvalidVersion | ||||
| 
 | ||||
| from pip._internal.exceptions import BadCommand, InstallationError | ||||
| from pip._internal.metadata import BaseDistribution, get_environment | ||||
| from pip._internal.req.constructors import ( | ||||
|     install_req_from_editable, | ||||
|     install_req_from_line, | ||||
| ) | ||||
| from pip._internal.req.req_file import COMMENT_RE | ||||
| from pip._internal.utils.direct_url_helpers import direct_url_as_pep440_direct_reference | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class _EditableInfo(NamedTuple): | ||||
|     requirement: str | ||||
|     comments: List[str] | ||||
| 
 | ||||
| 
 | ||||
| def freeze( | ||||
|     requirement: Optional[List[str]] = None, | ||||
|     local_only: bool = False, | ||||
|     user_only: bool = False, | ||||
|     paths: Optional[List[str]] = None, | ||||
|     isolated: bool = False, | ||||
|     exclude_editable: bool = False, | ||||
|     skip: Container[str] = (), | ||||
| ) -> Generator[str, None, None]: | ||||
|     installations: Dict[str, FrozenRequirement] = {} | ||||
| 
 | ||||
|     dists = get_environment(paths).iter_installed_distributions( | ||||
|         local_only=local_only, | ||||
|         skip=(), | ||||
|         user_only=user_only, | ||||
|     ) | ||||
|     for dist in dists: | ||||
|         req = FrozenRequirement.from_dist(dist) | ||||
|         if exclude_editable and req.editable: | ||||
|             continue | ||||
|         installations[req.canonical_name] = req | ||||
| 
 | ||||
|     if requirement: | ||||
|         # the options that don't get turned into an InstallRequirement | ||||
|         # should only be emitted once, even if the same option is in multiple | ||||
|         # requirements files, so we need to keep track of what has been emitted | ||||
|         # so that we don't emit it again if it's seen again | ||||
|         emitted_options: Set[str] = set() | ||||
|         # keep track of which files a requirement is in so that we can | ||||
|         # give an accurate warning if a requirement appears multiple times. | ||||
|         req_files: Dict[str, List[str]] = collections.defaultdict(list) | ||||
|         for req_file_path in requirement: | ||||
|             with open(req_file_path) as req_file: | ||||
|                 for line in req_file: | ||||
|                     if ( | ||||
|                         not line.strip() | ||||
|                         or line.strip().startswith("#") | ||||
|                         or line.startswith( | ||||
|                             ( | ||||
|                                 "-r", | ||||
|                                 "--requirement", | ||||
|                                 "-f", | ||||
|                                 "--find-links", | ||||
|                                 "-i", | ||||
|                                 "--index-url", | ||||
|                                 "--pre", | ||||
|                                 "--trusted-host", | ||||
|                                 "--process-dependency-links", | ||||
|                                 "--extra-index-url", | ||||
|                                 "--use-feature", | ||||
|                             ) | ||||
|                         ) | ||||
|                     ): | ||||
|                         line = line.rstrip() | ||||
|                         if line not in emitted_options: | ||||
|                             emitted_options.add(line) | ||||
|                             yield line | ||||
|                         continue | ||||
| 
 | ||||
|                     if line.startswith("-e") or line.startswith("--editable"): | ||||
|                         if line.startswith("-e"): | ||||
|                             line = line[2:].strip() | ||||
|                         else: | ||||
|                             line = line[len("--editable") :].strip().lstrip("=") | ||||
|                         line_req = install_req_from_editable( | ||||
|                             line, | ||||
|                             isolated=isolated, | ||||
|                         ) | ||||
|                     else: | ||||
|                         line_req = install_req_from_line( | ||||
|                             COMMENT_RE.sub("", line).strip(), | ||||
|                             isolated=isolated, | ||||
|                         ) | ||||
| 
 | ||||
|                     if not line_req.name: | ||||
|                         logger.info( | ||||
|                             "Skipping line in requirement file [%s] because " | ||||
|                             "it's not clear what it would install: %s", | ||||
|                             req_file_path, | ||||
|                             line.strip(), | ||||
|                         ) | ||||
|                         logger.info( | ||||
|                             "  (add #egg=PackageName to the URL to avoid" | ||||
|                             " this warning)" | ||||
|                         ) | ||||
|                     else: | ||||
|                         line_req_canonical_name = canonicalize_name(line_req.name) | ||||
|                         if line_req_canonical_name not in installations: | ||||
|                             # either it's not installed, or it is installed | ||||
|                             # but has been processed already | ||||
|                             if not req_files[line_req.name]: | ||||
|                                 logger.warning( | ||||
|                                     "Requirement file [%s] contains %s, but " | ||||
|                                     "package %r is not installed", | ||||
|                                     req_file_path, | ||||
|                                     COMMENT_RE.sub("", line).strip(), | ||||
|                                     line_req.name, | ||||
|                                 ) | ||||
|                             else: | ||||
|                                 req_files[line_req.name].append(req_file_path) | ||||
|                         else: | ||||
|                             yield str(installations[line_req_canonical_name]).rstrip() | ||||
|                             del installations[line_req_canonical_name] | ||||
|                             req_files[line_req.name].append(req_file_path) | ||||
| 
 | ||||
|         # Warn about requirements that were included multiple times (in a | ||||
|         # single requirements file or in different requirements files). | ||||
|         for name, files in req_files.items(): | ||||
|             if len(files) > 1: | ||||
|                 logger.warning( | ||||
|                     "Requirement %s included multiple times [%s]", | ||||
|                     name, | ||||
|                     ", ".join(sorted(set(files))), | ||||
|                 ) | ||||
| 
 | ||||
|         yield ("## The following requirements were added by pip freeze:") | ||||
|     for installation in sorted(installations.values(), key=lambda x: x.name.lower()): | ||||
|         if installation.canonical_name not in skip: | ||||
|             yield str(installation).rstrip() | ||||
| 
 | ||||
| 
 | ||||
| def _format_as_name_version(dist: BaseDistribution) -> str: | ||||
|     try: | ||||
|         dist_version = dist.version | ||||
|     except InvalidVersion: | ||||
|         # legacy version | ||||
|         return f"{dist.raw_name}==={dist.raw_version}" | ||||
|     else: | ||||
|         return f"{dist.raw_name}=={dist_version}" | ||||
| 
 | ||||
| 
 | ||||
| def _get_editable_info(dist: BaseDistribution) -> _EditableInfo: | ||||
|     """ | ||||
|     Compute and return values (req, comments) for use in | ||||
|     FrozenRequirement.from_dist(). | ||||
|     """ | ||||
|     editable_project_location = dist.editable_project_location | ||||
|     assert editable_project_location | ||||
|     location = os.path.normcase(os.path.abspath(editable_project_location)) | ||||
| 
 | ||||
|     from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs | ||||
| 
 | ||||
|     vcs_backend = vcs.get_backend_for_dir(location) | ||||
| 
 | ||||
|     if vcs_backend is None: | ||||
|         display = _format_as_name_version(dist) | ||||
|         logger.debug( | ||||
|             'No VCS found for editable requirement "%s" in: %r', | ||||
|             display, | ||||
|             location, | ||||
|         ) | ||||
|         return _EditableInfo( | ||||
|             requirement=location, | ||||
|             comments=[f"# Editable install with no version control ({display})"], | ||||
|         ) | ||||
| 
 | ||||
|     vcs_name = type(vcs_backend).__name__ | ||||
| 
 | ||||
|     try: | ||||
|         req = vcs_backend.get_src_requirement(location, dist.raw_name) | ||||
|     except RemoteNotFoundError: | ||||
|         display = _format_as_name_version(dist) | ||||
|         return _EditableInfo( | ||||
|             requirement=location, | ||||
|             comments=[f"# Editable {vcs_name} install with no remote ({display})"], | ||||
|         ) | ||||
|     except RemoteNotValidError as ex: | ||||
|         display = _format_as_name_version(dist) | ||||
|         return _EditableInfo( | ||||
|             requirement=location, | ||||
|             comments=[ | ||||
|                 f"# Editable {vcs_name} install ({display}) with either a deleted " | ||||
|                 f"local remote or invalid URI:", | ||||
|                 f"# '{ex.url}'", | ||||
|             ], | ||||
|         ) | ||||
|     except BadCommand: | ||||
|         logger.warning( | ||||
|             "cannot determine version of editable source in %s " | ||||
|             "(%s command not found in path)", | ||||
|             location, | ||||
|             vcs_backend.name, | ||||
|         ) | ||||
|         return _EditableInfo(requirement=location, comments=[]) | ||||
|     except InstallationError as exc: | ||||
|         logger.warning("Error when trying to get requirement for VCS system %s", exc) | ||||
|     else: | ||||
|         return _EditableInfo(requirement=req, comments=[]) | ||||
| 
 | ||||
|     logger.warning("Could not determine repository location of %s", location) | ||||
| 
 | ||||
|     return _EditableInfo( | ||||
|         requirement=location, | ||||
|         comments=["## !! Could not determine repository location"], | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True) | ||||
| class FrozenRequirement: | ||||
|     name: str | ||||
|     req: str | ||||
|     editable: bool | ||||
|     comments: Iterable[str] = field(default_factory=tuple) | ||||
| 
 | ||||
|     @property | ||||
|     def canonical_name(self) -> NormalizedName: | ||||
|         return canonicalize_name(self.name) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement": | ||||
|         editable = dist.editable | ||||
|         if editable: | ||||
|             req, comments = _get_editable_info(dist) | ||||
|         else: | ||||
|             comments = [] | ||||
|             direct_url = dist.direct_url | ||||
|             if direct_url: | ||||
|                 # if PEP 610 metadata is present, use it | ||||
|                 req = direct_url_as_pep440_direct_reference(direct_url, dist.raw_name) | ||||
|             else: | ||||
|                 # name==version requirement | ||||
|                 req = _format_as_name_version(dist) | ||||
| 
 | ||||
|         return cls(dist.raw_name, req, editable, comments=comments) | ||||
| 
 | ||||
|     def __str__(self) -> str: | ||||
|         req = self.req | ||||
|         if self.editable: | ||||
|             req = f"-e {req}" | ||||
|         return "\n".join(list(self.comments) + [str(req)]) + "\n" | ||||
|  | @ -0,0 +1,2 @@ | |||
| """For modules related to installing packages. | ||||
| """ | ||||
|  | @ -0,0 +1,47 @@ | |||
| """Legacy editable installation process, i.e. `setup.py develop`. | ||||
| """ | ||||
| 
 | ||||
| import logging | ||||
| from typing import Optional, Sequence | ||||
| 
 | ||||
| from pip._internal.build_env import BuildEnvironment | ||||
| from pip._internal.utils.logging import indent_log | ||||
| from pip._internal.utils.setuptools_build import make_setuptools_develop_args | ||||
| from pip._internal.utils.subprocess import call_subprocess | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def install_editable( | ||||
|     *, | ||||
|     global_options: Sequence[str], | ||||
|     prefix: Optional[str], | ||||
|     home: Optional[str], | ||||
|     use_user_site: bool, | ||||
|     name: str, | ||||
|     setup_py_path: str, | ||||
|     isolated: bool, | ||||
|     build_env: BuildEnvironment, | ||||
|     unpacked_source_directory: str, | ||||
| ) -> None: | ||||
|     """Install a package in editable mode. Most arguments are pass-through | ||||
|     to setuptools. | ||||
|     """ | ||||
|     logger.info("Running setup.py develop for %s", name) | ||||
| 
 | ||||
|     args = make_setuptools_develop_args( | ||||
|         setup_py_path, | ||||
|         global_options=global_options, | ||||
|         no_user_config=isolated, | ||||
|         prefix=prefix, | ||||
|         home=home, | ||||
|         use_user_site=use_user_site, | ||||
|     ) | ||||
| 
 | ||||
|     with indent_log(): | ||||
|         with build_env: | ||||
|             call_subprocess( | ||||
|                 args, | ||||
|                 command_desc="python setup.py develop", | ||||
|                 cwd=unpacked_source_directory, | ||||
|             ) | ||||
|  | @ -0,0 +1,741 @@ | |||
| """Support for installing and building the "wheel" binary package format. | ||||
| """ | ||||
| 
 | ||||
| import collections | ||||
| import compileall | ||||
| import contextlib | ||||
| import csv | ||||
| import importlib | ||||
| import logging | ||||
| import os.path | ||||
| import re | ||||
| import shutil | ||||
| import sys | ||||
| import warnings | ||||
| from base64 import urlsafe_b64encode | ||||
| from email.message import Message | ||||
| from itertools import chain, filterfalse, starmap | ||||
| from typing import ( | ||||
|     IO, | ||||
|     TYPE_CHECKING, | ||||
|     Any, | ||||
|     BinaryIO, | ||||
|     Callable, | ||||
|     Dict, | ||||
|     Generator, | ||||
|     Iterable, | ||||
|     Iterator, | ||||
|     List, | ||||
|     NewType, | ||||
|     Optional, | ||||
|     Protocol, | ||||
|     Sequence, | ||||
|     Set, | ||||
|     Tuple, | ||||
|     Union, | ||||
|     cast, | ||||
| ) | ||||
| from zipfile import ZipFile, ZipInfo | ||||
| 
 | ||||
| from pip._vendor.distlib.scripts import ScriptMaker | ||||
| from pip._vendor.distlib.util import get_export_entry | ||||
| from pip._vendor.packaging.utils import canonicalize_name | ||||
| 
 | ||||
| from pip._internal.exceptions import InstallationError | ||||
| from pip._internal.locations import get_major_minor_version | ||||
| from pip._internal.metadata import ( | ||||
|     BaseDistribution, | ||||
|     FilesystemWheel, | ||||
|     get_wheel_distribution, | ||||
| ) | ||||
| from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl | ||||
| from pip._internal.models.scheme import SCHEME_KEYS, Scheme | ||||
| from pip._internal.utils.filesystem import adjacent_tmp_file, replace | ||||
| from pip._internal.utils.misc import StreamWrapper, ensure_dir, hash_file, partition | ||||
| from pip._internal.utils.unpacking import ( | ||||
|     current_umask, | ||||
|     is_within_directory, | ||||
|     set_extracted_file_to_default_mode_plus_executable, | ||||
|     zip_item_is_executable, | ||||
| ) | ||||
| from pip._internal.utils.wheel import parse_wheel | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
| 
 | ||||
|     class File(Protocol): | ||||
|         src_record_path: "RecordPath" | ||||
|         dest_path: str | ||||
|         changed: bool | ||||
| 
 | ||||
|         def save(self) -> None: | ||||
|             pass | ||||
| 
 | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| RecordPath = NewType("RecordPath", str) | ||||
| InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]] | ||||
| 
 | ||||
| 
 | ||||
| def rehash(path: str, blocksize: int = 1 << 20) -> Tuple[str, str]: | ||||
|     """Return (encoded_digest, length) for path using hashlib.sha256()""" | ||||
|     h, length = hash_file(path, blocksize) | ||||
|     digest = "sha256=" + urlsafe_b64encode(h.digest()).decode("latin1").rstrip("=") | ||||
|     return (digest, str(length)) | ||||
| 
 | ||||
| 
 | ||||
| def csv_io_kwargs(mode: str) -> Dict[str, Any]: | ||||
|     """Return keyword arguments to properly open a CSV file | ||||
|     in the given mode. | ||||
|     """ | ||||
|     return {"mode": mode, "newline": "", "encoding": "utf-8"} | ||||
| 
 | ||||
| 
 | ||||
| def fix_script(path: str) -> bool: | ||||
|     """Replace #!python with #!/path/to/python | ||||
|     Return True if file was changed. | ||||
|     """ | ||||
|     # XXX RECORD hashes will need to be updated | ||||
|     assert os.path.isfile(path) | ||||
| 
 | ||||
|     with open(path, "rb") as script: | ||||
|         firstline = script.readline() | ||||
|         if not firstline.startswith(b"#!python"): | ||||
|             return False | ||||
|         exename = sys.executable.encode(sys.getfilesystemencoding()) | ||||
|         firstline = b"#!" + exename + os.linesep.encode("ascii") | ||||
|         rest = script.read() | ||||
|     with open(path, "wb") as script: | ||||
|         script.write(firstline) | ||||
|         script.write(rest) | ||||
|     return True | ||||
| 
 | ||||
| 
 | ||||
| def wheel_root_is_purelib(metadata: Message) -> bool: | ||||
|     return metadata.get("Root-Is-Purelib", "").lower() == "true" | ||||
| 
 | ||||
| 
 | ||||
| def get_entrypoints(dist: BaseDistribution) -> Tuple[Dict[str, str], Dict[str, str]]: | ||||
|     console_scripts = {} | ||||
|     gui_scripts = {} | ||||
|     for entry_point in dist.iter_entry_points(): | ||||
|         if entry_point.group == "console_scripts": | ||||
|             console_scripts[entry_point.name] = entry_point.value | ||||
|         elif entry_point.group == "gui_scripts": | ||||
|             gui_scripts[entry_point.name] = entry_point.value | ||||
|     return console_scripts, gui_scripts | ||||
| 
 | ||||
| 
 | ||||
| def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]: | ||||
|     """Determine if any scripts are not on PATH and format a warning. | ||||
|     Returns a warning message if one or more scripts are not on PATH, | ||||
|     otherwise None. | ||||
|     """ | ||||
|     if not scripts: | ||||
|         return None | ||||
| 
 | ||||
|     # Group scripts by the path they were installed in | ||||
|     grouped_by_dir: Dict[str, Set[str]] = collections.defaultdict(set) | ||||
|     for destfile in scripts: | ||||
|         parent_dir = os.path.dirname(destfile) | ||||
|         script_name = os.path.basename(destfile) | ||||
|         grouped_by_dir[parent_dir].add(script_name) | ||||
| 
 | ||||
|     # We don't want to warn for directories that are on PATH. | ||||
|     not_warn_dirs = [ | ||||
|         os.path.normcase(os.path.normpath(i)).rstrip(os.sep) | ||||
|         for i in os.environ.get("PATH", "").split(os.pathsep) | ||||
|     ] | ||||
|     # If an executable sits with sys.executable, we don't warn for it. | ||||
|     #     This covers the case of venv invocations without activating the venv. | ||||
|     not_warn_dirs.append( | ||||
|         os.path.normcase(os.path.normpath(os.path.dirname(sys.executable))) | ||||
|     ) | ||||
|     warn_for: Dict[str, Set[str]] = { | ||||
|         parent_dir: scripts | ||||
|         for parent_dir, scripts in grouped_by_dir.items() | ||||
|         if os.path.normcase(os.path.normpath(parent_dir)) not in not_warn_dirs | ||||
|     } | ||||
|     if not warn_for: | ||||
|         return None | ||||
| 
 | ||||
|     # Format a message | ||||
|     msg_lines = [] | ||||
|     for parent_dir, dir_scripts in warn_for.items(): | ||||
|         sorted_scripts: List[str] = sorted(dir_scripts) | ||||
|         if len(sorted_scripts) == 1: | ||||
|             start_text = f"script {sorted_scripts[0]} is" | ||||
|         else: | ||||
|             start_text = "scripts {} are".format( | ||||
|                 ", ".join(sorted_scripts[:-1]) + " and " + sorted_scripts[-1] | ||||
|             ) | ||||
| 
 | ||||
|         msg_lines.append( | ||||
|             f"The {start_text} installed in '{parent_dir}' which is not on PATH." | ||||
|         ) | ||||
| 
 | ||||
|     last_line_fmt = ( | ||||
|         "Consider adding {} to PATH or, if you prefer " | ||||
|         "to suppress this warning, use --no-warn-script-location." | ||||
|     ) | ||||
|     if len(msg_lines) == 1: | ||||
|         msg_lines.append(last_line_fmt.format("this directory")) | ||||
|     else: | ||||
|         msg_lines.append(last_line_fmt.format("these directories")) | ||||
| 
 | ||||
|     # Add a note if any directory starts with ~ | ||||
|     warn_for_tilde = any( | ||||
|         i[0] == "~" for i in os.environ.get("PATH", "").split(os.pathsep) if i | ||||
|     ) | ||||
|     if warn_for_tilde: | ||||
|         tilde_warning_msg = ( | ||||
|             "NOTE: The current PATH contains path(s) starting with `~`, " | ||||
|             "which may not be expanded by all applications." | ||||
|         ) | ||||
|         msg_lines.append(tilde_warning_msg) | ||||
| 
 | ||||
|     # Returns the formatted multiline message | ||||
|     return "\n".join(msg_lines) | ||||
| 
 | ||||
| 
 | ||||
| def _normalized_outrows( | ||||
|     outrows: Iterable[InstalledCSVRow], | ||||
| ) -> List[Tuple[str, str, str]]: | ||||
|     """Normalize the given rows of a RECORD file. | ||||
| 
 | ||||
|     Items in each row are converted into str. Rows are then sorted to make | ||||
|     the value more predictable for tests. | ||||
| 
 | ||||
|     Each row is a 3-tuple (path, hash, size) and corresponds to a record of | ||||
|     a RECORD file (see PEP 376 and PEP 427 for details).  For the rows | ||||
|     passed to this function, the size can be an integer as an int or string, | ||||
|     or the empty string. | ||||
|     """ | ||||
|     # Normally, there should only be one row per path, in which case the | ||||
|     # second and third elements don't come into play when sorting. | ||||
|     # However, in cases in the wild where a path might happen to occur twice, | ||||
|     # we don't want the sort operation to trigger an error (but still want | ||||
|     # determinism).  Since the third element can be an int or string, we | ||||
|     # coerce each element to a string to avoid a TypeError in this case. | ||||
|     # For additional background, see-- | ||||
|     # https://github.com/pypa/pip/issues/5868 | ||||
|     return sorted( | ||||
|         (record_path, hash_, str(size)) for record_path, hash_, size in outrows | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def _record_to_fs_path(record_path: RecordPath, lib_dir: str) -> str: | ||||
|     return os.path.join(lib_dir, record_path) | ||||
| 
 | ||||
| 
 | ||||
| def _fs_to_record_path(path: str, lib_dir: str) -> RecordPath: | ||||
|     # On Windows, do not handle relative paths if they belong to different | ||||
|     # logical disks | ||||
|     if os.path.splitdrive(path)[0].lower() == os.path.splitdrive(lib_dir)[0].lower(): | ||||
|         path = os.path.relpath(path, lib_dir) | ||||
| 
 | ||||
|     path = path.replace(os.path.sep, "/") | ||||
|     return cast("RecordPath", path) | ||||
| 
 | ||||
| 
 | ||||
| def get_csv_rows_for_installed( | ||||
|     old_csv_rows: List[List[str]], | ||||
|     installed: Dict[RecordPath, RecordPath], | ||||
|     changed: Set[RecordPath], | ||||
|     generated: List[str], | ||||
|     lib_dir: str, | ||||
| ) -> List[InstalledCSVRow]: | ||||
|     """ | ||||
|     :param installed: A map from archive RECORD path to installation RECORD | ||||
|         path. | ||||
|     """ | ||||
|     installed_rows: List[InstalledCSVRow] = [] | ||||
|     for row in old_csv_rows: | ||||
|         if len(row) > 3: | ||||
|             logger.warning("RECORD line has more than three elements: %s", row) | ||||
|         old_record_path = cast("RecordPath", row[0]) | ||||
|         new_record_path = installed.pop(old_record_path, old_record_path) | ||||
|         if new_record_path in changed: | ||||
|             digest, length = rehash(_record_to_fs_path(new_record_path, lib_dir)) | ||||
|         else: | ||||
|             digest = row[1] if len(row) > 1 else "" | ||||
|             length = row[2] if len(row) > 2 else "" | ||||
|         installed_rows.append((new_record_path, digest, length)) | ||||
|     for f in generated: | ||||
|         path = _fs_to_record_path(f, lib_dir) | ||||
|         digest, length = rehash(f) | ||||
|         installed_rows.append((path, digest, length)) | ||||
|     return installed_rows + [ | ||||
|         (installed_record_path, "", "") for installed_record_path in installed.values() | ||||
|     ] | ||||
| 
 | ||||
| 
 | ||||
| def get_console_script_specs(console: Dict[str, str]) -> List[str]: | ||||
|     """ | ||||
|     Given the mapping from entrypoint name to callable, return the relevant | ||||
|     console script specs. | ||||
|     """ | ||||
|     # Don't mutate caller's version | ||||
|     console = console.copy() | ||||
| 
 | ||||
|     scripts_to_generate = [] | ||||
| 
 | ||||
|     # Special case pip and setuptools to generate versioned wrappers | ||||
|     # | ||||
|     # The issue is that some projects (specifically, pip and setuptools) use | ||||
|     # code in setup.py to create "versioned" entry points - pip2.7 on Python | ||||
|     # 2.7, pip3.3 on Python 3.3, etc. But these entry points are baked into | ||||
|     # the wheel metadata at build time, and so if the wheel is installed with | ||||
|     # a *different* version of Python the entry points will be wrong. The | ||||
|     # correct fix for this is to enhance the metadata to be able to describe | ||||
|     # such versioned entry points. | ||||
|     # Currently, projects using versioned entry points will either have | ||||
|     # incorrect versioned entry points, or they will not be able to distribute | ||||
|     # "universal" wheels (i.e., they will need a wheel per Python version). | ||||
|     # | ||||
|     # Because setuptools and pip are bundled with _ensurepip and virtualenv, | ||||
|     # we need to use universal wheels. As a workaround, we | ||||
|     # override the versioned entry points in the wheel and generate the | ||||
|     # correct ones. | ||||
|     # | ||||
|     # To add the level of hack in this section of code, in order to support | ||||
|     # ensurepip this code will look for an ``ENSUREPIP_OPTIONS`` environment | ||||
|     # variable which will control which version scripts get installed. | ||||
|     # | ||||
|     # ENSUREPIP_OPTIONS=altinstall | ||||
|     #   - Only pipX.Y and easy_install-X.Y will be generated and installed | ||||
|     # ENSUREPIP_OPTIONS=install | ||||
|     #   - pipX.Y, pipX, easy_install-X.Y will be generated and installed. Note | ||||
|     #     that this option is technically if ENSUREPIP_OPTIONS is set and is | ||||
|     #     not altinstall | ||||
|     # DEFAULT | ||||
|     #   - The default behavior is to install pip, pipX, pipX.Y, easy_install | ||||
|     #     and easy_install-X.Y. | ||||
|     pip_script = console.pop("pip", None) | ||||
|     if pip_script: | ||||
|         if "ENSUREPIP_OPTIONS" not in os.environ: | ||||
|             scripts_to_generate.append("pip = " + pip_script) | ||||
| 
 | ||||
|         if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall": | ||||
|             scripts_to_generate.append(f"pip{sys.version_info[0]} = {pip_script}") | ||||
| 
 | ||||
|         scripts_to_generate.append(f"pip{get_major_minor_version()} = {pip_script}") | ||||
|         # Delete any other versioned pip entry points | ||||
|         pip_ep = [k for k in console if re.match(r"pip(\d+(\.\d+)?)?$", k)] | ||||
|         for k in pip_ep: | ||||
|             del console[k] | ||||
|     easy_install_script = console.pop("easy_install", None) | ||||
|     if easy_install_script: | ||||
|         if "ENSUREPIP_OPTIONS" not in os.environ: | ||||
|             scripts_to_generate.append("easy_install = " + easy_install_script) | ||||
| 
 | ||||
|         scripts_to_generate.append( | ||||
|             f"easy_install-{get_major_minor_version()} = {easy_install_script}" | ||||
|         ) | ||||
|         # Delete any other versioned easy_install entry points | ||||
|         easy_install_ep = [ | ||||
|             k for k in console if re.match(r"easy_install(-\d+\.\d+)?$", k) | ||||
|         ] | ||||
|         for k in easy_install_ep: | ||||
|             del console[k] | ||||
| 
 | ||||
|     # Generate the console entry points specified in the wheel | ||||
|     scripts_to_generate.extend(starmap("{} = {}".format, console.items())) | ||||
| 
 | ||||
|     return scripts_to_generate | ||||
| 
 | ||||
| 
 | ||||
| class ZipBackedFile: | ||||
|     def __init__( | ||||
|         self, src_record_path: RecordPath, dest_path: str, zip_file: ZipFile | ||||
|     ) -> None: | ||||
|         self.src_record_path = src_record_path | ||||
|         self.dest_path = dest_path | ||||
|         self._zip_file = zip_file | ||||
|         self.changed = False | ||||
| 
 | ||||
|     def _getinfo(self) -> ZipInfo: | ||||
|         return self._zip_file.getinfo(self.src_record_path) | ||||
| 
 | ||||
|     def save(self) -> None: | ||||
|         # When we open the output file below, any existing file is truncated | ||||
|         # before we start writing the new contents. This is fine in most | ||||
|         # cases, but can cause a segfault if pip has loaded a shared | ||||
|         # object (e.g. from pyopenssl through its vendored urllib3) | ||||
|         # Since the shared object is mmap'd an attempt to call a | ||||
|         # symbol in it will then cause a segfault. Unlinking the file | ||||
|         # allows writing of new contents while allowing the process to | ||||
|         # continue to use the old copy. | ||||
|         if os.path.exists(self.dest_path): | ||||
|             os.unlink(self.dest_path) | ||||
| 
 | ||||
|         zipinfo = self._getinfo() | ||||
| 
 | ||||
|         # optimization: the file is created by open(), | ||||
|         # skip the decompression when there is 0 bytes to decompress. | ||||
|         with open(self.dest_path, "wb") as dest: | ||||
|             if zipinfo.file_size > 0: | ||||
|                 with self._zip_file.open(zipinfo) as f: | ||||
|                     blocksize = min(zipinfo.file_size, 1024 * 1024) | ||||
|                     shutil.copyfileobj(f, dest, blocksize) | ||||
| 
 | ||||
|         if zip_item_is_executable(zipinfo): | ||||
|             set_extracted_file_to_default_mode_plus_executable(self.dest_path) | ||||
| 
 | ||||
| 
 | ||||
| class ScriptFile: | ||||
|     def __init__(self, file: "File") -> None: | ||||
|         self._file = file | ||||
|         self.src_record_path = self._file.src_record_path | ||||
|         self.dest_path = self._file.dest_path | ||||
|         self.changed = False | ||||
| 
 | ||||
|     def save(self) -> None: | ||||
|         self._file.save() | ||||
|         self.changed = fix_script(self.dest_path) | ||||
| 
 | ||||
| 
 | ||||
| class MissingCallableSuffix(InstallationError): | ||||
|     def __init__(self, entry_point: str) -> None: | ||||
|         super().__init__( | ||||
|             f"Invalid script entry point: {entry_point} - A callable " | ||||
|             "suffix is required. Cf https://packaging.python.org/" | ||||
|             "specifications/entry-points/#use-for-scripts for more " | ||||
|             "information." | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| def _raise_for_invalid_entrypoint(specification: str) -> None: | ||||
|     entry = get_export_entry(specification) | ||||
|     if entry is not None and entry.suffix is None: | ||||
|         raise MissingCallableSuffix(str(entry)) | ||||
| 
 | ||||
| 
 | ||||
| class PipScriptMaker(ScriptMaker): | ||||
|     def make( | ||||
|         self, specification: str, options: Optional[Dict[str, Any]] = None | ||||
|     ) -> List[str]: | ||||
|         _raise_for_invalid_entrypoint(specification) | ||||
|         return super().make(specification, options) | ||||
| 
 | ||||
| 
 | ||||
| def _install_wheel(  # noqa: C901, PLR0915 function is too long | ||||
|     name: str, | ||||
|     wheel_zip: ZipFile, | ||||
|     wheel_path: str, | ||||
|     scheme: Scheme, | ||||
|     pycompile: bool = True, | ||||
|     warn_script_location: bool = True, | ||||
|     direct_url: Optional[DirectUrl] = None, | ||||
|     requested: bool = False, | ||||
| ) -> None: | ||||
|     """Install a wheel. | ||||
| 
 | ||||
|     :param name: Name of the project to install | ||||
|     :param wheel_zip: open ZipFile for wheel being installed | ||||
|     :param scheme: Distutils scheme dictating the install directories | ||||
|     :param req_description: String used in place of the requirement, for | ||||
|         logging | ||||
|     :param pycompile: Whether to byte-compile installed Python files | ||||
|     :param warn_script_location: Whether to check that scripts are installed | ||||
|         into a directory on PATH | ||||
|     :raises UnsupportedWheel: | ||||
|         * when the directory holds an unpacked wheel with incompatible | ||||
|           Wheel-Version | ||||
|         * when the .dist-info dir does not match the wheel | ||||
|     """ | ||||
|     info_dir, metadata = parse_wheel(wheel_zip, name) | ||||
| 
 | ||||
|     if wheel_root_is_purelib(metadata): | ||||
|         lib_dir = scheme.purelib | ||||
|     else: | ||||
|         lib_dir = scheme.platlib | ||||
| 
 | ||||
|     # Record details of the files moved | ||||
|     #   installed = files copied from the wheel to the destination | ||||
|     #   changed = files changed while installing (scripts #! line typically) | ||||
|     #   generated = files newly generated during the install (script wrappers) | ||||
|     installed: Dict[RecordPath, RecordPath] = {} | ||||
|     changed: Set[RecordPath] = set() | ||||
|     generated: List[str] = [] | ||||
| 
 | ||||
|     def record_installed( | ||||
|         srcfile: RecordPath, destfile: str, modified: bool = False | ||||
|     ) -> None: | ||||
|         """Map archive RECORD paths to installation RECORD paths.""" | ||||
|         newpath = _fs_to_record_path(destfile, lib_dir) | ||||
|         installed[srcfile] = newpath | ||||
|         if modified: | ||||
|             changed.add(newpath) | ||||
| 
 | ||||
|     def is_dir_path(path: RecordPath) -> bool: | ||||
|         return path.endswith("/") | ||||
| 
 | ||||
|     def assert_no_path_traversal(dest_dir_path: str, target_path: str) -> None: | ||||
|         if not is_within_directory(dest_dir_path, target_path): | ||||
|             message = ( | ||||
|                 "The wheel {!r} has a file {!r} trying to install" | ||||
|                 " outside the target directory {!r}" | ||||
|             ) | ||||
|             raise InstallationError( | ||||
|                 message.format(wheel_path, target_path, dest_dir_path) | ||||
|             ) | ||||
| 
 | ||||
|     def root_scheme_file_maker( | ||||
|         zip_file: ZipFile, dest: str | ||||
|     ) -> Callable[[RecordPath], "File"]: | ||||
|         def make_root_scheme_file(record_path: RecordPath) -> "File": | ||||
|             normed_path = os.path.normpath(record_path) | ||||
|             dest_path = os.path.join(dest, normed_path) | ||||
|             assert_no_path_traversal(dest, dest_path) | ||||
|             return ZipBackedFile(record_path, dest_path, zip_file) | ||||
| 
 | ||||
|         return make_root_scheme_file | ||||
| 
 | ||||
|     def data_scheme_file_maker( | ||||
|         zip_file: ZipFile, scheme: Scheme | ||||
|     ) -> Callable[[RecordPath], "File"]: | ||||
|         scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS} | ||||
| 
 | ||||
|         def make_data_scheme_file(record_path: RecordPath) -> "File": | ||||
|             normed_path = os.path.normpath(record_path) | ||||
|             try: | ||||
|                 _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) | ||||
|             except ValueError: | ||||
|                 message = ( | ||||
|                     f"Unexpected file in {wheel_path}: {record_path!r}. .data directory" | ||||
|                     " contents should be named like: '<scheme key>/<path>'." | ||||
|                 ) | ||||
|                 raise InstallationError(message) | ||||
| 
 | ||||
|             try: | ||||
|                 scheme_path = scheme_paths[scheme_key] | ||||
|             except KeyError: | ||||
|                 valid_scheme_keys = ", ".join(sorted(scheme_paths)) | ||||
|                 message = ( | ||||
|                     f"Unknown scheme key used in {wheel_path}: {scheme_key} " | ||||
|                     f"(for file {record_path!r}). .data directory contents " | ||||
|                     f"should be in subdirectories named with a valid scheme " | ||||
|                     f"key ({valid_scheme_keys})" | ||||
|                 ) | ||||
|                 raise InstallationError(message) | ||||
| 
 | ||||
|             dest_path = os.path.join(scheme_path, dest_subpath) | ||||
|             assert_no_path_traversal(scheme_path, dest_path) | ||||
|             return ZipBackedFile(record_path, dest_path, zip_file) | ||||
| 
 | ||||
|         return make_data_scheme_file | ||||
| 
 | ||||
|     def is_data_scheme_path(path: RecordPath) -> bool: | ||||
|         return path.split("/", 1)[0].endswith(".data") | ||||
| 
 | ||||
|     paths = cast(List[RecordPath], wheel_zip.namelist()) | ||||
|     file_paths = filterfalse(is_dir_path, paths) | ||||
|     root_scheme_paths, data_scheme_paths = partition(is_data_scheme_path, file_paths) | ||||
| 
 | ||||
|     make_root_scheme_file = root_scheme_file_maker(wheel_zip, lib_dir) | ||||
|     files: Iterator[File] = map(make_root_scheme_file, root_scheme_paths) | ||||
| 
 | ||||
|     def is_script_scheme_path(path: RecordPath) -> bool: | ||||
|         parts = path.split("/", 2) | ||||
|         return len(parts) > 2 and parts[0].endswith(".data") and parts[1] == "scripts" | ||||
| 
 | ||||
|     other_scheme_paths, script_scheme_paths = partition( | ||||
|         is_script_scheme_path, data_scheme_paths | ||||
|     ) | ||||
| 
 | ||||
|     make_data_scheme_file = data_scheme_file_maker(wheel_zip, scheme) | ||||
|     other_scheme_files = map(make_data_scheme_file, other_scheme_paths) | ||||
|     files = chain(files, other_scheme_files) | ||||
| 
 | ||||
|     # Get the defined entry points | ||||
|     distribution = get_wheel_distribution( | ||||
|         FilesystemWheel(wheel_path), | ||||
|         canonicalize_name(name), | ||||
|     ) | ||||
|     console, gui = get_entrypoints(distribution) | ||||
| 
 | ||||
|     def is_entrypoint_wrapper(file: "File") -> bool: | ||||
|         # EP, EP.exe and EP-script.py are scripts generated for | ||||
|         # entry point EP by setuptools | ||||
|         path = file.dest_path | ||||
|         name = os.path.basename(path) | ||||
|         if name.lower().endswith(".exe"): | ||||
|             matchname = name[:-4] | ||||
|         elif name.lower().endswith("-script.py"): | ||||
|             matchname = name[:-10] | ||||
|         elif name.lower().endswith(".pya"): | ||||
|             matchname = name[:-4] | ||||
|         else: | ||||
|             matchname = name | ||||
|         # Ignore setuptools-generated scripts | ||||
|         return matchname in console or matchname in gui | ||||
| 
 | ||||
|     script_scheme_files: Iterator[File] = map( | ||||
|         make_data_scheme_file, script_scheme_paths | ||||
|     ) | ||||
|     script_scheme_files = filterfalse(is_entrypoint_wrapper, script_scheme_files) | ||||
|     script_scheme_files = map(ScriptFile, script_scheme_files) | ||||
|     files = chain(files, script_scheme_files) | ||||
| 
 | ||||
|     existing_parents = set() | ||||
|     for file in files: | ||||
|         # directory creation is lazy and after file filtering | ||||
|         # to ensure we don't install empty dirs; empty dirs can't be | ||||
|         # uninstalled. | ||||
|         parent_dir = os.path.dirname(file.dest_path) | ||||
|         if parent_dir not in existing_parents: | ||||
|             ensure_dir(parent_dir) | ||||
|             existing_parents.add(parent_dir) | ||||
|         file.save() | ||||
|         record_installed(file.src_record_path, file.dest_path, file.changed) | ||||
| 
 | ||||
|     def pyc_source_file_paths() -> Generator[str, None, None]: | ||||
|         # We de-duplicate installation paths, since there can be overlap (e.g. | ||||
|         # file in .data maps to same location as file in wheel root). | ||||
|         # Sorting installation paths makes it easier to reproduce and debug | ||||
|         # issues related to permissions on existing files. | ||||
|         for installed_path in sorted(set(installed.values())): | ||||
|             full_installed_path = os.path.join(lib_dir, installed_path) | ||||
|             if not os.path.isfile(full_installed_path): | ||||
|                 continue | ||||
|             if not full_installed_path.endswith(".py"): | ||||
|                 continue | ||||
|             yield full_installed_path | ||||
| 
 | ||||
|     def pyc_output_path(path: str) -> str: | ||||
|         """Return the path the pyc file would have been written to.""" | ||||
|         return importlib.util.cache_from_source(path) | ||||
| 
 | ||||
|     # Compile all of the pyc files for the installed files | ||||
|     if pycompile: | ||||
|         with contextlib.redirect_stdout( | ||||
|             StreamWrapper.from_stream(sys.stdout) | ||||
|         ) as stdout: | ||||
|             with warnings.catch_warnings(): | ||||
|                 warnings.filterwarnings("ignore") | ||||
|                 for path in pyc_source_file_paths(): | ||||
|                     success = compileall.compile_file(path, force=True, quiet=True) | ||||
|                     if success: | ||||
|                         pyc_path = pyc_output_path(path) | ||||
|                         assert os.path.exists(pyc_path) | ||||
|                         pyc_record_path = cast( | ||||
|                             "RecordPath", pyc_path.replace(os.path.sep, "/") | ||||
|                         ) | ||||
|                         record_installed(pyc_record_path, pyc_path) | ||||
|         logger.debug(stdout.getvalue()) | ||||
| 
 | ||||
|     maker = PipScriptMaker(None, scheme.scripts) | ||||
| 
 | ||||
|     # Ensure old scripts are overwritten. | ||||
|     # See https://github.com/pypa/pip/issues/1800 | ||||
|     maker.clobber = True | ||||
| 
 | ||||
|     # Ensure we don't generate any variants for scripts because this is almost | ||||
|     # never what somebody wants. | ||||
|     # See https://bitbucket.org/pypa/distlib/issue/35/ | ||||
|     maker.variants = {""} | ||||
| 
 | ||||
|     # This is required because otherwise distlib creates scripts that are not | ||||
|     # executable. | ||||
|     # See https://bitbucket.org/pypa/distlib/issue/32/ | ||||
|     maker.set_mode = True | ||||
| 
 | ||||
|     # Generate the console and GUI entry points specified in the wheel | ||||
|     scripts_to_generate = get_console_script_specs(console) | ||||
| 
 | ||||
|     gui_scripts_to_generate = list(starmap("{} = {}".format, gui.items())) | ||||
| 
 | ||||
|     generated_console_scripts = maker.make_multiple(scripts_to_generate) | ||||
|     generated.extend(generated_console_scripts) | ||||
| 
 | ||||
|     generated.extend(maker.make_multiple(gui_scripts_to_generate, {"gui": True})) | ||||
| 
 | ||||
|     if warn_script_location: | ||||
|         msg = message_about_scripts_not_on_PATH(generated_console_scripts) | ||||
|         if msg is not None: | ||||
|             logger.warning(msg) | ||||
| 
 | ||||
|     generated_file_mode = 0o666 & ~current_umask() | ||||
| 
 | ||||
|     @contextlib.contextmanager | ||||
|     def _generate_file(path: str, **kwargs: Any) -> Generator[BinaryIO, None, None]: | ||||
|         with adjacent_tmp_file(path, **kwargs) as f: | ||||
|             yield f | ||||
|         os.chmod(f.name, generated_file_mode) | ||||
|         replace(f.name, path) | ||||
| 
 | ||||
|     dest_info_dir = os.path.join(lib_dir, info_dir) | ||||
| 
 | ||||
|     # Record pip as the installer | ||||
|     installer_path = os.path.join(dest_info_dir, "INSTALLER") | ||||
|     with _generate_file(installer_path) as installer_file: | ||||
|         installer_file.write(b"pip\n") | ||||
|     generated.append(installer_path) | ||||
| 
 | ||||
|     # Record the PEP 610 direct URL reference | ||||
|     if direct_url is not None: | ||||
|         direct_url_path = os.path.join(dest_info_dir, DIRECT_URL_METADATA_NAME) | ||||
|         with _generate_file(direct_url_path) as direct_url_file: | ||||
|             direct_url_file.write(direct_url.to_json().encode("utf-8")) | ||||
|         generated.append(direct_url_path) | ||||
| 
 | ||||
|     # Record the REQUESTED file | ||||
|     if requested: | ||||
|         requested_path = os.path.join(dest_info_dir, "REQUESTED") | ||||
|         with open(requested_path, "wb"): | ||||
|             pass | ||||
|         generated.append(requested_path) | ||||
| 
 | ||||
|     record_text = distribution.read_text("RECORD") | ||||
|     record_rows = list(csv.reader(record_text.splitlines())) | ||||
| 
 | ||||
|     rows = get_csv_rows_for_installed( | ||||
|         record_rows, | ||||
|         installed=installed, | ||||
|         changed=changed, | ||||
|         generated=generated, | ||||
|         lib_dir=lib_dir, | ||||
|     ) | ||||
| 
 | ||||
|     # Record details of all files installed | ||||
|     record_path = os.path.join(dest_info_dir, "RECORD") | ||||
| 
 | ||||
|     with _generate_file(record_path, **csv_io_kwargs("w")) as record_file: | ||||
|         # Explicitly cast to typing.IO[str] as a workaround for the mypy error: | ||||
|         # "writer" has incompatible type "BinaryIO"; expected "_Writer" | ||||
|         writer = csv.writer(cast("IO[str]", record_file)) | ||||
|         writer.writerows(_normalized_outrows(rows)) | ||||
| 
 | ||||
| 
 | ||||
| @contextlib.contextmanager | ||||
| def req_error_context(req_description: str) -> Generator[None, None, None]: | ||||
|     try: | ||||
|         yield | ||||
|     except InstallationError as e: | ||||
|         message = f"For req: {req_description}. {e.args[0]}" | ||||
|         raise InstallationError(message) from e | ||||
| 
 | ||||
| 
 | ||||
| def install_wheel( | ||||
|     name: str, | ||||
|     wheel_path: str, | ||||
|     scheme: Scheme, | ||||
|     req_description: str, | ||||
|     pycompile: bool = True, | ||||
|     warn_script_location: bool = True, | ||||
|     direct_url: Optional[DirectUrl] = None, | ||||
|     requested: bool = False, | ||||
| ) -> None: | ||||
|     with ZipFile(wheel_path, allowZip64=True) as z: | ||||
|         with req_error_context(req_description): | ||||
|             _install_wheel( | ||||
|                 name=name, | ||||
|                 wheel_zip=z, | ||||
|                 wheel_path=wheel_path, | ||||
|                 scheme=scheme, | ||||
|                 pycompile=pycompile, | ||||
|                 warn_script_location=warn_script_location, | ||||
|                 direct_url=direct_url, | ||||
|                 requested=requested, | ||||
|             ) | ||||
|  | @ -0,0 +1,732 @@ | |||
| """Prepares a distribution for installation | ||||
| """ | ||||
| 
 | ||||
| # The following comment should be removed at some point in the future. | ||||
| # mypy: strict-optional=False | ||||
| 
 | ||||
| import mimetypes | ||||
| import os | ||||
| import shutil | ||||
| from dataclasses import dataclass | ||||
| from pathlib import Path | ||||
| from typing import Dict, Iterable, List, Optional | ||||
| 
 | ||||
| from pip._vendor.packaging.utils import canonicalize_name | ||||
| 
 | ||||
| from pip._internal.distributions import make_distribution_for_install_requirement | ||||
| from pip._internal.distributions.installed import InstalledDistribution | ||||
| from pip._internal.exceptions import ( | ||||
|     DirectoryUrlHashUnsupported, | ||||
|     HashMismatch, | ||||
|     HashUnpinned, | ||||
|     InstallationError, | ||||
|     MetadataInconsistent, | ||||
|     NetworkConnectionError, | ||||
|     VcsHashUnsupported, | ||||
| ) | ||||
| from pip._internal.index.package_finder import PackageFinder | ||||
| from pip._internal.metadata import BaseDistribution, get_metadata_distribution | ||||
| from pip._internal.models.direct_url import ArchiveInfo | ||||
| from pip._internal.models.link import Link | ||||
| from pip._internal.models.wheel import Wheel | ||||
| from pip._internal.network.download import BatchDownloader, Downloader | ||||
| from pip._internal.network.lazy_wheel import ( | ||||
|     HTTPRangeRequestUnsupported, | ||||
|     dist_from_wheel_url, | ||||
| ) | ||||
| from pip._internal.network.session import PipSession | ||||
| from pip._internal.operations.build.build_tracker import BuildTracker | ||||
| from pip._internal.req.req_install import InstallRequirement | ||||
| from pip._internal.utils._log import getLogger | ||||
| from pip._internal.utils.direct_url_helpers import ( | ||||
|     direct_url_for_editable, | ||||
|     direct_url_from_link, | ||||
| ) | ||||
| from pip._internal.utils.hashes import Hashes, MissingHashes | ||||
| from pip._internal.utils.logging import indent_log | ||||
| from pip._internal.utils.misc import ( | ||||
|     display_path, | ||||
|     hash_file, | ||||
|     hide_url, | ||||
|     redact_auth_from_requirement, | ||||
| ) | ||||
| from pip._internal.utils.temp_dir import TempDirectory | ||||
| from pip._internal.utils.unpacking import unpack_file | ||||
| from pip._internal.vcs import vcs | ||||
| 
 | ||||
| logger = getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| def _get_prepared_distribution( | ||||
|     req: InstallRequirement, | ||||
|     build_tracker: BuildTracker, | ||||
|     finder: PackageFinder, | ||||
|     build_isolation: bool, | ||||
|     check_build_deps: bool, | ||||
| ) -> BaseDistribution: | ||||
|     """Prepare a distribution for installation.""" | ||||
|     abstract_dist = make_distribution_for_install_requirement(req) | ||||
|     tracker_id = abstract_dist.build_tracker_id | ||||
|     if tracker_id is not None: | ||||
|         with build_tracker.track(req, tracker_id): | ||||
|             abstract_dist.prepare_distribution_metadata( | ||||
|                 finder, build_isolation, check_build_deps | ||||
|             ) | ||||
|     return abstract_dist.get_metadata_distribution() | ||||
| 
 | ||||
| 
 | ||||
| def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None: | ||||
|     vcs_backend = vcs.get_backend_for_scheme(link.scheme) | ||||
|     assert vcs_backend is not None | ||||
|     vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity) | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class File: | ||||
|     path: str | ||||
|     content_type: Optional[str] = None | ||||
| 
 | ||||
|     def __post_init__(self) -> None: | ||||
|         if self.content_type is None: | ||||
|             self.content_type = mimetypes.guess_type(self.path)[0] | ||||
| 
 | ||||
| 
 | ||||
| def get_http_url( | ||||
|     link: Link, | ||||
|     download: Downloader, | ||||
|     download_dir: Optional[str] = None, | ||||
|     hashes: Optional[Hashes] = None, | ||||
| ) -> File: | ||||
|     temp_dir = TempDirectory(kind="unpack", globally_managed=True) | ||||
|     # If a download dir is specified, is the file already downloaded there? | ||||
|     already_downloaded_path = None | ||||
|     if download_dir: | ||||
|         already_downloaded_path = _check_download_dir(link, download_dir, hashes) | ||||
| 
 | ||||
|     if already_downloaded_path: | ||||
|         from_path = already_downloaded_path | ||||
|         content_type = None | ||||
|     else: | ||||
|         # let's download to a tmp dir | ||||
|         from_path, content_type = download(link, temp_dir.path) | ||||
|         if hashes: | ||||
|             hashes.check_against_path(from_path) | ||||
| 
 | ||||
|     return File(from_path, content_type) | ||||
| 
 | ||||
| 
 | ||||
| def get_file_url( | ||||
|     link: Link, download_dir: Optional[str] = None, hashes: Optional[Hashes] = None | ||||
| ) -> File: | ||||
|     """Get file and optionally check its hash.""" | ||||
|     # If a download dir is specified, is the file already there and valid? | ||||
|     already_downloaded_path = None | ||||
|     if download_dir: | ||||
|         already_downloaded_path = _check_download_dir(link, download_dir, hashes) | ||||
| 
 | ||||
|     if already_downloaded_path: | ||||
|         from_path = already_downloaded_path | ||||
|     else: | ||||
|         from_path = link.file_path | ||||
| 
 | ||||
|     # If --require-hashes is off, `hashes` is either empty, the | ||||
|     # link's embedded hash, or MissingHashes; it is required to | ||||
|     # match. If --require-hashes is on, we are satisfied by any | ||||
|     # hash in `hashes` matching: a URL-based or an option-based | ||||
|     # one; no internet-sourced hash will be in `hashes`. | ||||
|     if hashes: | ||||
|         hashes.check_against_path(from_path) | ||||
|     return File(from_path, None) | ||||
| 
 | ||||
| 
 | ||||
| def unpack_url( | ||||
|     link: Link, | ||||
|     location: str, | ||||
|     download: Downloader, | ||||
|     verbosity: int, | ||||
|     download_dir: Optional[str] = None, | ||||
|     hashes: Optional[Hashes] = None, | ||||
| ) -> Optional[File]: | ||||
|     """Unpack link into location, downloading if required. | ||||
| 
 | ||||
|     :param hashes: A Hashes object, one of whose embedded hashes must match, | ||||
|         or HashMismatch will be raised. If the Hashes is empty, no matches are | ||||
|         required, and unhashable types of requirements (like VCS ones, which | ||||
|         would ordinarily raise HashUnsupported) are allowed. | ||||
|     """ | ||||
|     # non-editable vcs urls | ||||
|     if link.is_vcs: | ||||
|         unpack_vcs_link(link, location, verbosity=verbosity) | ||||
|         return None | ||||
| 
 | ||||
|     assert not link.is_existing_dir() | ||||
| 
 | ||||
|     # file urls | ||||
|     if link.is_file: | ||||
|         file = get_file_url(link, download_dir, hashes=hashes) | ||||
| 
 | ||||
|     # http urls | ||||
|     else: | ||||
|         file = get_http_url( | ||||
|             link, | ||||
|             download, | ||||
|             download_dir, | ||||
|             hashes=hashes, | ||||
|         ) | ||||
| 
 | ||||
|     # unpack the archive to the build dir location. even when only downloading | ||||
|     # archives, they have to be unpacked to parse dependencies, except wheels | ||||
|     if not link.is_wheel: | ||||
|         unpack_file(file.path, location, file.content_type) | ||||
| 
 | ||||
|     return file | ||||
| 
 | ||||
| 
 | ||||
| def _check_download_dir( | ||||
|     link: Link, | ||||
|     download_dir: str, | ||||
|     hashes: Optional[Hashes], | ||||
|     warn_on_hash_mismatch: bool = True, | ||||
| ) -> Optional[str]: | ||||
|     """Check download_dir for previously downloaded file with correct hash | ||||
|     If a correct file is found return its path else None | ||||
|     """ | ||||
|     download_path = os.path.join(download_dir, link.filename) | ||||
| 
 | ||||
|     if not os.path.exists(download_path): | ||||
|         return None | ||||
| 
 | ||||
|     # If already downloaded, does its hash match? | ||||
|     logger.info("File was already downloaded %s", download_path) | ||||
|     if hashes: | ||||
|         try: | ||||
|             hashes.check_against_path(download_path) | ||||
|         except HashMismatch: | ||||
|             if warn_on_hash_mismatch: | ||||
|                 logger.warning( | ||||
|                     "Previously-downloaded file %s has bad hash. Re-downloading.", | ||||
|                     download_path, | ||||
|                 ) | ||||
|             os.unlink(download_path) | ||||
|             return None | ||||
|     return download_path | ||||
| 
 | ||||
| 
 | ||||
| class RequirementPreparer: | ||||
|     """Prepares a Requirement""" | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         build_dir: str, | ||||
|         download_dir: Optional[str], | ||||
|         src_dir: str, | ||||
|         build_isolation: bool, | ||||
|         check_build_deps: bool, | ||||
|         build_tracker: BuildTracker, | ||||
|         session: PipSession, | ||||
|         progress_bar: str, | ||||
|         finder: PackageFinder, | ||||
|         require_hashes: bool, | ||||
|         use_user_site: bool, | ||||
|         lazy_wheel: bool, | ||||
|         verbosity: int, | ||||
|         legacy_resolver: bool, | ||||
|     ) -> None: | ||||
|         super().__init__() | ||||
| 
 | ||||
|         self.src_dir = src_dir | ||||
|         self.build_dir = build_dir | ||||
|         self.build_tracker = build_tracker | ||||
|         self._session = session | ||||
|         self._download = Downloader(session, progress_bar) | ||||
|         self._batch_download = BatchDownloader(session, progress_bar) | ||||
|         self.finder = finder | ||||
| 
 | ||||
|         # Where still-packed archives should be written to. If None, they are | ||||
|         # not saved, and are deleted immediately after unpacking. | ||||
|         self.download_dir = download_dir | ||||
| 
 | ||||
|         # Is build isolation allowed? | ||||
|         self.build_isolation = build_isolation | ||||
| 
 | ||||
|         # Should check build dependencies? | ||||
|         self.check_build_deps = check_build_deps | ||||
| 
 | ||||
|         # Should hash-checking be required? | ||||
|         self.require_hashes = require_hashes | ||||
| 
 | ||||
|         # Should install in user site-packages? | ||||
|         self.use_user_site = use_user_site | ||||
| 
 | ||||
|         # Should wheels be downloaded lazily? | ||||
|         self.use_lazy_wheel = lazy_wheel | ||||
| 
 | ||||
|         # How verbose should underlying tooling be? | ||||
|         self.verbosity = verbosity | ||||
| 
 | ||||
|         # Are we using the legacy resolver? | ||||
|         self.legacy_resolver = legacy_resolver | ||||
| 
 | ||||
|         # Memoized downloaded files, as mapping of url: path. | ||||
|         self._downloaded: Dict[str, str] = {} | ||||
| 
 | ||||
|         # Previous "header" printed for a link-based InstallRequirement | ||||
|         self._previous_requirement_header = ("", "") | ||||
| 
 | ||||
|     def _log_preparing_link(self, req: InstallRequirement) -> None: | ||||
|         """Provide context for the requirement being prepared.""" | ||||
|         if req.link.is_file and not req.is_wheel_from_cache: | ||||
|             message = "Processing %s" | ||||
|             information = str(display_path(req.link.file_path)) | ||||
|         else: | ||||
|             message = "Collecting %s" | ||||
|             information = redact_auth_from_requirement(req.req) if req.req else str(req) | ||||
| 
 | ||||
|         # If we used req.req, inject requirement source if available (this | ||||
|         # would already be included if we used req directly) | ||||
|         if req.req and req.comes_from: | ||||
|             if isinstance(req.comes_from, str): | ||||
|                 comes_from: Optional[str] = req.comes_from | ||||
|             else: | ||||
|                 comes_from = req.comes_from.from_path() | ||||
|             if comes_from: | ||||
|                 information += f" (from {comes_from})" | ||||
| 
 | ||||
|         if (message, information) != self._previous_requirement_header: | ||||
|             self._previous_requirement_header = (message, information) | ||||
|             logger.info(message, information) | ||||
| 
 | ||||
|         if req.is_wheel_from_cache: | ||||
|             with indent_log(): | ||||
|                 logger.info("Using cached %s", req.link.filename) | ||||
| 
 | ||||
|     def _ensure_link_req_src_dir( | ||||
|         self, req: InstallRequirement, parallel_builds: bool | ||||
|     ) -> None: | ||||
|         """Ensure source_dir of a linked InstallRequirement.""" | ||||
|         # Since source_dir is only set for editable requirements. | ||||
|         if req.link.is_wheel: | ||||
|             # We don't need to unpack wheels, so no need for a source | ||||
|             # directory. | ||||
|             return | ||||
|         assert req.source_dir is None | ||||
|         if req.link.is_existing_dir(): | ||||
|             # build local directories in-tree | ||||
|             req.source_dir = req.link.file_path | ||||
|             return | ||||
| 
 | ||||
|         # We always delete unpacked sdists after pip runs. | ||||
|         req.ensure_has_source_dir( | ||||
|             self.build_dir, | ||||
|             autodelete=True, | ||||
|             parallel_builds=parallel_builds, | ||||
|         ) | ||||
|         req.ensure_pristine_source_checkout() | ||||
| 
 | ||||
|     def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes: | ||||
|         # By the time this is called, the requirement's link should have | ||||
|         # been checked so we can tell what kind of requirements req is | ||||
|         # and raise some more informative errors than otherwise. | ||||
|         # (For example, we can raise VcsHashUnsupported for a VCS URL | ||||
|         # rather than HashMissing.) | ||||
|         if not self.require_hashes: | ||||
|             return req.hashes(trust_internet=True) | ||||
| 
 | ||||
|         # We could check these first 2 conditions inside unpack_url | ||||
|         # and save repetition of conditions, but then we would | ||||
|         # report less-useful error messages for unhashable | ||||
|         # requirements, complaining that there's no hash provided. | ||||
|         if req.link.is_vcs: | ||||
|             raise VcsHashUnsupported() | ||||
|         if req.link.is_existing_dir(): | ||||
|             raise DirectoryUrlHashUnsupported() | ||||
| 
 | ||||
|         # Unpinned packages are asking for trouble when a new version | ||||
|         # is uploaded.  This isn't a security check, but it saves users | ||||
|         # a surprising hash mismatch in the future. | ||||
|         # file:/// URLs aren't pinnable, so don't complain about them | ||||
|         # not being pinned. | ||||
|         if not req.is_direct and not req.is_pinned: | ||||
|             raise HashUnpinned() | ||||
| 
 | ||||
|         # If known-good hashes are missing for this requirement, | ||||
|         # shim it with a facade object that will provoke hash | ||||
|         # computation and then raise a HashMissing exception | ||||
|         # showing the user what the hash should be. | ||||
|         return req.hashes(trust_internet=False) or MissingHashes() | ||||
| 
 | ||||
|     def _fetch_metadata_only( | ||||
|         self, | ||||
|         req: InstallRequirement, | ||||
|     ) -> Optional[BaseDistribution]: | ||||
|         if self.legacy_resolver: | ||||
|             logger.debug( | ||||
|                 "Metadata-only fetching is not used in the legacy resolver", | ||||
|             ) | ||||
|             return None | ||||
|         if self.require_hashes: | ||||
|             logger.debug( | ||||
|                 "Metadata-only fetching is not used as hash checking is required", | ||||
|             ) | ||||
|             return None | ||||
|         # Try PEP 658 metadata first, then fall back to lazy wheel if unavailable. | ||||
|         return self._fetch_metadata_using_link_data_attr( | ||||
|             req | ||||
|         ) or self._fetch_metadata_using_lazy_wheel(req.link) | ||||
| 
 | ||||
|     def _fetch_metadata_using_link_data_attr( | ||||
|         self, | ||||
|         req: InstallRequirement, | ||||
|     ) -> Optional[BaseDistribution]: | ||||
|         """Fetch metadata from the data-dist-info-metadata attribute, if possible.""" | ||||
|         # (1) Get the link to the metadata file, if provided by the backend. | ||||
|         metadata_link = req.link.metadata_link() | ||||
|         if metadata_link is None: | ||||
|             return None | ||||
|         assert req.req is not None | ||||
|         logger.verbose( | ||||
|             "Obtaining dependency information for %s from %s", | ||||
|             req.req, | ||||
|             metadata_link, | ||||
|         ) | ||||
|         # (2) Download the contents of the METADATA file, separate from the dist itself. | ||||
|         metadata_file = get_http_url( | ||||
|             metadata_link, | ||||
|             self._download, | ||||
|             hashes=metadata_link.as_hashes(), | ||||
|         ) | ||||
|         with open(metadata_file.path, "rb") as f: | ||||
|             metadata_contents = f.read() | ||||
|         # (3) Generate a dist just from those file contents. | ||||
|         metadata_dist = get_metadata_distribution( | ||||
|             metadata_contents, | ||||
|             req.link.filename, | ||||
|             req.req.name, | ||||
|         ) | ||||
|         # (4) Ensure the Name: field from the METADATA file matches the name from the | ||||
|         #     install requirement. | ||||
|         # | ||||
|         #     NB: raw_name will fall back to the name from the install requirement if | ||||
|         #     the Name: field is not present, but it's noted in the raw_name docstring | ||||
|         #     that that should NEVER happen anyway. | ||||
|         if canonicalize_name(metadata_dist.raw_name) != canonicalize_name(req.req.name): | ||||
|             raise MetadataInconsistent( | ||||
|                 req, "Name", req.req.name, metadata_dist.raw_name | ||||
|             ) | ||||
|         return metadata_dist | ||||
| 
 | ||||
|     def _fetch_metadata_using_lazy_wheel( | ||||
|         self, | ||||
|         link: Link, | ||||
|     ) -> Optional[BaseDistribution]: | ||||
|         """Fetch metadata using lazy wheel, if possible.""" | ||||
|         # --use-feature=fast-deps must be provided. | ||||
|         if not self.use_lazy_wheel: | ||||
|             return None | ||||
|         if link.is_file or not link.is_wheel: | ||||
|             logger.debug( | ||||
|                 "Lazy wheel is not used as %r does not point to a remote wheel", | ||||
|                 link, | ||||
|             ) | ||||
|             return None | ||||
| 
 | ||||
|         wheel = Wheel(link.filename) | ||||
|         name = canonicalize_name(wheel.name) | ||||
|         logger.info( | ||||
|             "Obtaining dependency information from %s %s", | ||||
|             name, | ||||
|             wheel.version, | ||||
|         ) | ||||
|         url = link.url.split("#", 1)[0] | ||||
|         try: | ||||
|             return dist_from_wheel_url(name, url, self._session) | ||||
|         except HTTPRangeRequestUnsupported: | ||||
|             logger.debug("%s does not support range requests", url) | ||||
|             return None | ||||
| 
 | ||||
|     def _complete_partial_requirements( | ||||
|         self, | ||||
|         partially_downloaded_reqs: Iterable[InstallRequirement], | ||||
|         parallel_builds: bool = False, | ||||
|     ) -> None: | ||||
|         """Download any requirements which were only fetched by metadata.""" | ||||
|         # Download to a temporary directory. These will be copied over as | ||||
|         # needed for downstream 'download', 'wheel', and 'install' commands. | ||||
|         temp_dir = TempDirectory(kind="unpack", globally_managed=True).path | ||||
| 
 | ||||
|         # Map each link to the requirement that owns it. This allows us to set | ||||
|         # `req.local_file_path` on the appropriate requirement after passing | ||||
|         # all the links at once into BatchDownloader. | ||||
|         links_to_fully_download: Dict[Link, InstallRequirement] = {} | ||||
|         for req in partially_downloaded_reqs: | ||||
|             assert req.link | ||||
|             links_to_fully_download[req.link] = req | ||||
| 
 | ||||
|         batch_download = self._batch_download( | ||||
|             links_to_fully_download.keys(), | ||||
|             temp_dir, | ||||
|         ) | ||||
|         for link, (filepath, _) in batch_download: | ||||
|             logger.debug("Downloading link %s to %s", link, filepath) | ||||
|             req = links_to_fully_download[link] | ||||
|             # Record the downloaded file path so wheel reqs can extract a Distribution | ||||
|             # in .get_dist(). | ||||
|             req.local_file_path = filepath | ||||
|             # Record that the file is downloaded so we don't do it again in | ||||
|             # _prepare_linked_requirement(). | ||||
|             self._downloaded[req.link.url] = filepath | ||||
| 
 | ||||
|             # If this is an sdist, we need to unpack it after downloading, but the | ||||
|             # .source_dir won't be set up until we are in _prepare_linked_requirement(). | ||||
|             # Add the downloaded archive to the install requirement to unpack after | ||||
|             # preparing the source dir. | ||||
|             if not req.is_wheel: | ||||
|                 req.needs_unpacked_archive(Path(filepath)) | ||||
| 
 | ||||
|         # This step is necessary to ensure all lazy wheels are processed | ||||
|         # successfully by the 'download', 'wheel', and 'install' commands. | ||||
|         for req in partially_downloaded_reqs: | ||||
|             self._prepare_linked_requirement(req, parallel_builds) | ||||
| 
 | ||||
|     def prepare_linked_requirement( | ||||
|         self, req: InstallRequirement, parallel_builds: bool = False | ||||
|     ) -> BaseDistribution: | ||||
|         """Prepare a requirement to be obtained from req.link.""" | ||||
|         assert req.link | ||||
|         self._log_preparing_link(req) | ||||
|         with indent_log(): | ||||
|             # Check if the relevant file is already available | ||||
|             # in the download directory | ||||
|             file_path = None | ||||
|             if self.download_dir is not None and req.link.is_wheel: | ||||
|                 hashes = self._get_linked_req_hashes(req) | ||||
|                 file_path = _check_download_dir( | ||||
|                     req.link, | ||||
|                     self.download_dir, | ||||
|                     hashes, | ||||
|                     # When a locally built wheel has been found in cache, we don't warn | ||||
|                     # about re-downloading when the already downloaded wheel hash does | ||||
|                     # not match. This is because the hash must be checked against the | ||||
|                     # original link, not the cached link. It that case the already | ||||
|                     # downloaded file will be removed and re-fetched from cache (which | ||||
|                     # implies a hash check against the cache entry's origin.json). | ||||
|                     warn_on_hash_mismatch=not req.is_wheel_from_cache, | ||||
|                 ) | ||||
| 
 | ||||
|             if file_path is not None: | ||||
|                 # The file is already available, so mark it as downloaded | ||||
|                 self._downloaded[req.link.url] = file_path | ||||
|             else: | ||||
|                 # The file is not available, attempt to fetch only metadata | ||||
|                 metadata_dist = self._fetch_metadata_only(req) | ||||
|                 if metadata_dist is not None: | ||||
|                     req.needs_more_preparation = True | ||||
|                     return metadata_dist | ||||
| 
 | ||||
|             # None of the optimizations worked, fully prepare the requirement | ||||
|             return self._prepare_linked_requirement(req, parallel_builds) | ||||
| 
 | ||||
|     def prepare_linked_requirements_more( | ||||
|         self, reqs: Iterable[InstallRequirement], parallel_builds: bool = False | ||||
|     ) -> None: | ||||
|         """Prepare linked requirements more, if needed.""" | ||||
|         reqs = [req for req in reqs if req.needs_more_preparation] | ||||
|         for req in reqs: | ||||
|             # Determine if any of these requirements were already downloaded. | ||||
|             if self.download_dir is not None and req.link.is_wheel: | ||||
|                 hashes = self._get_linked_req_hashes(req) | ||||
|                 file_path = _check_download_dir(req.link, self.download_dir, hashes) | ||||
|                 if file_path is not None: | ||||
|                     self._downloaded[req.link.url] = file_path | ||||
|                     req.needs_more_preparation = False | ||||
| 
 | ||||
|         # Prepare requirements we found were already downloaded for some | ||||
|         # reason. The other downloads will be completed separately. | ||||
|         partially_downloaded_reqs: List[InstallRequirement] = [] | ||||
|         for req in reqs: | ||||
|             if req.needs_more_preparation: | ||||
|                 partially_downloaded_reqs.append(req) | ||||
|             else: | ||||
|                 self._prepare_linked_requirement(req, parallel_builds) | ||||
| 
 | ||||
|         # TODO: separate this part out from RequirementPreparer when the v1 | ||||
|         # resolver can be removed! | ||||
|         self._complete_partial_requirements( | ||||
|             partially_downloaded_reqs, | ||||
|             parallel_builds=parallel_builds, | ||||
|         ) | ||||
| 
 | ||||
|     def _prepare_linked_requirement( | ||||
|         self, req: InstallRequirement, parallel_builds: bool | ||||
|     ) -> BaseDistribution: | ||||
|         assert req.link | ||||
|         link = req.link | ||||
| 
 | ||||
|         hashes = self._get_linked_req_hashes(req) | ||||
| 
 | ||||
|         if hashes and req.is_wheel_from_cache: | ||||
|             assert req.download_info is not None | ||||
|             assert link.is_wheel | ||||
|             assert link.is_file | ||||
|             # We need to verify hashes, and we have found the requirement in the cache | ||||
|             # of locally built wheels. | ||||
|             if ( | ||||
|                 isinstance(req.download_info.info, ArchiveInfo) | ||||
|                 and req.download_info.info.hashes | ||||
|                 and hashes.has_one_of(req.download_info.info.hashes) | ||||
|             ): | ||||
|                 # At this point we know the requirement was built from a hashable source | ||||
|                 # artifact, and we verified that the cache entry's hash of the original | ||||
|                 # artifact matches one of the hashes we expect. We don't verify hashes | ||||
|                 # against the cached wheel, because the wheel is not the original. | ||||
|                 hashes = None | ||||
|             else: | ||||
|                 logger.warning( | ||||
|                     "The hashes of the source archive found in cache entry " | ||||
|                     "don't match, ignoring cached built wheel " | ||||
|                     "and re-downloading source." | ||||
|                 ) | ||||
|                 req.link = req.cached_wheel_source_link | ||||
|                 link = req.link | ||||
| 
 | ||||
|         self._ensure_link_req_src_dir(req, parallel_builds) | ||||
| 
 | ||||
|         if link.is_existing_dir(): | ||||
|             local_file = None | ||||
|         elif link.url not in self._downloaded: | ||||
|             try: | ||||
|                 local_file = unpack_url( | ||||
|                     link, | ||||
|                     req.source_dir, | ||||
|                     self._download, | ||||
|                     self.verbosity, | ||||
|                     self.download_dir, | ||||
|                     hashes, | ||||
|                 ) | ||||
|             except NetworkConnectionError as exc: | ||||
|                 raise InstallationError( | ||||
|                     f"Could not install requirement {req} because of HTTP " | ||||
|                     f"error {exc} for URL {link}" | ||||
|                 ) | ||||
|         else: | ||||
|             file_path = self._downloaded[link.url] | ||||
|             if hashes: | ||||
|                 hashes.check_against_path(file_path) | ||||
|             local_file = File(file_path, content_type=None) | ||||
| 
 | ||||
|         # If download_info is set, we got it from the wheel cache. | ||||
|         if req.download_info is None: | ||||
|             # Editables don't go through this function (see | ||||
|             # prepare_editable_requirement). | ||||
|             assert not req.editable | ||||
|             req.download_info = direct_url_from_link(link, req.source_dir) | ||||
|             # Make sure we have a hash in download_info. If we got it as part of the | ||||
|             # URL, it will have been verified and we can rely on it. Otherwise we | ||||
|             # compute it from the downloaded file. | ||||
|             # FIXME: https://github.com/pypa/pip/issues/11943 | ||||
|             if ( | ||||
|                 isinstance(req.download_info.info, ArchiveInfo) | ||||
|                 and not req.download_info.info.hashes | ||||
|                 and local_file | ||||
|             ): | ||||
|                 hash = hash_file(local_file.path)[0].hexdigest() | ||||
|                 # We populate info.hash for backward compatibility. | ||||
|                 # This will automatically populate info.hashes. | ||||
|                 req.download_info.info.hash = f"sha256={hash}" | ||||
| 
 | ||||
|         # For use in later processing, | ||||
|         # preserve the file path on the requirement. | ||||
|         if local_file: | ||||
|             req.local_file_path = local_file.path | ||||
| 
 | ||||
|         dist = _get_prepared_distribution( | ||||
|             req, | ||||
|             self.build_tracker, | ||||
|             self.finder, | ||||
|             self.build_isolation, | ||||
|             self.check_build_deps, | ||||
|         ) | ||||
|         return dist | ||||
| 
 | ||||
|     def save_linked_requirement(self, req: InstallRequirement) -> None: | ||||
|         assert self.download_dir is not None | ||||
|         assert req.link is not None | ||||
|         link = req.link | ||||
|         if link.is_vcs or (link.is_existing_dir() and req.editable): | ||||
|             # Make a .zip of the source_dir we already created. | ||||
|             req.archive(self.download_dir) | ||||
|             return | ||||
| 
 | ||||
|         if link.is_existing_dir(): | ||||
|             logger.debug( | ||||
|                 "Not copying link to destination directory " | ||||
|                 "since it is a directory: %s", | ||||
|                 link, | ||||
|             ) | ||||
|             return | ||||
|         if req.local_file_path is None: | ||||
|             # No distribution was downloaded for this requirement. | ||||
|             return | ||||
| 
 | ||||
|         download_location = os.path.join(self.download_dir, link.filename) | ||||
|         if not os.path.exists(download_location): | ||||
|             shutil.copy(req.local_file_path, download_location) | ||||
|             download_path = display_path(download_location) | ||||
|             logger.info("Saved %s", download_path) | ||||
| 
 | ||||
|     def prepare_editable_requirement( | ||||
|         self, | ||||
|         req: InstallRequirement, | ||||
|     ) -> BaseDistribution: | ||||
|         """Prepare an editable requirement.""" | ||||
|         assert req.editable, "cannot prepare a non-editable req as editable" | ||||
| 
 | ||||
|         logger.info("Obtaining %s", req) | ||||
| 
 | ||||
|         with indent_log(): | ||||
|             if self.require_hashes: | ||||
|                 raise InstallationError( | ||||
|                     f"The editable requirement {req} cannot be installed when " | ||||
|                     "requiring hashes, because there is no single file to " | ||||
|                     "hash." | ||||
|                 ) | ||||
|             req.ensure_has_source_dir(self.src_dir) | ||||
|             req.update_editable() | ||||
|             assert req.source_dir | ||||
|             req.download_info = direct_url_for_editable(req.unpacked_source_directory) | ||||
| 
 | ||||
|             dist = _get_prepared_distribution( | ||||
|                 req, | ||||
|                 self.build_tracker, | ||||
|                 self.finder, | ||||
|                 self.build_isolation, | ||||
|                 self.check_build_deps, | ||||
|             ) | ||||
| 
 | ||||
|             req.check_if_exists(self.use_user_site) | ||||
| 
 | ||||
|         return dist | ||||
| 
 | ||||
|     def prepare_installed_requirement( | ||||
|         self, | ||||
|         req: InstallRequirement, | ||||
|         skip_reason: str, | ||||
|     ) -> BaseDistribution: | ||||
|         """Prepare an already-installed requirement.""" | ||||
|         assert req.satisfied_by, "req should have been satisfied but isn't" | ||||
|         assert skip_reason is not None, ( | ||||
|             "did not get skip reason skipped but req.satisfied_by " | ||||
|             f"is set to {req.satisfied_by}" | ||||
|         ) | ||||
|         logger.info( | ||||
|             "Requirement %s: %s (%s)", skip_reason, req, req.satisfied_by.version | ||||
|         ) | ||||
|         with indent_log(): | ||||
|             if self.require_hashes: | ||||
|                 logger.debug( | ||||
|                     "Since it is already installed, we are trusting this " | ||||
|                     "package without checking its hash. To ensure a " | ||||
|                     "completely repeatable environment, install into an " | ||||
|                     "empty virtualenv." | ||||
|                 ) | ||||
|             return InstalledDistribution(req).get_metadata_distribution() | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Tykayn
						Tykayn