This commit is contained in:
2025-01-26 19:24:23 -08:00
parent 32cd60e92b
commit d1dde0dbc6
4155 changed files with 29170 additions and 216373 deletions

View File

@@ -1,6 +1,6 @@
from typing import List, Optional
__version__ = "24.3.1"
__version__ = "25.0"
def main(args: Optional[List[str]] = None) -> int:

View File

@@ -246,6 +246,8 @@ class BuildEnvironment:
# target from config file or env var should be ignored
"--target",
"",
"--cert",
finder.custom_cert or where(),
]
if logger.getEffectiveLevel() <= logging.DEBUG:
args.append("-vv")
@@ -270,21 +272,23 @@ class BuildEnvironment:
for link in finder.find_links:
args.extend(["--find-links", link])
if finder.proxy:
args.extend(["--proxy", finder.proxy])
for host in finder.trusted_hosts:
args.extend(["--trusted-host", host])
if finder.client_cert:
args.extend(["--client-cert", finder.client_cert])
if finder.allow_all_prereleases:
args.append("--pre")
if finder.prefer_binary:
args.append("--prefer-binary")
args.append("--")
args.extend(requirements)
extra_environ = {"_PIP_STANDALONE_CERT": where()}
with open_spinner(f"Installing {kind}") as spinner:
call_subprocess(
args,
command_desc=f"pip subprocess to install {kind}",
spinner=spinner,
extra_environ=extra_environ,
)

View File

@@ -29,6 +29,7 @@ from pip._internal.exceptions import (
NetworkConnectionError,
PreviousBuildDirError,
)
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.filesystem import check_path_owner
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
from pip._internal.utils.misc import get_prog, normalize_path
@@ -228,4 +229,12 @@ class Command(CommandContextMixIn):
)
options.cache_dir = None
if options.no_python_version_warning:
deprecated(
reason="--no-python-version-warning is deprecated.",
replacement="to remove the flag as it's a no-op",
gone_in="25.1",
issue=13154,
)
return self._run_wrapper(level_number, options, args)

View File

@@ -260,8 +260,8 @@ keyring_provider: Callable[..., Option] = partial(
default="auto",
help=(
"Enable the credential lookup via the keyring library if user input is allowed."
" Specify which mechanism to use [disabled, import, subprocess]."
" (default: disabled)"
" Specify which mechanism to use [auto, disabled, import, subprocess]."
" (default: %default)"
),
)

View File

@@ -123,6 +123,7 @@ class SessionCommandMixin(CommandContextMixIn):
"https": options.proxy,
}
session.trust_env = False
session.pip_proxy = options.proxy
# Determine if we can prompt the user for authentication or not
session.auth.prompting = not options.no_input

View File

@@ -63,7 +63,7 @@ def _raw_progress_bar(
size: Optional[int],
) -> Generator[bytes, None, None]:
def write_progress(current: int, total: int) -> None:
sys.stdout.write("Progress %d of %d\n" % (current, total))
sys.stdout.write(f"Progress {current} of {total}\n")
sys.stdout.flush()
current = 0

View File

@@ -8,6 +8,7 @@ from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.exceptions import CommandError, PipError
from pip._internal.utils import filesystem
from pip._internal.utils.logging import getLogger
from pip._internal.utils.misc import format_size
logger = getLogger(__name__)
@@ -180,10 +181,12 @@ class CacheCommand(Command):
if not files:
logger.warning(no_matching_msg)
bytes_removed = 0
for filename in files:
bytes_removed += os.stat(filename).st_size
os.unlink(filename)
logger.verbose("Removed %s", filename)
logger.info("Files removed: %s", len(files))
logger.info("Files removed: %s (%s)", len(files), format_size(bytes_removed))
def purge_cache(self, options: Values, args: List[Any]) -> None:
if args:

View File

@@ -10,6 +10,13 @@ from typing import List, Optional
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.rich import print_json
# Eagerly import self_outdated_check to avoid crashes. Otherwise,
# this module would be imported *after* pip was replaced, resulting
# in crashes if the new self_outdated_check module was incompatible
# with the rest of pip that's already imported, or allowing a
# wheel to execute arbitrary code on install by replacing
# self_outdated_check.
import pip._internal.self_outdated_check # noqa: F401
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.cmdoptions import make_target_python
@@ -408,12 +415,6 @@ class InstallCommand(RequirementCommand):
# If we're not replacing an already installed pip,
# we're not modifying it.
modifying_pip = pip_req.satisfied_by is None
if modifying_pip:
# Eagerly import this module to avoid crashes. Otherwise, this
# module would be imported *after* pip was replaced, resulting in
# crashes if the new self_outdated_check module was incompatible
# with the rest of pip that's already imported.
import pip._internal.self_outdated_check # noqa: F401
protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
reqs_to_build = [
@@ -432,7 +433,7 @@ class InstallCommand(RequirementCommand):
if build_failures:
raise InstallationError(
"ERROR: Failed to build installable wheels for some "
"Failed to build installable wheels for some "
"pyproject.toml based projects ({})".format(
", ".join(r.name for r in build_failures) # type: ignore
)

View File

@@ -66,6 +66,7 @@ class _PackageInfo(NamedTuple):
author: str
author_email: str
license: str
license_expression: str
entry_points: List[str]
files: Optional[List[str]]
@@ -161,6 +162,7 @@ def search_packages_info(query: List[str]) -> Generator[_PackageInfo, None, None
author=metadata.get("Author", ""),
author_email=metadata.get("Author-email", ""),
license=metadata.get("License", ""),
license_expression=metadata.get("License-Expression", ""),
entry_points=entry_points,
files=files,
)
@@ -180,13 +182,18 @@ def print_results(
if i > 0:
write_output("---")
metadata_version_tuple = tuple(map(int, dist.metadata_version.split(".")))
write_output("Name: %s", dist.name)
write_output("Version: %s", dist.version)
write_output("Summary: %s", dist.summary)
write_output("Home-page: %s", dist.homepage)
write_output("Author: %s", dist.author)
write_output("Author-email: %s", dist.author_email)
write_output("License: %s", dist.license)
if metadata_version_tuple >= (2, 4) and dist.license_expression:
write_output("License-Expression: %s", dist.license_expression)
else:
write_output("License: %s", dist.license)
write_output("Location: %s", dist.location)
if dist.editable_project_location is not None:
write_output(

View File

@@ -330,7 +330,7 @@ class Configuration:
This should be treated like items of a dictionary. The order
here doesn't affect what gets overridden. That is controlled
by OVERRIDE_ORDER. However this does control the order they are
displayed to the user. It's probably most ergononmic to display
displayed to the user. It's probably most ergonomic to display
things in the same order as OVERRIDE_ORDER
"""
# SMELL: Move the conditions out of this function

View File

@@ -334,44 +334,30 @@ class CandidatePreferences:
allow_all_prereleases: bool = False
@dataclass(frozen=True)
class BestCandidateResult:
"""A collection of candidates, returned by `PackageFinder.find_best_candidate`.
This class is only intended to be instantiated by CandidateEvaluator's
`compute_best_candidate()` method.
:param all_candidates: A sequence of all available candidates found.
:param applicable_candidates: The applicable candidates.
:param best_candidate: The most preferred candidate found, or None
if no applicable candidates were found.
"""
def __init__(
self,
candidates: List[InstallationCandidate],
applicable_candidates: List[InstallationCandidate],
best_candidate: Optional[InstallationCandidate],
) -> None:
"""
:param candidates: A sequence of all available candidates found.
:param applicable_candidates: The applicable candidates.
:param best_candidate: The most preferred candidate found, or None
if no applicable candidates were found.
"""
assert set(applicable_candidates) <= set(candidates)
all_candidates: List[InstallationCandidate]
applicable_candidates: List[InstallationCandidate]
best_candidate: Optional[InstallationCandidate]
if best_candidate is None:
assert not applicable_candidates
def __post_init__(self) -> None:
assert set(self.applicable_candidates) <= set(self.all_candidates)
if self.best_candidate is None:
assert not self.applicable_candidates
else:
assert best_candidate in applicable_candidates
self._applicable_candidates = applicable_candidates
self._candidates = candidates
self.best_candidate = best_candidate
def iter_all(self) -> Iterable[InstallationCandidate]:
"""Iterate through all candidates."""
return iter(self._candidates)
def iter_applicable(self) -> Iterable[InstallationCandidate]:
"""Iterate through the applicable candidates."""
return iter(self._applicable_candidates)
assert self.best_candidate in self.applicable_candidates
class CandidateEvaluator:
@@ -675,11 +661,29 @@ class PackageFinder:
def index_urls(self) -> List[str]:
return self.search_scope.index_urls
@property
def proxy(self) -> Optional[str]:
return self._link_collector.session.pip_proxy
@property
def trusted_hosts(self) -> Iterable[str]:
for host_port in self._link_collector.session.pip_trusted_origins:
yield build_netloc(*host_port)
@property
def custom_cert(self) -> Optional[str]:
# session.verify is either a boolean (use default bundle/no SSL
# verification) or a string path to a custom CA bundle to use. We only
# care about the latter.
verify = self._link_collector.session.verify
return verify if isinstance(verify, str) else None
@property
def client_cert(self) -> Optional[str]:
cert = self._link_collector.session.cert
assert not isinstance(cert, tuple), "pip only supports PEM client certs"
return cert
@property
def allow_all_prereleases(self) -> bool:
return self._candidate_prefs.allow_all_prereleases
@@ -732,6 +736,11 @@ class PackageFinder:
return no_eggs + eggs
def _log_skipped_link(self, link: Link, result: LinkType, detail: str) -> None:
# This is a hot method so don't waste time hashing links unless we're
# actually going to log 'em.
if not logger.isEnabledFor(logging.DEBUG):
return
entry = (link, result, detail)
if entry not in self._logged_links:
# Put the link at the end so the reason is more visible and because
@@ -929,7 +938,7 @@ class PackageFinder:
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
req,
_format_versions(best_candidate_result.iter_all()),
_format_versions(best_candidate_result.all_candidates),
)
raise DistributionNotFound(f"No matching distribution found for {req}")
@@ -963,7 +972,7 @@ class PackageFinder:
logger.debug(
"Using version %s (newest of versions: %s)",
best_candidate.version,
_format_versions(best_candidate_result.iter_applicable()),
_format_versions(best_candidate_result.applicable_candidates),
)
return best_candidate
@@ -971,7 +980,7 @@ class PackageFinder:
logger.debug(
"Installed version (%s) is most up-to-date (past versions: %s)",
installed_version,
_format_versions(best_candidate_result.iter_applicable()),
_format_versions(best_candidate_result.applicable_candidates),
)
raise BestVersionAlreadyInstalled

View File

@@ -30,7 +30,7 @@ def _should_use_importlib_metadata() -> bool:
"""Whether to use the ``importlib.metadata`` or ``pkg_resources`` backend.
By default, pip uses ``importlib.metadata`` on Python 3.11+, and
``pkg_resourcess`` otherwise. This can be overridden by a couple of ways:
``pkg_resources`` otherwise. This can be overridden by a couple of ways:
* If environment variable ``_PIP_USE_IMPORTLIB_METADATA`` is set, it
dictates whether ``importlib.metadata`` is used, regardless of Python
@@ -71,7 +71,7 @@ def get_default_environment() -> BaseEnvironment:
This returns an Environment instance from the chosen backend. The default
Environment instance should be built from ``sys.path`` and may use caching
to share instance state accorss calls.
to share instance state across calls.
"""
return select_backend().Environment.default()

View File

@@ -23,6 +23,8 @@ METADATA_FIELDS = [
("Maintainer", False),
("Maintainer-email", False),
("License", False),
("License-Expression", False),
("License-File", True),
("Classifier", True),
("Requires-Dist", True),
("Requires-Python", False),

View File

@@ -2,6 +2,7 @@ import email.message
import importlib.metadata
import pathlib
import zipfile
from os import PathLike
from typing import (
Collection,
Dict,
@@ -95,6 +96,11 @@ class WheelDistribution(importlib.metadata.Distribution):
raise UnsupportedWheel(error)
return text
def locate_file(self, path: str | PathLike[str]) -> pathlib.Path:
# This method doesn't make sense for our in-memory wheel, but the API
# requires us to define it.
raise NotImplementedError
class Distribution(BaseDistribution):
def __init__(
@@ -190,7 +196,7 @@ class Distribution(BaseDistribution):
return content
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
# importlib.metadata's EntryPoint structure sasitfies BaseEntryPoint.
# importlib.metadata's EntryPoint structure satisfies BaseEntryPoint.
return self._dist.entry_points
def _metadata_impl(self) -> email.message.Message:

View File

@@ -170,12 +170,23 @@ def _ensure_quoted_url(url: str) -> str:
and without double-quoting other characters.
"""
# Split the URL into parts according to the general structure
# `scheme://netloc/path;parameters?query#fragment`.
result = urllib.parse.urlparse(url)
# `scheme://netloc/path?query#fragment`.
result = urllib.parse.urlsplit(url)
# If the netloc is empty, then the URL refers to a local filesystem path.
is_local_path = not result.netloc
path = _clean_url_path(result.path, is_local_path=is_local_path)
return urllib.parse.urlunparse(result._replace(path=path))
return urllib.parse.urlunsplit(result._replace(path=path))
def _absolute_link_url(base_url: str, url: str) -> str:
"""
A faster implementation of urllib.parse.urljoin with a shortcut
for absolute http/https URLs.
"""
if url.startswith(("https://", "http://")):
return url
else:
return urllib.parse.urljoin(base_url, url)
@functools.total_ordering
@@ -185,6 +196,7 @@ class Link:
__slots__ = [
"_parsed_url",
"_url",
"_path",
"_hashes",
"comes_from",
"requires_python",
@@ -241,6 +253,8 @@ class Link:
# Store the url as a private attribute to prevent accidentally
# trying to set a new value.
self._url = url
# The .path property is hot, so calculate its value ahead of time.
self._path = urllib.parse.unquote(self._parsed_url.path)
link_hash = LinkHash.find_hash_url_fragment(url)
hashes_from_link = {} if link_hash is None else link_hash.as_dict()
@@ -270,7 +284,7 @@ class Link:
if file_url is None:
return None
url = _ensure_quoted_url(urllib.parse.urljoin(page_url, file_url))
url = _ensure_quoted_url(_absolute_link_url(page_url, file_url))
pyrequire = file_data.get("requires-python")
yanked_reason = file_data.get("yanked")
hashes = file_data.get("hashes", {})
@@ -322,7 +336,7 @@ class Link:
if not href:
return None
url = _ensure_quoted_url(urllib.parse.urljoin(base_url, href))
url = _ensure_quoted_url(_absolute_link_url(base_url, href))
pyrequire = anchor_attribs.get("data-requires-python")
yanked_reason = anchor_attribs.get("data-yanked")
@@ -421,7 +435,7 @@ class Link:
@property
def path(self) -> str:
return urllib.parse.unquote(self._parsed_url.path)
return self._path
def splitext(self) -> Tuple[str, str]:
return splitext(posixpath.basename(self.path.rstrip("/")))
@@ -452,10 +466,10 @@ class Link:
project_name = match.group(1)
if not self._project_name_re.match(project_name):
deprecated(
reason=f"{self} contains an egg fragment with a non-PEP 508 name",
reason=f"{self} contains an egg fragment with a non-PEP 508 name.",
replacement="to use the req @ url syntax, and remove the egg fragment",
gone_in="25.0",
issue=11617,
gone_in="25.1",
issue=13157,
)
return project_name

View File

@@ -76,6 +76,18 @@ class SafeFileCache(SeparateBodyBaseCache):
with adjacent_tmp_file(path) as f:
f.write(data)
# Inherit the read/write permissions of the cache directory
# to enable multi-user cache use-cases.
mode = (
os.stat(self.directory).st_mode
& 0o666 # select read/write permissions of cache directory
| 0o600 # set owner read/write permissions
)
# Change permissions only if there is no risk of following a symlink.
if os.chmod in os.supports_fd:
os.chmod(f.fileno(), mode)
elif os.chmod in os.supports_follow_symlinks:
os.chmod(f.name, mode, follow_symlinks=False)
replace(f.name, path)

Some files were not shown because too many files have changed in this diff Show More