Voice et bot modif

This commit is contained in:
pi 2026-06-16 17:09:34 +00:00
parent 189d56026b
commit 7333a22bcd
10774 changed files with 634644 additions and 933308 deletions

View file

@ -1,12 +1,11 @@
from __future__ import annotations
import contextlib
import hashlib
import logging
import os
from collections.abc import Generator
from types import TracebackType
from typing import Dict, Generator, Optional, Set, Type, Union
from pip._internal.models.link import Link
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.temp_dir import TempDirectory
@ -19,7 +18,7 @@ def update_env_context_manager(**changes: str) -> Generator[None, None, None]:
# Save values from the target and change them.
non_existent_marker = object()
saved_values: dict[str, object | str] = {}
saved_values: Dict[str, Union[object, str]] = {}
for name, new_value in changes.items():
try:
saved_values[name] = target[name]
@ -40,7 +39,7 @@ def update_env_context_manager(**changes: str) -> Generator[None, None, None]:
@contextlib.contextmanager
def get_build_tracker() -> Generator[BuildTracker, None, None]:
def get_build_tracker() -> Generator["BuildTracker", None, None]:
root = os.environ.get("PIP_BUILD_TRACKER")
with contextlib.ExitStack() as ctx:
if root is None:
@ -67,18 +66,18 @@ class BuildTracker:
def __init__(self, root: str) -> None:
self._root = root
self._entries: dict[TrackerId, InstallRequirement] = {}
self._entries: Dict[TrackerId, InstallRequirement] = {}
logger.debug("Created build tracker: %s", self._root)
def __enter__(self) -> BuildTracker:
def __enter__(self) -> "BuildTracker":
logger.debug("Entered build tracker: %s", self._root)
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
self.cleanup()
@ -100,7 +99,7 @@ class BuildTracker:
except FileNotFoundError:
pass
else:
message = f"{req.link} is already being built: {contents}"
message = "{} is already being built: {}".format(req.link, contents)
raise LookupError(message)
# If we're here, req should really not be building already.

View file

@ -1,4 +1,5 @@
"""Metadata generation logic for source distributions."""
"""Metadata generation logic for source distributions.
"""
import os

View file

@ -1,4 +1,5 @@
"""Metadata generation logic for source distributions."""
"""Metadata generation logic for source distributions.
"""
import os
@ -37,5 +38,4 @@ def generate_editable_metadata(
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)

View file

@ -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(
"More than one .egg-info directory found in {}".format(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)

View file

@ -1,7 +1,6 @@
from __future__ import annotations
import logging
import os
from typing import Optional
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
@ -14,25 +13,25 @@ def build_wheel_pep517(
name: str,
backend: BuildBackendHookCaller,
metadata_directory: str,
wheel_directory: str,
) -> str | None:
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", wheel_directory)
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(
wheel_directory=wheel_directory,
tempd,
metadata_directory=metadata_directory,
)
except Exception:
logger.error("Failed building wheel for %s", name)
return None
return os.path.join(wheel_directory, wheel_name)
return os.path.join(tempd, wheel_name)

View file

@ -1,7 +1,6 @@
from __future__ import annotations
import logging
import os
from typing import Optional
from pip._vendor.pyproject_hooks import BuildBackendHookCaller, HookMissing
@ -14,15 +13,15 @@ def build_wheel_editable(
name: str,
backend: BuildBackendHookCaller,
metadata_directory: str,
wheel_directory: str,
) -> str | None:
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", wheel_directory)
logger.debug("Destination directory: %s", tempd)
runner = runner_with_spinner_message(
f"Building editable for {name} (pyproject.toml)"
@ -30,7 +29,7 @@ def build_wheel_editable(
with backend.subprocess_runner(runner):
try:
wheel_name = backend.build_editable(
wheel_directory=wheel_directory,
tempd,
metadata_directory=metadata_directory,
)
except HookMissing as e:
@ -44,4 +43,4 @@ def build_wheel_editable(
except Exception:
logger.error("Failed building editable for %s", name)
return None
return os.path.join(wheel_directory, wheel_name)
return os.path.join(tempd, wheel_name)

View file

@ -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 = ("Legacy build of wheel for {!r} created no files.\n").format(name)
msg += format_command_result(command_args, command_output)
logger.warning(msg)
return None
if len(names) > 1:
msg = (
"Legacy build of wheel for {!r} created more than one file.\n"
"Filenames (choosing first): {}\n"
).format(name, names)
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

View file

@ -1,47 +1,40 @@
"""Validation of dependencies of packages"""
from __future__ import annotations
"""Validation of dependencies of packages
"""
import logging
from collections.abc import Generator, Iterable
from contextlib import suppress
from email.parser import Parser
from functools import reduce
from typing import (
Callable,
NamedTuple,
)
from typing import Callable, Dict, 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.specifiers import LegacySpecifier
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import LegacyVersion
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.metadata.base import DistributionVersion
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.deprecation import deprecated
logger = logging.getLogger(__name__)
class PackageDetails(NamedTuple):
version: Version
dependencies: list[Requirement]
version: DistributionVersion
dependencies: List[Requirement]
# Shorthands
PackageSet = dict[NormalizedName, PackageDetails]
Missing = tuple[NormalizedName, Requirement]
Conflicting = tuple[NormalizedName, Version, Requirement]
PackageSet = Dict[NormalizedName, PackageDetails]
Missing = Tuple[NormalizedName, Requirement]
Conflicting = Tuple[NormalizedName, DistributionVersion, Requirement]
MissingDict = dict[NormalizedName, list[Missing]]
ConflictingDict = dict[NormalizedName, list[Conflicting]]
CheckResult = tuple[MissingDict, ConflictingDict]
ConflictDetails = tuple[PackageSet, CheckResult]
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]:
def create_package_set_from_installed() -> Tuple[PackageSet, bool]:
"""Converts a list of distributions into a PackageSet."""
package_set = {}
problems = False
@ -53,13 +46,13 @@ def create_package_set_from_installed() -> tuple[PackageSet, bool]:
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)
logger.warning("Error parsing requirements for %s: %s", name, e)
problems = True
return package_set, problems
def check_package_set(
package_set: PackageSet, should_ignore: Callable[[str], bool] | None = None
package_set: PackageSet, should_ignore: Optional[Callable[[str], bool]] = None
) -> CheckResult:
"""Check if a package set is consistent
@ -67,13 +60,15 @@ def check_package_set(
package name and returns a boolean.
"""
warn_legacy_versions_and_specifiers(package_set)
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()
missing_deps: Set[Missing] = set()
conflicting_deps: Set[Conflicting] = set()
if should_ignore and should_ignore(package_name):
continue
@ -103,7 +98,7 @@ def check_package_set(
return missing, conflicting
def check_install_conflicts(to_install: list[InstallRequirement]) -> ConflictDetails:
def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDetails:
"""For checking if the dependency graph would be consistent after \
installing given requirements
"""
@ -123,25 +118,9 @@ def check_install_conflicts(to_install: list[InstallRequirement]) -> ConflictDet
)
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]:
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()
@ -159,8 +138,8 @@ def _simulate_installation_of(
def _create_whitelist(
would_be_installed: set[NormalizedName], package_set: PackageSet
) -> set[NormalizedName]:
would_be_installed: Set[NormalizedName], package_set: PackageSet
) -> Set[NormalizedName]:
packages_affected = set(would_be_installed)
for package_name in package_set:
@ -173,3 +152,36 @@ def _create_whitelist(
break
return packages_affected
def warn_legacy_versions_and_specifiers(package_set: PackageSet) -> None:
for project_name, package_details in package_set.items():
if isinstance(package_details.version, LegacyVersion):
deprecated(
reason=(
f"{project_name} {package_details.version} "
f"has a non-standard version number."
),
replacement=(
f"to upgrade to a newer version of {project_name} "
f"or contact the author to suggest that they "
f"release a version with a conforming version number"
),
issue=12063,
gone_in="24.1",
)
for dep in package_details.dependencies:
if any(isinstance(spec, LegacySpecifier) for spec in dep.specifier):
deprecated(
reason=(
f"{project_name} {package_details.version} "
f"has a non-standard dependency specifier {dep}."
),
replacement=(
f"to upgrade to a newer version of {project_name} "
f"or contact the author to suggest that they "
f"release a version with a conforming dependency specifiers"
),
issue=12063,
gone_in="24.1",
)

View file

@ -1,14 +1,10 @@
from __future__ import annotations
import collections
import logging
import os
from collections.abc import Container, Generator, Iterable
from dataclasses import dataclass, field
from typing import NamedTuple
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._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version
from pip._internal.exceptions import BadCommand, InstallationError
from pip._internal.metadata import BaseDistribution, get_environment
@ -24,19 +20,19 @@ logger = logging.getLogger(__name__)
class _EditableInfo(NamedTuple):
requirement: str
comments: list[str]
comments: List[str]
def freeze(
requirement: list[str] | None = None,
requirement: Optional[List[str]] = None,
local_only: bool = False,
user_only: bool = False,
paths: list[str] | None = None,
paths: Optional[List[str]] = None,
isolated: bool = False,
exclude_editable: bool = False,
skip: Container[str] = (),
) -> Generator[str, None, None]:
installations: dict[str, FrozenRequirement] = {}
installations: Dict[str, FrozenRequirement] = {}
dists = get_environment(paths).iter_installed_distributions(
local_only=local_only,
@ -54,10 +50,10 @@ def freeze(
# 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()
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)
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:
@ -86,7 +82,7 @@ def freeze(
yield line
continue
if line.startswith(("-e", "--editable")):
if line.startswith("-e") or line.startswith("--editable"):
if line.startswith("-e"):
line = line[2:].strip()
else:
@ -149,13 +145,10 @@ def freeze(
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:
dist_version = dist.version
if isinstance(dist_version, Version):
return f"{dist.raw_name}=={dist_version}"
return f"{dist.raw_name}==={dist_version}"
def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
@ -224,19 +217,22 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
)
@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)
def __init__(
self,
name: str,
req: str,
editable: bool,
comments: Iterable[str] = (),
) -> None:
self.name = name
self.canonical_name = canonicalize_name(name)
self.req = req
self.editable = editable
self.comments = comments
@classmethod
def from_dist(cls, dist: BaseDistribution) -> FrozenRequirement:
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
editable = dist.editable
if editable:
req, comments = _get_editable_info(dist)

View file

@ -1 +1,2 @@
"""For modules related to installing packages."""
"""For modules related to installing packages.
"""

View file

@ -0,0 +1,46 @@
"""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,
)

View file

@ -1,6 +1,5 @@
"""Support for installing and building the "wheel" binary package format."""
from __future__ import annotations
"""Support for installing and building the "wheel" binary package format.
"""
import collections
import compileall
@ -12,19 +11,26 @@ import os.path
import re
import shutil
import sys
import textwrap
import warnings
from base64 import urlsafe_b64encode
from collections.abc import Generator, Iterable, Iterator, Sequence
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,
Protocol,
Optional,
Sequence,
Set,
Tuple,
Union,
cast,
)
@ -44,7 +50,7 @@ from pip._internal.metadata import (
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.misc import captured_stdout, ensure_dir, hash_file, partition
from pip._internal.utils.unpacking import (
current_umask,
is_within_directory,
@ -53,30 +59,32 @@ from pip._internal.utils.unpacking import (
)
from pip._internal.utils.wheel import parse_wheel
if TYPE_CHECKING:
from typing import Protocol
class File(Protocol):
src_record_path: RecordPath
dest_path: str
changed: bool
class File(Protocol):
src_record_path: "RecordPath"
dest_path: str
changed: bool
def save(self) -> None:
pass
def save(self) -> None:
pass
logger = logging.getLogger(__name__)
RecordPath = NewType("RecordPath", str)
InstalledCSVRow = tuple[RecordPath, str, Union[int, str]]
InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]]
def rehash(path: str, blocksize: int = 1 << 20) -> tuple[str, 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]:
def csv_io_kwargs(mode: str) -> Dict[str, Any]:
"""Return keyword arguments to properly open a CSV file
in the given mode.
"""
@ -107,7 +115,7 @@ 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]]:
def get_entrypoints(dist: BaseDistribution) -> Tuple[Dict[str, str], Dict[str, str]]:
console_scripts = {}
gui_scripts = {}
for entry_point in dist.iter_entry_points():
@ -118,7 +126,7 @@ def get_entrypoints(dist: BaseDistribution) -> tuple[dict[str, str], dict[str, s
return console_scripts, gui_scripts
def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> str | None:
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.
@ -127,7 +135,7 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> str | None:
return None
# Group scripts by the path they were installed in
grouped_by_dir: dict[str, set[str]] = collections.defaultdict(set)
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)
@ -143,7 +151,7 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> str | None:
not_warn_dirs.append(
os.path.normcase(os.path.normpath(os.path.dirname(sys.executable)))
)
warn_for: dict[str, set[str]] = {
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
@ -154,7 +162,7 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> str | None:
# Format a message
msg_lines = []
for parent_dir, dir_scripts in warn_for.items():
sorted_scripts: list[str] = sorted(dir_scripts)
sorted_scripts: List[str] = sorted(dir_scripts)
if len(sorted_scripts) == 1:
start_text = f"script {sorted_scripts[0]} is"
else:
@ -192,7 +200,7 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> str | None:
def _normalized_outrows(
outrows: Iterable[InstalledCSVRow],
) -> list[tuple[str, str, str]]:
) -> 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
@ -231,17 +239,17 @@ def _fs_to_record_path(path: str, lib_dir: str) -> RecordPath:
def get_csv_rows_for_installed(
old_csv_rows: list[list[str]],
installed: dict[RecordPath, RecordPath],
changed: set[RecordPath],
generated: list[str],
old_csv_rows: List[List[str]],
installed: Dict[RecordPath, RecordPath],
changed: Set[RecordPath],
generated: List[str],
lib_dir: str,
) -> list[InstalledCSVRow]:
) -> List[InstalledCSVRow]:
"""
:param installed: A map from archive RECORD path to installation RECORD
path.
"""
installed_rows: list[InstalledCSVRow] = []
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)
@ -262,7 +270,7 @@ def get_csv_rows_for_installed(
]
def get_console_script_specs(console: dict[str, str]) -> list[str]:
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.
@ -280,15 +288,17 @@ def get_console_script_specs(console: dict[str, str]) -> list[str]:
# 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
# such versioned entry points, but that won't happen till Metadata 2.0 is
# available.
# In the meantime, 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
# we need to use universal wheels. So, as a stopgap until Metadata 2.0, we
# override the versioned entry points in the wheel and generate the
# correct ones.
# correct ones. This code is purely a short-term measure until Metadata 2.0
# is available.
#
# 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
@ -350,6 +360,12 @@ class ZipBackedFile:
return self._zip_file.getinfo(self.src_record_path)
def save(self) -> None:
# 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(self.dest_path)
ensure_dir(parent_dir)
# 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
@ -363,20 +379,16 @@ class ZipBackedFile:
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)
with self._zip_file.open(zipinfo) as f:
with open(self.dest_path, "wb") as dest:
shutil.copyfileobj(f, dest)
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:
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
@ -391,7 +403,7 @@ class MissingCallableSuffix(InstallationError):
def __init__(self, entry_point: str) -> None:
super().__init__(
f"Invalid script entry point: {entry_point} - A callable "
"suffix is required. See https://packaging.python.org/"
"suffix is required. Cf https://packaging.python.org/"
"specifications/entry-points/#use-for-scripts for more "
"information."
)
@ -404,34 +416,21 @@ def _raise_for_invalid_entrypoint(specification: str) -> None:
class PipScriptMaker(ScriptMaker):
# Override distlib's default script template with one that
# doesn't import `re` module, allowing scripts to load faster.
script_template = textwrap.dedent(
"""\
import sys
from %(module)s import %(import_name)s
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(%(func)s())
"""
)
def make(
self, specification: str, options: dict[str, Any] | None = None
) -> list[str]:
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
def _install_wheel(
name: str,
wheel_zip: ZipFile,
wheel_path: str,
scheme: Scheme,
pycompile: bool = True,
warn_script_location: bool = True,
direct_url: DirectUrl | None = None,
direct_url: Optional[DirectUrl] = None,
requested: bool = False,
) -> None:
"""Install a wheel.
@ -460,9 +459,9 @@ def _install_wheel( # noqa: C901, PLR0915 function is too long
# 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] = []
installed: Dict[RecordPath, RecordPath] = {}
changed: Set[RecordPath] = set()
generated: List[str] = []
def record_installed(
srcfile: RecordPath, destfile: str, modified: bool = False
@ -488,8 +487,8 @@ def _install_wheel( # noqa: C901, PLR0915 function is too long
def root_scheme_file_maker(
zip_file: ZipFile, dest: str
) -> Callable[[RecordPath], File]:
def make_root_scheme_file(record_path: RecordPath) -> File:
) -> 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)
@ -499,18 +498,18 @@ def _install_wheel( # noqa: C901, PLR0915 function is too long
def data_scheme_file_maker(
zip_file: ZipFile, scheme: Scheme
) -> Callable[[RecordPath], File]:
) -> Callable[[RecordPath], "File"]:
scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS}
def make_data_scheme_file(record_path: RecordPath) -> File:
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>'."
)
"Unexpected file in {}: {!r}. .data directory contents"
" should be named like: '<scheme key>/<path>'."
).format(wheel_path, record_path)
raise InstallationError(message)
try:
@ -518,11 +517,10 @@ def _install_wheel( # noqa: C901, PLR0915 function is too long
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})"
)
"Unknown scheme key used in {}: {} (for file {!r}). .data"
" directory contents should be in subdirectories named"
" with a valid scheme key ({})"
).format(wheel_path, scheme_key, record_path, valid_scheme_keys)
raise InstallationError(message)
dest_path = os.path.join(scheme_path, dest_subpath)
@ -534,7 +532,7 @@ def _install_wheel( # noqa: C901, PLR0915 function is too long
def is_data_scheme_path(path: RecordPath) -> bool:
return path.split("/", 1)[0].endswith(".data")
paths = cast(list[RecordPath], wheel_zip.namelist())
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)
@ -560,7 +558,7 @@ def _install_wheel( # noqa: C901, PLR0915 function is too long
)
console, gui = get_entrypoints(distribution)
def is_entrypoint_wrapper(file: File) -> bool:
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
@ -583,15 +581,7 @@ def _install_wheel( # noqa: C901, PLR0915 function is too long
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)
@ -614,9 +604,7 @@ def _install_wheel( # noqa: C901, PLR0915 function is too long
# Compile all of the pyc files for the installed files
if pycompile:
with contextlib.redirect_stdout(
StreamWrapper.from_stream(sys.stdout)
) as stdout:
with captured_stdout() as stdout:
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
for path in pyc_source_file_paths():
@ -729,7 +717,7 @@ def install_wheel(
req_description: str,
pycompile: bool = True,
warn_script_location: bool = True,
direct_url: DirectUrl | None = None,
direct_url: Optional[DirectUrl] = None,
requested: bool = False,
) -> None:
with ZipFile(wheel_path, allowZip64=True) as z:

View file

@ -1,20 +1,17 @@
"""Prepares a distribution for installation"""
"""Prepares a distribution for installation
"""
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import mimetypes
import os
import shutil
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Dict, Iterable, List, Optional
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.build_env import BuildEnvironmentInstaller
from pip._internal.distributions import make_distribution_for_install_requirement
from pip._internal.distributions.installed import InstalledDistribution
from pip._internal.exceptions import (
@ -31,7 +28,7 @@ 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 Downloader
from pip._internal.network.download import BatchDownloader, Downloader
from pip._internal.network.lazy_wheel import (
HTTPRangeRequestUnsupported,
dist_from_wheel_url,
@ -56,16 +53,13 @@ from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.unpacking import unpack_file
from pip._internal.vcs import vcs
if TYPE_CHECKING:
from pip._internal.cli.progress_bars import BarType
logger = getLogger(__name__)
def _get_prepared_distribution(
req: InstallRequirement,
build_tracker: BuildTracker,
build_env_installer: BuildEnvironmentInstaller,
finder: PackageFinder,
build_isolation: bool,
check_build_deps: bool,
) -> BaseDistribution:
@ -75,7 +69,7 @@ def _get_prepared_distribution(
if tracker_id is not None:
with build_tracker.track(req, tracker_id):
abstract_dist.prepare_distribution_metadata(
build_env_installer, build_isolation, check_build_deps
finder, build_isolation, check_build_deps
)
return abstract_dist.get_metadata_distribution()
@ -86,26 +80,20 @@ def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:
vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity)
@dataclass
class File:
path: str
content_type: str | None = None
def __post_init__(self) -> None:
if self.content_type is None:
# Try to guess the file's MIME type. If the system MIME tables
# can't be loaded, give up.
try:
self.content_type = mimetypes.guess_type(self.path)[0]
except OSError:
pass
def __init__(self, path: str, content_type: Optional[str]) -> None:
self.path = path
if content_type is None:
self.content_type = mimetypes.guess_type(path)[0]
else:
self.content_type = content_type
def get_http_url(
link: Link,
download: Downloader,
download_dir: str | None = None,
hashes: Hashes | None = None,
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?
@ -126,7 +114,7 @@ def get_http_url(
def get_file_url(
link: Link, download_dir: str | None = None, hashes: Hashes | None = None
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?
@ -154,9 +142,9 @@ def unpack_url(
location: str,
download: Downloader,
verbosity: int,
download_dir: str | None = None,
hashes: Hashes | None = None,
) -> File | None:
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,
@ -195,9 +183,9 @@ def unpack_url(
def _check_download_dir(
link: Link,
download_dir: str,
hashes: Hashes | None,
hashes: Optional[Hashes],
warn_on_hash_mismatch: bool = True,
) -> str | None:
) -> Optional[str]:
"""Check download_dir for previously downloaded file with correct hash
If a correct file is found return its path else None
"""
@ -225,25 +213,22 @@ def _check_download_dir(
class RequirementPreparer:
"""Prepares a Requirement"""
def __init__( # noqa: PLR0913 (too many parameters)
def __init__(
self,
*,
build_dir: str,
download_dir: str | None,
download_dir: Optional[str],
src_dir: str,
build_isolation: bool,
build_isolation_installer: BuildEnvironmentInstaller,
check_build_deps: bool,
build_tracker: BuildTracker,
session: PipSession,
progress_bar: BarType,
progress_bar: str,
finder: PackageFinder,
require_hashes: bool,
use_user_site: bool,
lazy_wheel: bool,
verbosity: int,
legacy_resolver: bool,
resume_retries: int,
) -> None:
super().__init__()
@ -251,7 +236,8 @@ class RequirementPreparer:
self.build_dir = build_dir
self.build_tracker = build_tracker
self._session = session
self._download = Downloader(session, progress_bar, resume_retries)
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
@ -260,7 +246,6 @@ class RequirementPreparer:
# Is build isolation allowed?
self.build_isolation = build_isolation
self.build_env_installer = build_isolation_installer
# Should check build dependencies?
self.check_build_deps = check_build_deps
@ -281,7 +266,7 @@ class RequirementPreparer:
self.legacy_resolver = legacy_resolver
# Memoized downloaded files, as mapping of url: path.
self._downloaded: dict[str, str] = {}
self._downloaded: Dict[str, str] = {}
# Previous "header" printed for a link-based InstallRequirement
self._previous_requirement_header = ("", "")
@ -299,7 +284,7 @@ class RequirementPreparer:
# would already be included if we used req directly)
if req.req and req.comes_from:
if isinstance(req.comes_from, str):
comes_from: str | None = req.comes_from
comes_from: Optional[str] = req.comes_from
else:
comes_from = req.comes_from.from_path()
if comes_from:
@ -371,7 +356,7 @@ class RequirementPreparer:
def _fetch_metadata_only(
self,
req: InstallRequirement,
) -> BaseDistribution | None:
) -> Optional[BaseDistribution]:
if self.legacy_resolver:
logger.debug(
"Metadata-only fetching is not used in the legacy resolver",
@ -390,7 +375,7 @@ class RequirementPreparer:
def _fetch_metadata_using_link_data_attr(
self,
req: InstallRequirement,
) -> BaseDistribution | None:
) -> 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()
@ -431,7 +416,7 @@ class RequirementPreparer:
def _fetch_metadata_using_lazy_wheel(
self,
link: Link,
) -> BaseDistribution | None:
) -> Optional[BaseDistribution]:
"""Fetch metadata using lazy wheel, if possible."""
# --use-feature=fast-deps must be provided.
if not self.use_lazy_wheel:
@ -444,7 +429,7 @@ class RequirementPreparer:
return None
wheel = Wheel(link.filename)
name = wheel.name
name = canonicalize_name(wheel.name)
logger.info(
"Obtaining dependency information from %s %s",
name,
@ -470,12 +455,15 @@ class RequirementPreparer:
# 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] = {}
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._download.batch(links_to_fully_download.keys(), temp_dir)
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]
@ -531,12 +519,6 @@ class RequirementPreparer:
metadata_dist = self._fetch_metadata_only(req)
if metadata_dist is not None:
req.needs_more_preparation = True
req.set_dist(metadata_dist)
# Ensure download_info is available even in dry-run mode
if req.download_info is None:
req.download_info = direct_url_from_link(
req.link, req.source_dir
)
return metadata_dist
# None of the optimizations worked, fully prepare the requirement
@ -558,7 +540,7 @@ class RequirementPreparer:
# Prepare requirements we found were already downloaded for some
# reason. The other downloads will be completed separately.
partially_downloaded_reqs: list[InstallRequirement] = []
partially_downloaded_reqs: List[InstallRequirement] = []
for req in reqs:
if req.needs_more_preparation:
partially_downloaded_reqs.append(req)
@ -658,7 +640,7 @@ class RequirementPreparer:
dist = _get_prepared_distribution(
req,
self.build_tracker,
self.build_env_installer,
self.finder,
self.build_isolation,
self.check_build_deps,
)
@ -714,7 +696,7 @@ class RequirementPreparer:
dist = _get_prepared_distribution(
req,
self.build_tracker,
self.build_env_installer,
self.finder,
self.build_isolation,
self.check_build_deps,
)