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,4 +1,4 @@
from __future__ import annotations
from typing import List, Optional
from pip._internal.utils import _log
@ -7,7 +7,7 @@ from pip._internal.utils import _log
_log.init_logging()
def main(args: list[str] | None = None) -> int:
def main(args: (Optional[List[str]]) = None) -> int:
"""This is preserved for old console scripts that may still be referencing
it.

View file

@ -1,6 +1,5 @@
"""Build Environment used for isolation during sdist building"""
from __future__ import annotations
"""Build Environment used for isolation during sdist building
"""
import logging
import os
@ -9,34 +8,27 @@ import site
import sys
import textwrap
from collections import OrderedDict
from collections.abc import Iterable
from types import TracebackType
from typing import TYPE_CHECKING, Protocol, TypedDict
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
from pip._vendor.certifi import where
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.version import Version
from pip import __file__ as pip_location
from pip._internal.cli.spinners import open_spinner
from pip._internal.locations import get_platlib, get_purelib, get_scheme
from pip._internal.metadata import get_default_environment, get_environment
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.logging import VERBOSE
from pip._internal.utils.packaging import get_requirement
from pip._internal.utils.subprocess import call_subprocess
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
if TYPE_CHECKING:
from pip._internal.index.package_finder import PackageFinder
from pip._internal.req.req_install import InstallRequirement
class ExtraEnviron(TypedDict, total=False):
extra_environ: dict[str, str]
logger = logging.getLogger(__name__)
def _dedup(a: str, b: str) -> tuple[str] | tuple[str, str]:
def _dedup(a: str, b: str) -> Union[Tuple[str], Tuple[str, str]]:
return (a, b) if a != b else (a,)
@ -65,7 +57,7 @@ def get_runnable_pip() -> str:
return os.fsdecode(source / "__pip-runner__.py")
def _get_system_sitepackages() -> set[str]:
def _get_system_sitepackages() -> Set[str]:
"""Get system site packages
Usually from site.getsitepackages,
@ -85,171 +77,10 @@ def _get_system_sitepackages() -> set[str]:
return {os.path.normcase(path) for path in system_sites}
class BuildEnvironmentInstaller(Protocol):
"""
Interface for installing build dependencies into an isolated build
environment.
"""
def install(
self,
requirements: Iterable[str],
prefix: _Prefix,
*,
kind: str,
for_req: InstallRequirement | None,
) -> None: ...
class SubprocessBuildEnvironmentInstaller:
"""
Install build dependencies by calling pip in a subprocess.
"""
def __init__(
self,
finder: PackageFinder,
build_constraints: list[str] | None = None,
build_constraint_feature_enabled: bool = False,
) -> None:
self.finder = finder
self._build_constraints = build_constraints or []
self._build_constraint_feature_enabled = build_constraint_feature_enabled
def _deprecation_constraint_check(self) -> None:
"""
Check for deprecation warning: PIP_CONSTRAINT affecting build environments.
This warns when build-constraint feature is NOT enabled and PIP_CONSTRAINT
is not empty.
"""
if self._build_constraint_feature_enabled or self._build_constraints:
return
pip_constraint = os.environ.get("PIP_CONSTRAINT")
if not pip_constraint or not pip_constraint.strip():
return
deprecated(
reason=(
"Setting PIP_CONSTRAINT will not affect "
"build constraints in the future,"
),
replacement=(
"to specify build constraints using --build-constraint or "
"PIP_BUILD_CONSTRAINT. To disable this warning without "
"any build constraints set --use-feature=build-constraint or "
'PIP_USE_FEATURE="build-constraint"'
),
gone_in="26.2",
issue=None,
)
def install(
self,
requirements: Iterable[str],
prefix: _Prefix,
*,
kind: str,
for_req: InstallRequirement | None,
) -> None:
self._deprecation_constraint_check()
finder = self.finder
args: list[str] = [
sys.executable,
get_runnable_pip(),
"install",
"--ignore-installed",
"--no-user",
"--prefix",
prefix.path,
"--no-warn-script-location",
"--disable-pip-version-check",
# As the build environment is ephemeral, it's wasteful to
# pre-compile everything, especially as not every Python
# module will be used/compiled in most cases.
"--no-compile",
# The prefix specified two lines above, thus
# target from config file or env var should be ignored
"--target",
"",
]
if logger.getEffectiveLevel() <= logging.DEBUG:
args.append("-vv")
elif logger.getEffectiveLevel() <= VERBOSE:
args.append("-v")
for format_control in ("no_binary", "only_binary"):
formats = getattr(finder.format_control, format_control)
args.extend(
(
"--" + format_control.replace("_", "-"),
",".join(sorted(formats or {":none:"})),
)
)
index_urls = finder.index_urls
if index_urls:
args.extend(["-i", index_urls[0]])
for extra_index in index_urls[1:]:
args.extend(["--extra-index-url", extra_index])
else:
args.append("--no-index")
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.custom_cert:
args.extend(["--cert", finder.custom_cert])
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")
# Handle build constraints
if self._build_constraint_feature_enabled:
args.extend(["--use-feature", "build-constraint"])
if self._build_constraints:
# Build constraints must be passed as both constraints
# and build constraints, so that nested builds receive
# build constraints
for constraint_file in self._build_constraints:
args.extend(["--constraint", constraint_file])
args.extend(["--build-constraint", constraint_file])
extra_environ: ExtraEnviron = {}
if self._build_constraint_feature_enabled and not self._build_constraints:
# If there are no build constraints but the build constraints
# feature is enabled then we must ignore regular constraints
# in the isolated build environment
extra_environ = {"extra_environ": {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}}
args.append("--")
args.extend(requirements)
identify_requirement = (
f" for {for_req.name}" if for_req and for_req.name else ""
)
with open_spinner(f"Installing {kind}") as spinner:
call_subprocess(
args,
command_desc=f"installing {kind}{identify_requirement}",
spinner=spinner,
**extra_environ,
)
class BuildEnvironment:
"""Creates and manages an isolated environment to install build deps"""
def __init__(self, installer: BuildEnvironmentInstaller) -> None:
self.installer = installer
def __init__(self) -> None:
temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
self._prefixes = OrderedDict(
@ -257,8 +88,8 @@ class BuildEnvironment:
for name in ("normal", "overlay")
)
self._bin_dirs: list[str] = []
self._lib_dirs: list[str] = []
self._bin_dirs: List[str] = []
self._lib_dirs: List[str] = []
for prefix in reversed(list(self._prefixes.values())):
self._bin_dirs.append(prefix.bin_dir)
self._lib_dirs.extend(prefix.lib_dirs)
@ -326,9 +157,9 @@ class BuildEnvironment:
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:
for varname, old_value in self._save_env.items():
if old_value is None:
@ -338,7 +169,7 @@ class BuildEnvironment:
def check_requirements(
self, reqs: Iterable[str]
) -> tuple[set[tuple[str, str]], set[str]]:
) -> Tuple[Set[Tuple[str, str]], Set[str]]:
"""Return 2 sets:
- conflicting requirements: set of (installed, wanted) reqs tuples
- missing requirements: set of reqs
@ -352,7 +183,7 @@ class BuildEnvironment:
else get_default_environment()
)
for req_str in reqs:
req = get_requirement(req_str)
req = Requirement(req_str)
# We're explicitly evaluating with an empty extra value, since build
# environments are not provided any mechanism to select specific extras.
if req.marker is not None and not req.marker.evaluate({"extra": ""}):
@ -372,18 +203,81 @@ class BuildEnvironment:
def install_requirements(
self,
finder: "PackageFinder",
requirements: Iterable[str],
prefix_as_string: str,
*,
kind: str,
for_req: InstallRequirement | None = None,
) -> None:
prefix = self._prefixes[prefix_as_string]
assert not prefix.setup
prefix.setup = True
if not requirements:
return
self.installer.install(requirements, prefix, kind=kind, for_req=for_req)
self._install_requirements(
get_runnable_pip(),
finder,
requirements,
prefix,
kind=kind,
)
@staticmethod
def _install_requirements(
pip_runnable: str,
finder: "PackageFinder",
requirements: Iterable[str],
prefix: _Prefix,
*,
kind: str,
) -> None:
args: List[str] = [
sys.executable,
pip_runnable,
"install",
"--ignore-installed",
"--no-user",
"--prefix",
prefix.path,
"--no-warn-script-location",
]
if logger.getEffectiveLevel() <= logging.DEBUG:
args.append("-v")
for format_control in ("no_binary", "only_binary"):
formats = getattr(finder.format_control, format_control)
args.extend(
(
"--" + format_control.replace("_", "-"),
",".join(sorted(formats or {":none:"})),
)
)
index_urls = finder.index_urls
if index_urls:
args.extend(["-i", index_urls[0]])
for extra_index in index_urls[1:]:
args.extend(["--extra-index-url", extra_index])
else:
args.append("--no-index")
for link in finder.find_links:
args.extend(["--find-links", link])
for host in finder.trusted_hosts:
args.extend(["--trusted-host", host])
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,
)
class NoOpBuildEnvironment(BuildEnvironment):
@ -397,9 +291,9 @@ class NoOpBuildEnvironment(BuildEnvironment):
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:
pass
@ -408,10 +302,10 @@ class NoOpBuildEnvironment(BuildEnvironment):
def install_requirements(
self,
finder: "PackageFinder",
requirements: Iterable[str],
prefix_as_string: str,
*,
kind: str,
for_req: InstallRequirement | None = None,
) -> None:
raise NotImplementedError()

View file

@ -1,13 +1,12 @@
"""Cache Management"""
from __future__ import annotations
"""Cache Management
"""
import hashlib
import json
import logging
import os
from pathlib import Path
from typing import Any
from typing import Any, Dict, List, Optional
from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
from pip._vendor.packaging.utils import canonicalize_name
@ -24,7 +23,7 @@ logger = logging.getLogger(__name__)
ORIGIN_JSON_NAME = "origin.json"
def _hash_dict(d: dict[str, str]) -> str:
def _hash_dict(d: Dict[str, str]) -> str:
"""Return a stable sha224 of a dictionary."""
s = json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
return hashlib.sha224(s.encode("ascii")).hexdigest()
@ -41,11 +40,11 @@ class Cache:
assert not cache_dir or os.path.isabs(cache_dir)
self.cache_dir = cache_dir or None
def _get_cache_path_parts(self, link: Link) -> list[str]:
def _get_cache_path_parts(self, link: Link) -> List[str]:
"""Get parts of part that must be os.path.joined with cache_dir"""
# We want to generate an url to use as our cache key, we don't want to
# just reuse the URL because it might have other items in the fragment
# just re-use the URL because it might have other items in the fragment
# and we don't care about those.
key_parts = {"url": link.url_without_fragment}
if link.hash_name is not None and link.hash is not None:
@ -74,7 +73,7 @@ class Cache:
return parts
def _get_candidates(self, link: Link, canonical_package_name: str) -> list[Any]:
def _get_candidates(self, link: Link, canonical_package_name: str) -> List[Any]:
can_not_cache = not self.cache_dir or not canonical_package_name or not link
if can_not_cache:
return []
@ -91,8 +90,8 @@ class Cache:
def get(
self,
link: Link,
package_name: str | None,
supported_tags: list[Tag],
package_name: Optional[str],
supported_tags: List[Tag],
) -> Link:
"""Returns a link to a cached item if it exists, otherwise returns the
passed link.
@ -129,8 +128,8 @@ class SimpleWheelCache(Cache):
def get(
self,
link: Link,
package_name: str | None,
supported_tags: list[Tag],
package_name: Optional[str],
supported_tags: List[Tag],
) -> Link:
candidates = []
@ -143,7 +142,7 @@ class SimpleWheelCache(Cache):
wheel = Wheel(wheel_name)
except InvalidWheelFilename:
continue
if wheel.name != canonical_package_name:
if canonicalize_name(wheel.name) != canonical_package_name:
logger.debug(
"Ignoring cached wheel %s for %s as it "
"does not match the expected distribution name %s.",
@ -190,7 +189,7 @@ class CacheEntry:
):
self.link = link
self.persistent = persistent
self.origin: DirectUrl | None = None
self.origin: Optional[DirectUrl] = None
origin_direct_url_path = Path(self.link.file_path).parent / ORIGIN_JSON_NAME
if origin_direct_url_path.exists():
try:
@ -227,8 +226,8 @@ class WheelCache(Cache):
def get(
self,
link: Link,
package_name: str | None,
supported_tags: list[Tag],
package_name: Optional[str],
supported_tags: List[Tag],
) -> Link:
cache_entry = self.get_cache_entry(link, package_name, supported_tags)
if cache_entry is None:
@ -238,9 +237,9 @@ class WheelCache(Cache):
def get_cache_entry(
self,
link: Link,
package_name: str | None,
supported_tags: list[Tag],
) -> CacheEntry | None:
package_name: Optional[str],
supported_tags: List[Tag],
) -> Optional[CacheEntry]:
"""Returns a CacheEntry with a link to a cached item if it exists or
None. The cache entry indicates if the item was found in the persistent
or ephemeral cache.

View file

@ -1,3 +1,4 @@
"""Subpackage containing all of pip's command line interface related code"""
"""Subpackage containing all of pip's command line interface related code
"""
# This file intentionally does not import submodules

View file

@ -1,13 +1,11 @@
"""Logic that powers autocompletion installed by ``pip completion``."""
from __future__ import annotations
"""Logic that powers autocompletion installed by ``pip completion``.
"""
import optparse
import os
import sys
from collections.abc import Iterable
from itertools import chain
from typing import Any
from typing import Any, Iterable, List, Optional
from pip._internal.cli.main_parser import create_main_parser
from pip._internal.commands import commands_dict, create_command
@ -19,10 +17,6 @@ def autocomplete() -> None:
# Don't complete if user hasn't sourced bash_completion file.
if "PIP_AUTO_COMPLETE" not in os.environ:
return
# Don't complete if autocompletion environment variables
# are not present
if not os.environ.get("COMP_WORDS") or not os.environ.get("COMP_CWORD"):
return
cwords = os.environ["COMP_WORDS"].split()[1:]
cword = int(os.environ["COMP_CWORD"])
try:
@ -35,7 +29,7 @@ def autocomplete() -> None:
options = []
# subcommand
subcommand_name: str | None = None
subcommand_name: Optional[str] = None
for word in cwords:
if word in subcommands:
subcommand_name = word
@ -103,12 +97,6 @@ def autocomplete() -> None:
if option[1] and option[0][:2] == "--":
opt_label += "="
print(opt_label)
# Complete sub-commands (unless one is already given).
if not any(name in cwords for name in subcommand.handler_map()):
for handler_name in subcommand.handler_map():
if handler_name.startswith(current):
print(handler_name)
else:
# show main parser options only when necessary
@ -130,8 +118,8 @@ def autocomplete() -> None:
def get_path_completion_type(
cwords: list[str], cword: int, opts: Iterable[Any]
) -> str | None:
cwords: List[str], cword: int, opts: Iterable[Any]
) -> Optional[str]:
"""Get the type of path completion (``file``, ``dir``, ``path`` or None)
:param cwords: same as the environmental variable ``COMP_WORDS``

View file

@ -1,7 +1,6 @@
"""Base Command class, and related routines"""
from __future__ import annotations
import functools
import logging
import logging.config
import optparse
@ -9,9 +8,8 @@ import os
import sys
import traceback
from optparse import Values
from typing import Callable
from typing import Any, Callable, List, Optional, Tuple
from pip._vendor.rich import reconfigure
from pip._vendor.rich import traceback as rich_traceback
from pip._internal.cli import cmdoptions
@ -30,6 +28,7 @@ from pip._internal.exceptions import (
InstallationError,
NetworkConnectionError,
PreviousBuildDirError,
UninstallationError,
)
from pip._internal.utils.filesystem import check_path_owner
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
@ -62,7 +61,7 @@ class Command(CommandContextMixIn):
isolated=isolated,
)
self.tempdir_registry: TempDirRegistry | None = None
self.tempdir_registry: Optional[TempDirRegistry] = None
# Commands should add options to this option group
optgroup_name = f"{self.name.capitalize()} Options"
@ -89,78 +88,21 @@ class Command(CommandContextMixIn):
# are present.
assert not hasattr(options, "no_index")
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
raise NotImplementedError
def _run_wrapper(self, level_number: int, options: Values, args: list[str]) -> int:
def _inner_run() -> int:
try:
return self.run(options, args)
finally:
self.handle_pip_version_check(options)
if options.debug_mode:
rich_traceback.install(show_locals=True)
return _inner_run()
try:
status = _inner_run()
assert isinstance(status, int)
return status
except DiagnosticPipError as exc:
logger.error("%s", exc, extra={"rich": True})
logger.debug("Exception information:", exc_info=True)
return ERROR
except PreviousBuildDirError as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return PREVIOUS_BUILD_DIR_ERROR
except (
InstallationError,
BadCommand,
NetworkConnectionError,
) as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return ERROR
except CommandError as exc:
logger.critical("%s", exc)
logger.debug("Exception information:", exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to
# stderr because stdout no longer works.
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical("Operation cancelled by user")
logger.debug("Exception information:", exc_info=True)
return ERROR
except BaseException:
logger.critical("Exception:", exc_info=True)
return UNKNOWN_ERROR
def parse_args(self, args: list[str]) -> tuple[Values, list[str]]:
def parse_args(self, args: List[str]) -> Tuple[Values, List[str]]:
# factored out for testability
return self.parser.parse_args(args)
def main(self, args: list[str]) -> int:
def main(self, args: List[str]) -> int:
try:
with self.main_context():
return self._main(args)
finally:
logging.shutdown()
def _main(self, args: list[str]) -> int:
def _main(self, args: List[str]) -> int:
# We must initialize this before the tempdir manager, otherwise the
# configuration would not be accessible by the time we clean up the
# tempdir manager.
@ -173,13 +115,7 @@ class Command(CommandContextMixIn):
# Set verbosity so that it can be used elsewhere.
self.verbosity = options.verbose - options.quiet
if options.debug_mode:
self.verbosity = 2
if hasattr(options, "progress_bar") and options.progress_bar == "auto":
options.progress_bar = "on" if self.verbosity >= 0 else "off"
reconfigure(no_color=options.no_color)
level_number = setup_logging(
verbosity=self.verbosity,
no_color=options.no_color,
@ -235,10 +171,66 @@ class Command(CommandContextMixIn):
)
options.cache_dir = None
return self._run_wrapper(level_number, options, args)
def intercepts_unhandled_exc(
run_func: Callable[..., int]
) -> Callable[..., int]:
@functools.wraps(run_func)
def exc_logging_wrapper(*args: Any) -> int:
try:
status = run_func(*args)
assert isinstance(status, int)
return status
except DiagnosticPipError as exc:
logger.error("%s", exc, extra={"rich": True})
logger.debug("Exception information:", exc_info=True)
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
"""
map of names to handler actions for commands with sub-actions
"""
return {}
return ERROR
except PreviousBuildDirError as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return PREVIOUS_BUILD_DIR_ERROR
except (
InstallationError,
UninstallationError,
BadCommand,
NetworkConnectionError,
) as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return ERROR
except CommandError as exc:
logger.critical("%s", exc)
logger.debug("Exception information:", exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to
# stderr because stdout no longer works.
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical("Operation cancelled by user")
logger.debug("Exception information:", exc_info=True)
return ERROR
except BaseException:
logger.critical("Exception:", exc_info=True)
return UNKNOWN_ERROR
return exc_logging_wrapper
try:
if not options.debug_mode:
run = intercepts_unhandled_exc(self.run)
else:
run = self.run
rich_traceback.install(show_locals=True)
return run(options, args)
finally:
self.handle_pip_version_check(options)

View file

@ -9,16 +9,15 @@ pass on state. To be consistent, all options will follow this design.
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import importlib.util
import logging
import os
import pathlib
import textwrap
from functools import partial
from optparse import SUPPRESS_HELP, Option, OptionGroup, OptionParser, Values
from textwrap import dedent
from typing import Any, Callable
from typing import Any, Callable, Dict, Optional, Tuple
from pip._vendor.packaging.utils import canonicalize_name
@ -48,7 +47,7 @@ def raise_option_error(parser: OptionParser, option: Option, msg: str) -> None:
parser.error(msg)
def make_option_group(group: dict[str, Any], parser: ConfigOptionParser) -> OptionGroup:
def make_option_group(group: Dict[str, Any], parser: ConfigOptionParser) -> OptionGroup:
"""
Return an OptionGroup object
group -- assumed to be dict with 'name' and 'options' keys
@ -100,29 +99,6 @@ def check_dist_restriction(options: Values, check_target: bool = False) -> None:
)
def check_build_constraints(options: Values) -> None:
"""Function for validating build constraints options.
:param options: The OptionParser options.
"""
if hasattr(options, "build_constraints") and options.build_constraints:
if not options.build_isolation:
raise CommandError(
"--build-constraint cannot be used with --no-build-isolation."
)
# Import here to avoid circular imports
from pip._internal.network.session import PipSession
from pip._internal.req.req_file import get_file_content
# Eagerly check build constraints file contents
# is valid so that we don't fail in when trying
# to check constraints in isolated build process
with PipSession() as session:
for constraint_file in options.build_constraints:
get_file_content(constraint_file, session)
def _path_option_check(option: Option, opt: str, value: str) -> str:
return os.path.expanduser(value)
@ -183,7 +159,8 @@ require_virtualenv: Callable[..., Option] = partial(
action="store_true",
default=False,
help=(
"Allow pip to only run in a virtual environment; exit with an error otherwise."
"Allow pip to only run in a virtual environment; "
"exit with an error otherwise."
),
)
@ -249,13 +226,9 @@ progress_bar: Callable[..., Option] = partial(
"--progress-bar",
dest="progress_bar",
type="choice",
choices=["auto", "on", "off", "raw"],
default="auto",
help=(
"Specify whether the progress bar should be used. In 'auto'"
" mode, --quiet will suppress all progress bars."
" [auto, on, off, raw] (default: auto)"
),
choices=["on", "off"],
default="on",
help="Specify whether the progress bar should be used [on, off] (default: on)",
)
log: Callable[..., Option] = partial(
@ -287,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 [auto, disabled, import, subprocess]."
" (default: %default)"
" Specify which mechanism to use [disabled, import, subprocess]."
" (default: disabled)"
),
)
@ -307,17 +280,8 @@ retries: Callable[..., Option] = partial(
dest="retries",
type="int",
default=5,
help="Maximum attempts to establish a new HTTP connection. (default: %default)",
)
resume_retries: Callable[..., Option] = partial(
Option,
"--resume-retries",
dest="resume_retries",
type="int",
default=5,
help="Maximum attempts to resume or restart an incomplete download. "
"(default: %default)",
help="Maximum number of retries each connection should attempt "
"(default %default times).",
)
timeout: Callable[..., Option] = partial(
@ -451,21 +415,6 @@ def constraints() -> Option:
)
def build_constraints() -> Option:
return Option(
"--build-constraint",
dest="build_constraints",
action="append",
type="str",
default=[],
metavar="file",
help=(
"Constrain build dependencies using the given constraints file. "
"This option can be used multiple times."
),
)
def requirements() -> Option:
return Option(
"-r",
@ -596,7 +545,7 @@ platforms: Callable[..., Option] = partial(
# This was made a separate function for unit-testing purposes.
def _convert_python_version(value: str) -> tuple[tuple[int, ...], str | None]:
def _convert_python_version(value: str) -> Tuple[Tuple[int, ...], Optional[str]]:
"""
Convert a version string like "3", "37", or "3.7.3" into a tuple of ints.
@ -784,46 +733,6 @@ no_deps: Callable[..., Option] = partial(
help="Don't install package dependencies.",
)
def _handle_dependency_group(
option: Option, opt: str, value: str, parser: OptionParser
) -> None:
"""
Process a value provided for the --group option.
Splits on the rightmost ":", and validates that the path (if present) ends
in `pyproject.toml`. Defaults the path to `pyproject.toml` when one is not given.
`:` cannot appear in dependency group names, so this is a safe and simple parse.
This is an optparse.Option callback for the dependency_groups option.
"""
path, sep, groupname = value.rpartition(":")
if not sep:
path = "pyproject.toml"
else:
# check for 'pyproject.toml' filenames using pathlib
if pathlib.PurePath(path).name != "pyproject.toml":
msg = "group paths use 'pyproject.toml' filenames"
raise_option_error(parser, option=option, msg=msg)
parser.values.dependency_groups.append((path, groupname))
dependency_groups: Callable[..., Option] = partial(
Option,
"--group",
dest="dependency_groups",
default=[],
type=str,
action="callback",
callback=_handle_dependency_group,
metavar="[path:]group",
help='Install a named dependency-group from a "pyproject.toml" file. '
'If a path is given, the name of the file must be "pyproject.toml". '
'Defaults to using "pyproject.toml" in the current directory.',
)
ignore_requires_python: Callable[..., Option] = partial(
Option,
"--ignore-requires-python",
@ -849,16 +758,62 @@ check_build_deps: Callable[..., Option] = partial(
dest="check_build_deps",
action="store_true",
default=False,
help="Check the build dependencies.",
help="Check the build dependencies when PEP517 is used.",
)
def _handle_no_use_pep517(
option: Option, opt: str, value: str, parser: OptionParser
) -> None:
"""
Process a value provided for the --no-use-pep517 option.
This is an optparse.Option callback for the no_use_pep517 option.
"""
# Since --no-use-pep517 doesn't accept arguments, the value argument
# will be None if --no-use-pep517 is passed via the command-line.
# However, the value can be non-None if the option is triggered e.g.
# by an environment variable, for example "PIP_NO_USE_PEP517=true".
if value is not None:
msg = """A value was passed for --no-use-pep517,
probably using either the PIP_NO_USE_PEP517 environment variable
or the "no-use-pep517" config file option. Use an appropriate value
of the PIP_USE_PEP517 environment variable or the "use-pep517"
config file option instead.
"""
raise_option_error(parser, option=option, msg=msg)
# If user doesn't wish to use pep517, we check if setuptools and wheel are installed
# and raise error if it is not.
packages = ("setuptools", "wheel")
if not all(importlib.util.find_spec(package) for package in packages):
msg = (
f"It is not possible to use --no-use-pep517 "
f"without {' and '.join(packages)} installed."
)
raise_option_error(parser, option=option, msg=msg)
# Otherwise, --no-use-pep517 was passed via the command-line.
parser.values.use_pep517 = False
use_pep517: Any = partial(
Option,
"--use-pep517",
dest="use_pep517",
action="store_true",
default=True,
default=None,
help="Use PEP 517 for building source distributions "
"(use --no-use-pep517 to force legacy behaviour).",
)
no_use_pep517: Any = partial(
Option,
"--no-use-pep517",
dest="use_pep517",
action="callback",
callback=_handle_no_use_pep517,
default=None,
help=SUPPRESS_HELP,
)
@ -891,11 +846,30 @@ config_settings: Callable[..., Option] = partial(
action="callback",
callback=_handle_config_settings,
metavar="settings",
help="Configuration settings to be passed to the build backend. "
help="Configuration settings to be passed to the PEP 517 build backend. "
"Settings take the form KEY=VALUE. Use multiple --config-settings options "
"to pass multiple keys to the backend.",
)
build_options: Callable[..., Option] = partial(
Option,
"--build-option",
dest="build_options",
metavar="options",
action="append",
help="Extra arguments to be supplied to 'setup.py bdist_wheel'.",
)
global_options: Callable[..., Option] = partial(
Option,
"--global-option",
dest="global_options",
action="append",
metavar="options",
help="Extra global options to be supplied to the setup.py "
"call before the install or bdist_wheel command.",
)
no_clean: Callable[..., Option] = partial(
Option,
"--no-clean",
@ -913,20 +887,12 @@ pre: Callable[..., Option] = partial(
"pip only finds stable versions.",
)
json: Callable[..., Option] = partial(
Option,
"--json",
action="store_true",
default=False,
help="Output data in a machine-readable JSON format.",
)
disable_pip_version_check: Callable[..., Option] = partial(
Option,
"--disable-pip-version-check",
dest="disable_pip_version_check",
action="store_true",
default=False,
default=True,
help="Don't periodically check PyPI to determine whether a new version "
"of pip is available for download. Implied with --no-index.",
)
@ -937,7 +903,7 @@ root_user_action: Callable[..., Option] = partial(
dest="root_user_action",
default="warn",
choices=["warn", "ignore"],
help="Action if pip is run as a root user [warn, ignore] (default: warn)",
help="Action if pip is run as a root user. By default, a warning message is shown.",
)
@ -1024,13 +990,12 @@ no_python_version_warning: Callable[..., Option] = partial(
dest="no_python_version_warning",
action="store_true",
default=False,
help=SUPPRESS_HELP, # No-op, a hold-over from the Python 2->3 transition.
help="Silence deprecation warnings for upcoming unsupported Pythons.",
)
# Features that are now always on. A warning is printed if they are used.
ALWAYS_ENABLED_FEATURES = [
"truststore", # always on since 24.2
"no-binary-enable-wheel-cache", # always on since 23.1
]
@ -1043,7 +1008,7 @@ use_new_feature: Callable[..., Option] = partial(
default=[],
choices=[
"fast-deps",
"build-constraint",
"truststore",
]
+ ALWAYS_ENABLED_FEATURES,
help="Enable new functionality, that may be backward incompatible.",
@ -1058,16 +1023,16 @@ use_deprecated_feature: Callable[..., Option] = partial(
default=[],
choices=[
"legacy-resolver",
"legacy-certs",
],
help=("Enable deprecated functionality, that will be removed in the future."),
)
##########
# groups #
##########
general_group: dict[str, Any] = {
general_group: Dict[str, Any] = {
"name": "General Options",
"options": [
help_,
@ -1095,11 +1060,10 @@ general_group: dict[str, Any] = {
no_python_version_warning,
use_new_feature,
use_deprecated_feature,
resume_retries,
],
}
index_group: dict[str, Any] = {
index_group: Dict[str, Any] = {
"name": "Package Index Options",
"options": [
index_url,

View file

@ -1,6 +1,5 @@
from collections.abc import Generator
from contextlib import AbstractContextManager, ExitStack, contextmanager
from typing import TypeVar
from contextlib import ExitStack, contextmanager
from typing import ContextManager, Generator, TypeVar
_T = TypeVar("_T", covariant=True)
@ -22,7 +21,7 @@ class CommandContextMixIn:
finally:
self._in_main_context = False
def enter_context(self, context_provider: AbstractContextManager[_T]) -> _T:
def enter_context(self, context_provider: ContextManager[_T]) -> _T:
assert self._in_main_context
return self._main_context.enter_context(context_provider)

View file

@ -1,175 +0,0 @@
"""
Contains command classes which may interact with an index / the network.
Unlike its sister module, req_command, this module still uses lazy imports
so commands which don't always hit the network (e.g. list w/o --outdated or
--uptodate) don't need waste time importing PipSession and friends.
"""
from __future__ import annotations
import logging
import os
import sys
from functools import lru_cache
from optparse import Values
from typing import TYPE_CHECKING
from pip._vendor import certifi
from pip._internal.cli.base_command import Command
from pip._internal.cli.command_context import CommandContextMixIn
if TYPE_CHECKING:
from ssl import SSLContext
from pip._internal.network.session import PipSession
logger = logging.getLogger(__name__)
@lru_cache
def _create_truststore_ssl_context() -> SSLContext | None:
if sys.version_info < (3, 10):
logger.debug("Disabling truststore because Python version isn't 3.10+")
return None
try:
import ssl
except ImportError:
logger.warning("Disabling truststore since ssl support is missing")
return None
try:
from pip._vendor import truststore
except ImportError:
logger.warning("Disabling truststore because platform isn't supported")
return None
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.load_verify_locations(certifi.where())
return ctx
class SessionCommandMixin(CommandContextMixIn):
"""
A class mixin for command classes needing _build_session().
"""
def __init__(self) -> None:
super().__init__()
self._session: PipSession | None = None
@classmethod
def _get_index_urls(cls, options: Values) -> list[str] | None:
"""Return a list of index urls from user-provided options."""
index_urls = []
if not getattr(options, "no_index", False):
url = getattr(options, "index_url", None)
if url:
index_urls.append(url)
urls = getattr(options, "extra_index_urls", None)
if urls:
index_urls.extend(urls)
# Return None rather than an empty list
return index_urls or None
def get_default_session(self, options: Values) -> PipSession:
"""Get a default-managed session."""
if self._session is None:
self._session = self.enter_context(self._build_session(options))
# there's no type annotation on requests.Session, so it's
# automatically ContextManager[Any] and self._session becomes Any,
# then https://github.com/python/mypy/issues/7696 kicks in
assert self._session is not None
return self._session
def _build_session(
self,
options: Values,
retries: int | None = None,
timeout: int | None = None,
) -> PipSession:
from pip._internal.network.session import PipSession
cache_dir = options.cache_dir
assert not cache_dir or os.path.isabs(cache_dir)
if "legacy-certs" not in options.deprecated_features_enabled:
ssl_context = _create_truststore_ssl_context()
else:
ssl_context = None
session = PipSession(
cache=os.path.join(cache_dir, "http-v2") if cache_dir else None,
retries=retries if retries is not None else options.retries,
trusted_hosts=options.trusted_hosts,
index_urls=self._get_index_urls(options),
ssl_context=ssl_context,
)
# Handle custom ca-bundles from the user
if options.cert:
session.verify = options.cert
# Handle SSL client certificate
if options.client_cert:
session.cert = options.client_cert
# Handle timeouts
if options.timeout or timeout:
session.timeout = timeout if timeout is not None else options.timeout
# Handle configured proxies
if options.proxy:
session.proxies = {
"http": options.proxy,
"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
session.auth.keyring_provider = options.keyring_provider
return session
def _pip_self_version_check(session: PipSession, options: Values) -> None:
from pip._internal.self_outdated_check import pip_self_version_check as check
check(session, options)
class IndexGroupCommand(Command, SessionCommandMixin):
"""
Abstract base class for commands with the index_group options.
This also corresponds to the commands that permit the pip version check.
"""
def handle_pip_version_check(self, options: Values) -> None:
"""
Do the pip version check if not disabled.
This overrides the default behavior of not doing the check.
"""
# Make sure the index_group options are present.
assert hasattr(options, "no_index")
if options.disable_pip_version_check or options.no_index:
return
try:
# Otherwise, check if we're using the latest version of pip available.
session = self._build_session(
options,
retries=0,
timeout=min(5, options.timeout),
)
with session:
_pip_self_version_check(session, options)
except Exception:
logger.warning("There was an error checking the latest version of pip.")
logger.debug("See below for error", exc_info=True)

View file

@ -1,12 +1,11 @@
"""Primary application entrypoint."""
from __future__ import annotations
"""Primary application entrypoint.
"""
import locale
import logging
import os
import sys
import warnings
from typing import List, Optional
from pip._internal.cli.autocompletion import autocomplete
from pip._internal.cli.main_parser import parse_command
@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
# main, this should not be an issue in practice.
def main(args: list[str] | None = None) -> int:
def main(args: Optional[List[str]] = None) -> int:
if args is None:
args = sys.argv[1:]

View file

@ -1,10 +1,10 @@
"""A single place for constructing and exposing the main parser"""
from __future__ import annotations
"""A single place for constructing and exposing the main parser
"""
import os
import subprocess
import sys
from typing import List, Optional, Tuple
from pip._internal.build_env import get_runnable_pip
from pip._internal.cli import cmdoptions
@ -47,7 +47,7 @@ def create_main_parser() -> ConfigOptionParser:
return parser
def identify_python_interpreter(python: str) -> str | None:
def identify_python_interpreter(python: str) -> Optional[str]:
# If the named file exists, use it.
# If it's a directory, assume it's a virtual environment and
# look for the environment's Python executable.
@ -66,7 +66,7 @@ def identify_python_interpreter(python: str) -> str | None:
return None
def parse_command(args: list[str]) -> tuple[str, list[str]]:
def parse_command(args: List[str]) -> Tuple[str, List[str]]:
parser = create_main_parser()
# Note: parser calls disable_interspersed_args(), so the result of this

View file

@ -1,15 +1,12 @@
"""Base option parser setup"""
from __future__ import annotations
import logging
import optparse
import shutil
import sys
import textwrap
from collections.abc import Generator
from contextlib import suppress
from typing import Any, NoReturn
from typing import Any, Dict, Generator, List, Tuple
from pip._internal.cli.status_codes import UNKNOWN_ERROR
from pip._internal.configuration import Configuration, ConfigurationError
@ -70,7 +67,7 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), " "))
return msg
def format_description(self, description: str | None) -> str:
def format_description(self, description: str) -> str:
# leave full control over description to us
if description:
if hasattr(self.parser, "main"):
@ -88,7 +85,7 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
else:
return ""
def format_epilog(self, epilog: str | None) -> str:
def format_epilog(self, epilog: str) -> str:
# leave full control over epilog to us
if epilog:
return epilog
@ -145,7 +142,7 @@ class CustomOptionParser(optparse.OptionParser):
return group
@property
def option_list_all(self) -> list[optparse.Option]:
def option_list_all(self) -> List[optparse.Option]:
"""Get a list of all options, including those in option groups."""
res = self.option_list[:]
for i in self.option_groups:
@ -180,34 +177,33 @@ class ConfigOptionParser(CustomOptionParser):
def _get_ordered_configuration_items(
self,
) -> Generator[tuple[str, Any], None, None]:
) -> Generator[Tuple[str, Any], None, None]:
# Configuration gives keys in an unordered manner. Order them.
override_order = ["global", self.name, ":env:"]
# Pool the options into different groups
section_items: dict[str, list[tuple[str, Any]]] = {
section_items: Dict[str, List[Tuple[str, Any]]] = {
name: [] for name in override_order
}
for section_key, val in self.config.items():
# ignore empty values
if not val:
logger.debug(
"Ignoring configuration key '%s' as it's value is empty.",
section_key,
)
continue
for _, value in self.config.items(): # noqa: PERF102
for section_key, val in value.items():
# ignore empty values
if not val:
logger.debug(
"Ignoring configuration key '%s' as its value is empty.",
section_key,
)
continue
section, key = section_key.split(".", 1)
if section in override_order:
section_items[section].append((key, val))
section, key = section_key.split(".", 1)
if section in override_order:
section_items[section].append((key, val))
# Yield each group in their override order
for section in override_order:
yield from section_items[section]
for key, val in section_items[section]:
yield key, val
def _update_defaults(self, defaults: dict[str, Any]) -> dict[str, Any]:
def _update_defaults(self, defaults: Dict[str, Any]) -> Dict[str, Any]:
"""Updates the given defaults with values from the config files and
the environ. Does a little special handling for certain types of
options (lists)."""
@ -293,6 +289,6 @@ class ConfigOptionParser(CustomOptionParser):
defaults[option.dest] = option.check_value(opt_str, default)
return optparse.Values(defaults)
def error(self, msg: str) -> NoReturn:
def error(self, msg: str) -> None:
self.print_usage(sys.stderr)
self.exit(UNKNOWN_ERROR, f"{msg}\n")

View file

@ -1,15 +1,10 @@
from __future__ import annotations
import functools
import sys
from collections.abc import Generator, Iterable, Iterator
from typing import Callable, Literal, TypeVar
from typing import Callable, Generator, Iterable, Iterator, Optional, Tuple
from pip._vendor.rich.progress import (
BarColumn,
DownloadColumn,
FileSizeColumn,
MofNCompleteColumn,
Progress,
ProgressColumn,
SpinnerColumn,
@ -19,27 +14,22 @@ from pip._vendor.rich.progress import (
TransferSpeedColumn,
)
from pip._internal.cli.spinners import RateLimiter
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.logging import get_console, get_indentation
from pip._internal.utils.logging import get_indentation
T = TypeVar("T")
ProgressRenderer = Callable[[Iterable[T]], Iterator[T]]
BarType = Literal["on", "off", "raw"]
DownloadProgressRenderer = Callable[[Iterable[bytes]], Iterator[bytes]]
def _rich_download_progress_bar(
def _rich_progress_bar(
iterable: Iterable[bytes],
*,
bar_type: BarType,
size: int | None,
initial_progress: int | None = None,
bar_type: str,
size: int,
) -> Generator[bytes, None, None]:
assert bar_type == "on", "This should only be used in the default mode."
if not size:
total = float("inf")
columns: tuple[ProgressColumn, ...] = (
columns: Tuple[ProgressColumn, ...] = (
TextColumn("[progress.description]{task.description}"),
SpinnerColumn("line", speed=1.5),
FileSizeColumn(),
@ -53,99 +43,26 @@ def _rich_download_progress_bar(
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TextColumn("{task.fields[time_description]}"),
TimeRemainingColumn(elapsed_when_finished=True),
TextColumn("eta"),
TimeRemainingColumn(),
)
progress = Progress(*columns, refresh_per_second=5)
task_id = progress.add_task(
" " * (get_indentation() + 2), total=total, time_description="eta"
)
if initial_progress is not None:
progress.update(task_id, advance=initial_progress)
progress = Progress(*columns, refresh_per_second=30)
task_id = progress.add_task(" " * (get_indentation() + 2), total=total)
with progress:
for chunk in iterable:
yield chunk
progress.update(task_id, advance=len(chunk))
progress.update(task_id, time_description="")
def _rich_install_progress_bar(
iterable: Iterable[InstallRequirement], *, total: int
) -> Iterator[InstallRequirement]:
columns = (
TextColumn("{task.fields[indent]}"),
BarColumn(),
MofNCompleteColumn(),
TextColumn("{task.description}"),
)
console = get_console()
bar = Progress(*columns, refresh_per_second=6, console=console, transient=True)
# Hiding the progress bar at initialization forces a refresh cycle to occur
# until the bar appears, avoiding very short flashes.
task = bar.add_task("", total=total, indent=" " * get_indentation(), visible=False)
with bar:
for req in iterable:
bar.update(task, description=rf"\[{req.name}]", visible=True)
yield req
bar.advance(task)
def _raw_progress_bar(
iterable: Iterable[bytes],
*,
size: int | None,
initial_progress: int | None = None,
) -> Generator[bytes, None, None]:
def write_progress(current: int, total: int) -> None:
sys.stdout.write(f"Progress {current} of {total}\n")
sys.stdout.flush()
current = initial_progress or 0
total = size or 0
rate_limiter = RateLimiter(0.25)
write_progress(current, total)
for chunk in iterable:
current += len(chunk)
if rate_limiter.ready() or current == total:
write_progress(current, total)
rate_limiter.reset()
yield chunk
def get_download_progress_renderer(
*, bar_type: BarType, size: int | None = None, initial_progress: int | None = None
) -> ProgressRenderer[bytes]:
*, bar_type: str, size: Optional[int] = None
) -> DownloadProgressRenderer:
"""Get an object that can be used to render the download progress.
Returns a callable, that takes an iterable to "wrap".
"""
if bar_type == "on":
return functools.partial(
_rich_download_progress_bar,
bar_type=bar_type,
size=size,
initial_progress=initial_progress,
)
elif bar_type == "raw":
return functools.partial(
_raw_progress_bar,
size=size,
initial_progress=initial_progress,
)
return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size)
else:
return iter # no-op, when passed an iterator
def get_install_progress_renderer(
*, bar_type: BarType, total: int
) -> ProgressRenderer[InstallRequirement]:
"""Get an object that can be used to render the install progress.
Returns a callable, that takes an iterable to "wrap".
"""
if bar_type == "on":
return functools.partial(_rich_install_progress_bar, total=total)
else:
return iter

View file

@ -1,23 +1,21 @@
"""Contains the RequirementCommand base class.
"""Contains the Command base classes that depend on PipSession.
This class is in a separate module so the commands that do not always
need PackageFinder capability don't unnecessarily import the
The classes in this module are in a separate module so the commands not
needing download / PackageFinder capability don't unnecessarily import the
PackageFinder machinery and all its vendored dependencies, etc.
"""
from __future__ import annotations
import logging
import os
import sys
from functools import partial
from optparse import Values
from typing import Any, Callable, TypeVar
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
from pip._internal.build_env import SubprocessBuildEnvironmentInstaller
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.index_command import IndexGroupCommand
from pip._internal.cli.index_command import SessionCommandMixin as SessionCommandMixin
from pip._internal.cli.base_command import Command
from pip._internal.cli.command_context import CommandContextMixIn
from pip._internal.exceptions import CommandError, PreviousBuildDirError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
@ -32,27 +30,162 @@ from pip._internal.req.constructors import (
install_req_from_parsed_requirement,
install_req_from_req_string,
)
from pip._internal.req.req_dependency_group import parse_dependency_groups
from pip._internal.req.req_file import parse_requirements
from pip._internal.req.req_install import InstallRequirement
from pip._internal.resolution.base import BaseResolver
from pip._internal.self_outdated_check import pip_self_version_check
from pip._internal.utils.temp_dir import (
TempDirectory,
TempDirectoryTypeRegistry,
tempdir_kinds,
)
from pip._internal.utils.virtualenv import running_under_virtualenv
if TYPE_CHECKING:
from ssl import SSLContext
logger = logging.getLogger(__name__)
def should_ignore_regular_constraints(options: Values) -> bool:
def _create_truststore_ssl_context() -> Optional["SSLContext"]:
if sys.version_info < (3, 10):
raise CommandError("The truststore feature is only available for Python 3.10+")
try:
import ssl
except ImportError:
logger.warning("Disabling truststore since ssl support is missing")
return None
try:
from pip._vendor import truststore
except ImportError as e:
raise CommandError(f"The truststore feature is unavailable: {e}")
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
class SessionCommandMixin(CommandContextMixIn):
"""
Check if regular constraints should be ignored because
we are in a isolated build process and build constraints
feature is enabled but no build constraints were passed.
A class mixin for command classes needing _build_session().
"""
return os.environ.get("_PIP_IN_BUILD_IGNORE_CONSTRAINTS") == "1"
def __init__(self) -> None:
super().__init__()
self._session: Optional[PipSession] = None
@classmethod
def _get_index_urls(cls, options: Values) -> Optional[List[str]]:
"""Return a list of index urls from user-provided options."""
index_urls = []
if not getattr(options, "no_index", False):
url = getattr(options, "index_url", None)
if url:
index_urls.append(url)
urls = getattr(options, "extra_index_urls", None)
if urls:
index_urls.extend(urls)
# Return None rather than an empty list
return index_urls or None
def get_default_session(self, options: Values) -> PipSession:
"""Get a default-managed session."""
if self._session is None:
self._session = self.enter_context(self._build_session(options))
# there's no type annotation on requests.Session, so it's
# automatically ContextManager[Any] and self._session becomes Any,
# then https://github.com/python/mypy/issues/7696 kicks in
assert self._session is not None
return self._session
def _build_session(
self,
options: Values,
retries: Optional[int] = None,
timeout: Optional[int] = None,
fallback_to_certifi: bool = False,
) -> PipSession:
cache_dir = options.cache_dir
assert not cache_dir or os.path.isabs(cache_dir)
if "truststore" in options.features_enabled:
try:
ssl_context = _create_truststore_ssl_context()
except Exception:
if not fallback_to_certifi:
raise
ssl_context = None
else:
ssl_context = None
session = PipSession(
cache=os.path.join(cache_dir, "http-v2") if cache_dir else None,
retries=retries if retries is not None else options.retries,
trusted_hosts=options.trusted_hosts,
index_urls=self._get_index_urls(options),
ssl_context=ssl_context,
)
# Handle custom ca-bundles from the user
if options.cert:
session.verify = options.cert
# Handle SSL client certificate
if options.client_cert:
session.cert = options.client_cert
# Handle timeouts
if options.timeout or timeout:
session.timeout = timeout if timeout is not None else options.timeout
# Handle configured proxies
if options.proxy:
session.proxies = {
"http": options.proxy,
"https": options.proxy,
}
# Determine if we can prompt the user for authentication or not
session.auth.prompting = not options.no_input
session.auth.keyring_provider = options.keyring_provider
return session
class IndexGroupCommand(Command, SessionCommandMixin):
"""
Abstract base class for commands with the index_group options.
This also corresponds to the commands that permit the pip version check.
"""
def handle_pip_version_check(self, options: Values) -> None:
"""
Do the pip version check if not disabled.
This overrides the default behavior of not doing the check.
"""
# Make sure the index_group options are present.
assert hasattr(options, "no_index")
if options.disable_pip_version_check or options.no_index:
return
# Otherwise, check if we're using the latest version of pip available.
session = self._build_session(
options,
retries=0,
timeout=min(5, options.timeout),
# This is set to ensure the function does not fail when truststore is
# specified in use-feature but cannot be loaded. This usually raises a
# CommandError and shows a nice user-facing error, but this function is not
# called in that try-except block.
fallback_to_certifi=True,
)
with session:
pip_self_version_check(session, options)
KEEPABLE_TEMPDIR_TYPES = [
@ -62,12 +195,37 @@ KEEPABLE_TEMPDIR_TYPES = [
]
_CommandT = TypeVar("_CommandT", bound="RequirementCommand")
def warn_if_run_as_root() -> None:
"""Output a warning for sudo users on Unix.
In a virtual environment, sudo pip still writes to virtualenv.
On Windows, users may run pip as Administrator without issues.
This warning only applies to Unix root users outside of virtualenv.
"""
if running_under_virtualenv():
return
if not hasattr(os, "getuid"):
return
# On Windows, there are no "system managed" Python packages. Installing as
# Administrator via pip is the correct way of updating system environments.
#
# We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform
# checks: https://mypy.readthedocs.io/en/stable/common_issues.html
if sys.platform == "win32" or sys.platform == "cygwin":
return
if os.getuid() != 0:
return
logger.warning(
"Running pip as the 'root' user can result in broken permissions and "
"conflicting behaviour with the system package manager. "
"It is recommended to use a virtual environment instead: "
"https://pip.pypa.io/warnings/venv"
)
def with_cleanup(
func: Callable[[_CommandT, Values, list[str]], int],
) -> Callable[[_CommandT, Values, list[str]], int]:
def with_cleanup(func: Any) -> Any:
"""Decorator for common logic related to managing temporary
directories.
"""
@ -76,7 +234,9 @@ def with_cleanup(
for t in KEEPABLE_TEMPDIR_TYPES:
registry.set_delete(t, False)
def wrapper(self: _CommandT, options: Values, args: list[str]) -> int:
def wrapper(
self: RequirementCommand, options: Values, args: List[Any]
) -> Optional[int]:
assert self.tempdir_registry is not None
if options.no_clean:
configure_tempdir_registry(self.tempdir_registry)
@ -97,7 +257,6 @@ class RequirementCommand(IndexGroupCommand):
def __init__(self, *args: Any, **kw: Any) -> None:
super().__init__(*args, **kw)
self.cmd_opts.add_option(cmdoptions.dependency_groups())
self.cmd_opts.add_option(cmdoptions.no_clean())
@staticmethod
@ -117,7 +276,7 @@ class RequirementCommand(IndexGroupCommand):
session: PipSession,
finder: PackageFinder,
use_user_site: bool,
download_dir: str | None = None,
download_dir: Optional[str] = None,
verbosity: int = 0,
) -> RequirementPreparer:
"""
@ -146,22 +305,11 @@ class RequirementCommand(IndexGroupCommand):
"fast-deps has no effect when used with the legacy resolver."
)
# Handle build constraints
build_constraints = getattr(options, "build_constraints", [])
build_constraint_feature_enabled = (
"build-constraint" in options.features_enabled
)
return RequirementPreparer(
build_dir=temp_build_dir_path,
src_dir=options.src_dir,
download_dir=download_dir,
build_isolation=options.build_isolation,
build_isolation_installer=SubprocessBuildEnvironmentInstaller(
finder,
build_constraints=build_constraints,
build_constraint_feature_enabled=build_constraint_feature_enabled,
),
check_build_deps=options.check_build_deps,
build_tracker=build_tracker,
session=session,
@ -172,7 +320,6 @@ class RequirementCommand(IndexGroupCommand):
lazy_wheel=lazy_wheel,
verbosity=verbosity,
legacy_resolver=legacy_resolver,
resume_retries=options.resume_retries,
)
@classmethod
@ -181,13 +328,14 @@ class RequirementCommand(IndexGroupCommand):
preparer: RequirementPreparer,
finder: PackageFinder,
options: Values,
wheel_cache: WheelCache | None = None,
wheel_cache: Optional[WheelCache] = None,
use_user_site: bool = False,
ignore_installed: bool = True,
ignore_requires_python: bool = False,
force_reinstall: bool = False,
upgrade_strategy: str = "to-satisfy-only",
py_version_info: tuple[int, ...] | None = None,
use_pep517: Optional[bool] = None,
py_version_info: Optional[Tuple[int, ...]] = None,
) -> BaseResolver:
"""
Create a Resolver instance for the given parameters.
@ -195,6 +343,7 @@ class RequirementCommand(IndexGroupCommand):
make_install_req = partial(
install_req_from_req_string,
isolated=options.isolated_mode,
use_pep517=use_pep517,
)
resolver_variant = cls.determine_resolver_variant(options)
# The long import name and duplicated invocation is needed to convince
@ -234,56 +383,47 @@ class RequirementCommand(IndexGroupCommand):
def get_requirements(
self,
args: list[str],
args: List[str],
options: Values,
finder: PackageFinder,
session: PipSession,
) -> list[InstallRequirement]:
) -> List[InstallRequirement]:
"""
Parse command-line arguments into the corresponding requirements.
"""
requirements: list[InstallRequirement] = []
if not should_ignore_regular_constraints(options):
for filename in options.constraints:
for parsed_req in parse_requirements(
filename,
constraint=True,
finder=finder,
options=options,
session=session,
):
req_to_add = install_req_from_parsed_requirement(
parsed_req,
isolated=options.isolated_mode,
user_supplied=False,
)
requirements.append(req_to_add)
requirements: List[InstallRequirement] = []
for filename in options.constraints:
for parsed_req in parse_requirements(
filename,
constraint=True,
finder=finder,
options=options,
session=session,
):
req_to_add = install_req_from_parsed_requirement(
parsed_req,
isolated=options.isolated_mode,
user_supplied=False,
)
requirements.append(req_to_add)
for req in args:
req_to_add = install_req_from_line(
req,
comes_from=None,
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
user_supplied=True,
config_settings=getattr(options, "config_settings", None),
)
requirements.append(req_to_add)
if options.dependency_groups:
for req in parse_dependency_groups(options.dependency_groups):
req_to_add = install_req_from_req_string(
req,
isolated=options.isolated_mode,
user_supplied=True,
)
requirements.append(req_to_add)
for req in options.editables:
req_to_add = install_req_from_editable(
req,
user_supplied=True,
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
config_settings=getattr(options, "config_settings", None),
)
requirements.append(req_to_add)
@ -296,12 +436,11 @@ class RequirementCommand(IndexGroupCommand):
req_to_add = install_req_from_parsed_requirement(
parsed_req,
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
user_supplied=True,
config_settings=(
parsed_req.options.get("config_settings")
if parsed_req.options
else None
),
config_settings=parsed_req.options.get("config_settings")
if parsed_req.options
else None,
)
requirements.append(req_to_add)
@ -309,12 +448,7 @@ class RequirementCommand(IndexGroupCommand):
if any(req.has_hash_options for req in requirements):
options.require_hashes = True
if not (
args
or options.editables
or options.requirements
or options.dependency_groups
):
if not (args or options.editables or options.requirements):
opts = {"name": self.name}
if options.find_links:
raise CommandError(
@ -346,8 +480,8 @@ class RequirementCommand(IndexGroupCommand):
self,
options: Values,
session: PipSession,
target_python: TargetPython | None = None,
ignore_requires_python: bool | None = None,
target_python: Optional[TargetPython] = None,
ignore_requires_python: Optional[bool] = None,
) -> PackageFinder:
"""
Create a package finder appropriate to this requirement command.

View file

@ -1,31 +1,15 @@
from __future__ import annotations
import contextlib
import itertools
import logging
import sys
import time
from collections.abc import Generator
from typing import IO, Final
from pip._vendor.rich.console import (
Console,
ConsoleOptions,
RenderableType,
RenderResult,
)
from pip._vendor.rich.live import Live
from pip._vendor.rich.measure import Measurement
from pip._vendor.rich.text import Text
from typing import IO, Generator, Optional
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_console, get_indentation
from pip._internal.utils.logging import get_indentation
logger = logging.getLogger(__name__)
SPINNER_CHARS: Final = r"-\|/"
SPINS_PER_SECOND: Final = 8
class SpinnerInterface:
def spin(self) -> None:
@ -39,10 +23,10 @@ class InteractiveSpinner(SpinnerInterface):
def __init__(
self,
message: str,
file: IO[str] | None = None,
spin_chars: str = SPINNER_CHARS,
file: Optional[IO[str]] = None,
spin_chars: str = "-\\|/",
# Empirically, 8 updates/second looks nice
min_update_interval_seconds: float = 1 / SPINS_PER_SECOND,
min_update_interval_seconds: float = 0.125,
):
self._message = message
if file is None:
@ -152,66 +136,6 @@ def open_spinner(message: str) -> Generator[SpinnerInterface, None, None]:
spinner.finish("done")
class _PipRichSpinner:
"""
Custom rich spinner that matches the style of the legacy spinners.
(*) Updates will be handled in a background thread by a rich live panel
which will call render() automatically at the appropriate time.
"""
def __init__(self, label: str) -> None:
self.label = label
self._spin_cycle = itertools.cycle(SPINNER_CHARS)
self._spinner_text = ""
self._finished = False
self._indent = get_indentation() * " "
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
yield self.render()
def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
text = self.render()
return Measurement.get(console, options, text)
def render(self) -> RenderableType:
if not self._finished:
self._spinner_text = next(self._spin_cycle)
return Text.assemble(self._indent, self.label, " ... ", self._spinner_text)
def finish(self, status: str) -> None:
"""Stop spinning and set a final status message."""
self._spinner_text = status
self._finished = True
@contextlib.contextmanager
def open_rich_spinner(label: str, console: Console | None = None) -> Generator[None]:
if not logger.isEnabledFor(logging.INFO):
# Don't show spinner if --quiet is given.
yield
return
console = console or get_console()
spinner = _PipRichSpinner(label)
with Live(spinner, refresh_per_second=SPINS_PER_SECOND, console=console):
try:
yield
except KeyboardInterrupt:
spinner.finish("canceled")
raise
except Exception:
spinner.finish("error")
raise
else:
spinner.finish("done")
HIDE_CURSOR = "\x1b[?25l"
SHOW_CURSOR = "\x1b[?25h"

View file

@ -2,11 +2,9 @@
Package containing all pip commands
"""
from __future__ import annotations
import importlib
from collections import namedtuple
from typing import Any
from typing import Any, Dict, Optional
from pip._internal.cli.base_command import Command
@ -19,17 +17,12 @@ CommandInfo = namedtuple("CommandInfo", "module_path, class_name, summary")
# Even though the module path starts with the same "pip._internal.commands"
# prefix, the full path makes testing easier (specifically when modifying
# `commands_dict` in test setup / teardown).
commands_dict: dict[str, CommandInfo] = {
commands_dict: Dict[str, CommandInfo] = {
"install": CommandInfo(
"pip._internal.commands.install",
"InstallCommand",
"Install packages.",
),
"lock": CommandInfo(
"pip._internal.commands.lock",
"LockCommand",
"Generate a lock file.",
),
"download": CommandInfo(
"pip._internal.commands.download",
"DownloadCommand",
@ -125,7 +118,7 @@ def create_command(name: str, **kwargs: Any) -> Command:
return command
def get_similar_commands(name: str) -> str | None:
def get_similar_commands(name: str) -> Optional[str]:
"""Command name auto-correct."""
from difflib import get_close_matches

View file

@ -1,14 +1,13 @@
import os
import textwrap
from optparse import Values
from typing import Callable
from typing import Any, List
from pip._internal.cli.base_command import Command
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__)
@ -49,8 +48,8 @@ class CacheCommand(Command):
self.parser.insert_option_group(0, self.cmd_opts)
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
return {
def run(self, options: Values, args: List[str]) -> int:
handlers = {
"dir": self.get_cache_dir,
"info": self.get_cache_info,
"list": self.list_cache_items,
@ -58,18 +57,15 @@ class CacheCommand(Command):
"purge": self.purge_cache,
}
def run(self, options: Values, args: list[str]) -> int:
handler_map = self.handler_map()
if not options.cache_dir:
logger.error("pip cache commands can not function since cache is disabled.")
return ERROR
# Determine action
if not args or args[0] not in handler_map:
if not args or args[0] not in handlers:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handler_map)),
", ".join(sorted(handlers)),
)
return ERROR
@ -77,20 +73,20 @@ class CacheCommand(Command):
# Error handling happens here, not in the action-handlers.
try:
handler_map[action](options, args[1:])
handlers[action](options, args[1:])
except PipError as e:
logger.error(e.args[0])
return ERROR
return SUCCESS
def get_cache_dir(self, options: Values, args: list[str]) -> None:
def get_cache_dir(self, options: Values, args: List[Any]) -> None:
if args:
raise CommandError("Too many arguments")
logger.info(options.cache_dir)
def get_cache_info(self, options: Values, args: list[str]) -> None:
def get_cache_info(self, options: Values, args: List[Any]) -> None:
if args:
raise CommandError("Too many arguments")
@ -132,7 +128,7 @@ class CacheCommand(Command):
logger.info(message)
def list_cache_items(self, options: Values, args: list[str]) -> None:
def list_cache_items(self, options: Values, args: List[Any]) -> None:
if len(args) > 1:
raise CommandError("Too many arguments")
@ -147,7 +143,7 @@ class CacheCommand(Command):
else:
self.format_for_abspath(files)
def format_for_human(self, files: list[str]) -> None:
def format_for_human(self, files: List[str]) -> None:
if not files:
logger.info("No locally built wheels cached.")
return
@ -160,11 +156,11 @@ class CacheCommand(Command):
logger.info("Cache contents:\n")
logger.info("\n".join(sorted(results)))
def format_for_abspath(self, files: list[str]) -> None:
def format_for_abspath(self, files: List[str]) -> None:
if files:
logger.info("\n".join(sorted(files)))
def remove_cache_items(self, options: Values, args: list[str]) -> None:
def remove_cache_items(self, options: Values, args: List[Any]) -> None:
if len(args) > 1:
raise CommandError("Too many arguments")
@ -184,14 +180,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 (%s)", len(files), format_size(bytes_removed))
logger.info("Files removed: %s", len(files))
def purge_cache(self, options: Values, args: list[str]) -> None:
def purge_cache(self, options: Values, args: List[Any]) -> None:
if args:
raise CommandError("Too many arguments")
@ -200,14 +194,14 @@ class CacheCommand(Command):
def _cache_dir(self, options: Values, subdir: str) -> str:
return os.path.join(options.cache_dir, subdir)
def _find_http_files(self, options: Values) -> list[str]:
def _find_http_files(self, options: Values) -> List[str]:
old_http_dir = self._cache_dir(options, "http")
new_http_dir = self._cache_dir(options, "http-v2")
return filesystem.find_files(old_http_dir, "*") + filesystem.find_files(
new_http_dir, "*"
)
def _find_wheels(self, options: Values, pattern: str) -> list[str]:
def _find_wheels(self, options: Values, pattern: str) -> List[str]:
wheel_dir = self._cache_dir(options, "wheels")
# The wheel filename format, as specified in PEP 427, is:

View file

@ -1,15 +1,14 @@
import logging
from optparse import Values
from typing import List
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.metadata import get_default_environment
from pip._internal.operations.check import (
check_package_set,
check_unsupported,
create_package_set_from_installed,
warn_legacy_versions_and_specifiers,
)
from pip._internal.utils.compatibility_tags import get_supported
from pip._internal.utils.misc import write_output
logger = logging.getLogger(__name__)
@ -18,19 +17,13 @@ logger = logging.getLogger(__name__)
class CheckCommand(Command):
"""Verify installed packages have compatible dependencies."""
ignore_require_venv = True
usage = """
%prog [options]"""
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
package_set, parsing_probs = create_package_set_from_installed()
warn_legacy_versions_and_specifiers(package_set)
missing, conflicting = check_package_set(package_set)
unsupported = list(
check_unsupported(
get_default_environment().iter_installed_distributions(),
get_supported(),
)
)
for project_name in missing:
version = package_set[project_name].version
@ -53,13 +46,8 @@ class CheckCommand(Command):
dep_name,
dep_version,
)
for package in unsupported:
write_output(
"%s %s is not supported on this platform",
package.raw_name,
package.version,
)
if missing or conflicting or parsing_probs or unsupported:
if missing or conflicting or parsing_probs:
return ERROR
else:
write_output("No broken requirements found.")

View file

@ -1,6 +1,7 @@
import sys
import textwrap
from optparse import Values
from typing import List
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import SUCCESS
@ -37,18 +38,12 @@ COMPLETION_SCRIPTS = {
""",
"fish": """
function __fish_complete_pip
set -lx COMP_WORDS \\
(commandline --current-process --tokenize --cut-at-cursor) \\
(commandline --current-token --cut-at-cursor)
set -lx COMP_CWORD (math (count $COMP_WORDS) - 1)
set -lx COMP_WORDS (commandline -o) ""
set -lx COMP_CWORD ( \\
math (contains -i -- (commandline -t) $COMP_WORDS)-1 \\
)
set -lx PIP_AUTO_COMPLETE 1
set -l completions
if string match -q '2.*' $version
set completions (eval $COMP_WORDS[1])
else
set completions ($COMP_WORDS[1])
end
string split \\ -- $completions
string split \\ -- (eval $COMP_WORDS[1])
end
complete -fa "(__fish_complete_pip)" -c {prog}
""",
@ -118,7 +113,7 @@ class CompletionCommand(Command):
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
"""Prints the completion code of the given shell"""
shells = COMPLETION_SCRIPTS.keys()
shell_options = ["--" + shell for shell in sorted(shells)]

View file

@ -1,10 +1,8 @@
from __future__ import annotations
import logging
import os
import subprocess
from optparse import Values
from typing import Any, Callable
from typing import Any, List, Optional
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
@ -95,8 +93,8 @@ class ConfigurationCommand(Command):
self.parser.insert_option_group(0, self.cmd_opts)
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
return {
def run(self, options: Values, args: List[str]) -> int:
handlers = {
"list": self.list_values,
"edit": self.open_in_editor,
"get": self.get_name,
@ -105,14 +103,11 @@ class ConfigurationCommand(Command):
"debug": self.list_config_values,
}
def run(self, options: Values, args: list[str]) -> int:
handler_map = self.handler_map()
# Determine action
if not args or args[0] not in handler_map:
if not args or args[0] not in handlers:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handler_map)),
", ".join(sorted(handlers)),
)
return ERROR
@ -136,14 +131,14 @@ class ConfigurationCommand(Command):
# Error handling happens here, not in the action-handlers.
try:
handler_map[action](options, args[1:])
handlers[action](options, args[1:])
except PipError as e:
logger.error(e.args[0])
return ERROR
return SUCCESS
def _determine_file(self, options: Values, need_value: bool) -> Kind | None:
def _determine_file(self, options: Values, need_value: bool) -> Optional[Kind]:
file_options = [
key
for key, value in (
@ -173,32 +168,31 @@ class ConfigurationCommand(Command):
"(--user, --site, --global) to perform."
)
def list_values(self, options: Values, args: list[str]) -> None:
def list_values(self, options: Values, args: List[str]) -> None:
self._get_n_args(args, "list", n=0)
for key, value in sorted(self.configuration.items()):
for key, value in sorted(value.items()):
write_output("%s=%r", key, value)
write_output("%s=%r", key, value)
def get_name(self, options: Values, args: list[str]) -> None:
def get_name(self, options: Values, args: List[str]) -> None:
key = self._get_n_args(args, "get [name]", n=1)
value = self.configuration.get_value(key)
write_output("%s", value)
def set_name_value(self, options: Values, args: list[str]) -> None:
def set_name_value(self, options: Values, args: List[str]) -> None:
key, value = self._get_n_args(args, "set [name] [value]", n=2)
self.configuration.set_value(key, value)
self._save_configuration()
def unset_name(self, options: Values, args: list[str]) -> None:
def unset_name(self, options: Values, args: List[str]) -> None:
key = self._get_n_args(args, "unset [name]", n=1)
self.configuration.unset_value(key)
self._save_configuration()
def list_config_values(self, options: Values, args: list[str]) -> None:
def list_config_values(self, options: Values, args: List[str]) -> None:
"""List config key-value pairs across different config files"""
self._get_n_args(args, "debug", n=0)
@ -212,15 +206,13 @@ class ConfigurationCommand(Command):
file_exists = os.path.exists(fname)
write_output("%s, exists: %r", fname, file_exists)
if file_exists:
self.print_config_file_values(variant, fname)
self.print_config_file_values(variant)
def print_config_file_values(self, variant: Kind, fname: str) -> None:
def print_config_file_values(self, variant: Kind) -> None:
"""Get key-value pairs from the file of a variant"""
for name, value in self.configuration.get_values_in_config(variant).items():
with indent_log():
if name == fname:
for confname, confvalue in value.items():
write_output("%s: %s", confname, confvalue)
write_output("%s: %s", name, value)
def print_env_var_values(self) -> None:
"""Get key-values pairs present as environment variables"""
@ -230,7 +222,7 @@ class ConfigurationCommand(Command):
env_var = f"PIP_{key.upper()}"
write_output("%s=%r", env_var, value)
def open_in_editor(self, options: Values, args: list[str]) -> None:
def open_in_editor(self, options: Values, args: List[str]) -> None:
editor = self._determine_editor(options)
fname = self.configuration.get_file_to_edit()
@ -252,7 +244,7 @@ class ConfigurationCommand(Command):
except subprocess.CalledProcessError as e:
raise PipError(f"Editor Subprocess exited with exit code {e.returncode}")
def _get_n_args(self, args: list[str], example: str, n: int) -> Any:
def _get_n_args(self, args: List[str], example: str, n: int) -> Any:
"""Helper to make sure the command got the right number of arguments"""
if len(args) != n:
msg = (

View file

@ -1,12 +1,11 @@
from __future__ import annotations
import importlib.resources
import locale
import logging
import os
import sys
from optparse import Values
from types import ModuleType
from typing import Any
from typing import Any, Dict, List, Optional
import pip._vendor
from pip._vendor.certifi import where
@ -18,7 +17,6 @@ from pip._internal.cli.cmdoptions import make_target_python
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.configuration import Configuration
from pip._internal.metadata import get_environment
from pip._internal.utils.compat import open_text_resource
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import get_pip_version
@ -36,8 +34,8 @@ def show_sys_implementation() -> None:
show_value("name", implementation_name)
def create_vendor_txt_map() -> dict[str, str]:
with open_text_resource("pip._vendor", "vendor.txt") as f:
def create_vendor_txt_map() -> Dict[str, str]:
with importlib.resources.open_text("pip._vendor", "vendor.txt") as f:
# Purge non version specifying lines.
# Also, remove any space prefix or suffixes (including comments).
lines = [
@ -48,7 +46,7 @@ def create_vendor_txt_map() -> dict[str, str]:
return dict(line.split("==", 1) for line in lines)
def get_module_from_module_name(module_name: str) -> ModuleType | None:
def get_module_from_module_name(module_name: str) -> Optional[ModuleType]:
# Module name can be uppercase in vendor.txt for some reason...
module_name = module_name.lower().replace("-", "_")
# PATCH: setuptools is actually only pkg_resources.
@ -66,7 +64,7 @@ def get_module_from_module_name(module_name: str) -> ModuleType | None:
raise
def get_vendor_version_from_module(module_name: str) -> str | None:
def get_vendor_version_from_module(module_name: str) -> Optional[str]:
module = get_module_from_module_name(module_name)
version = getattr(module, "__version__", None)
@ -81,7 +79,7 @@ def get_vendor_version_from_module(module_name: str) -> str | None:
return version
def show_actual_vendor_versions(vendor_txt_versions: dict[str, str]) -> None:
def show_actual_vendor_versions(vendor_txt_versions: Dict[str, str]) -> None:
"""Log the actual version and print extra info if there is
a conflict or if the actual version could not be imported.
"""
@ -171,7 +169,7 @@ class DebugCommand(Command):
self.parser.insert_option_group(0, self.cmd_opts)
self.parser.config.load()
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
logger.warning(
"This command is only meant for debugging. "
"Do not use this with automation for parsing and getting these "

View file

@ -1,12 +1,14 @@
import logging
import os
from optparse import Values
from typing import List
from pip._internal.cli import cmdoptions
from pip._internal.cli.cmdoptions import make_target_python
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.req.req_install import check_legacy_setup_py_options
from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
from pip._internal.utils.temp_dir import TempDirectory
@ -35,9 +37,9 @@ class DownloadCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.constraints())
self.cmd_opts.add_option(cmdoptions.build_constraints())
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
self.cmd_opts.add_option(cmdoptions.prefer_binary())
@ -47,6 +49,7 @@ class DownloadCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.progress_bar())
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
self.cmd_opts.add_option(cmdoptions.check_build_deps())
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
@ -72,14 +75,13 @@ class DownloadCommand(RequirementCommand):
self.parser.insert_option_group(0, self.cmd_opts)
@with_cleanup
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
options.ignore_installed = True
# editable doesn't really make sense for `pip download`, but the bowels
# of the RequirementSet code require that property.
options.editables = []
cmdoptions.check_dist_restriction(options)
cmdoptions.check_build_constraints(options)
options.download_dir = normalize_path(options.download_dir)
ensure_dir(options.download_dir)
@ -103,6 +105,7 @@ class DownloadCommand(RequirementCommand):
)
reqs = self.get_requirements(args, options, finder, session)
check_legacy_setup_py_options(options, reqs)
preparer = self.make_requirement_preparer(
temp_build_dir=directory,
@ -120,6 +123,7 @@ class DownloadCommand(RequirementCommand):
finder=finder,
options=options,
ignore_requires_python=options.ignore_requires_python,
use_pep517=options.use_pep517,
py_version_info=options.python_version,
)
@ -127,15 +131,16 @@ class DownloadCommand(RequirementCommand):
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
downloaded: list[str] = []
downloaded: List[str] = []
for req in requirement_set.requirements.values():
if req.satisfied_by is None:
assert req.name is not None
preparer.save_linked_requirement(req)
downloaded.append(req.name)
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
requirement_set.warn_legacy_versions_and_specifiers()
if downloaded:
write_output("Successfully downloaded %s", " ".join(downloaded))

View file

@ -1,5 +1,6 @@
import sys
from optparse import Values
from typing import AbstractSet, List
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
@ -12,11 +13,12 @@ def _should_suppress_build_backends() -> bool:
return sys.version_info < (3, 12)
def _dev_pkgs() -> set[str]:
def _dev_pkgs() -> AbstractSet[str]:
pkgs = {"pip"}
if _should_suppress_build_backends():
pkgs |= {"setuptools", "distribute", "wheel"}
pkgs |= {"setuptools", "distribute", "wheel", "pkg-resources"}
return pkgs
@ -28,9 +30,9 @@ class FreezeCommand(Command):
packages are listed in a case-insensitive sorted order.
"""
ignore_require_venv = True
usage = """
%prog [options]"""
log_streams = ("ext://sys.stderr", "ext://sys.stderr")
def add_options(self) -> None:
self.cmd_opts.add_option(
@ -84,7 +86,7 @@ class FreezeCommand(Command):
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
skip = set(stdlib_pkgs)
if not options.freeze_all:
skip.update(_dev_pkgs())

View file

@ -2,6 +2,7 @@ import hashlib
import logging
import sys
from optparse import Values
from typing import List
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
@ -36,7 +37,7 @@ class HashCommand(Command):
)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
if not args:
self.parser.print_usage(sys.stderr)
return ERROR

View file

@ -1,4 +1,5 @@
from optparse import Values
from typing import List
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import SUCCESS
@ -12,7 +13,7 @@ class HelpCommand(Command):
%prog <command>"""
ignore_require_venv = True
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
from pip._internal.commands import (
commands_dict,
create_command,

View file

@ -1,20 +1,13 @@
from __future__ import annotations
import json
import logging
from collections.abc import Iterable
from optparse import Values
from typing import Any, Callable
from typing import Any, Iterable, List, Optional, Union
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import LegacyVersion, Version
from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import IndexGroupCommand
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.commands.search import (
get_installed_distribution,
print_dist_installation_info,
)
from pip._internal.commands.search import print_dist_installation_info
from pip._internal.exceptions import CommandError, DistributionNotFound, PipError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
@ -41,7 +34,6 @@ class IndexCommand(IndexGroupCommand):
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.json())
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
@ -53,19 +45,22 @@ class IndexCommand(IndexGroupCommand):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
return {
def run(self, options: Values, args: List[str]) -> int:
handlers = {
"versions": self.get_available_package_versions,
}
def run(self, options: Values, args: list[str]) -> int:
handler_map = self.handler_map()
logger.warning(
"pip index is currently an experimental command. "
"It may be removed/changed in a future release "
"without prior warning."
)
# Determine action
if not args or args[0] not in handler_map:
if not args or args[0] not in handlers:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handler_map)),
", ".join(sorted(handlers)),
)
return ERROR
@ -73,7 +68,7 @@ class IndexCommand(IndexGroupCommand):
# Error handling happens here, not in the action-handlers.
try:
handler_map[action](options, args[1:])
handlers[action](options, args[1:])
except PipError as e:
logger.error(e.args[0])
return ERROR
@ -84,8 +79,8 @@ class IndexCommand(IndexGroupCommand):
self,
options: Values,
session: PipSession,
target_python: TargetPython | None = None,
ignore_requires_python: bool | None = None,
target_python: Optional[TargetPython] = None,
ignore_requires_python: Optional[bool] = None,
) -> PackageFinder:
"""
Create a package finder appropriate to the index command.
@ -105,7 +100,7 @@ class IndexCommand(IndexGroupCommand):
target_python=target_python,
)
def get_available_package_versions(self, options: Values, args: list[Any]) -> None:
def get_available_package_versions(self, options: Values, args: List[Any]) -> None:
if len(args) != 1:
raise CommandError("You need to specify exactly one argument")
@ -120,7 +115,7 @@ class IndexCommand(IndexGroupCommand):
ignore_requires_python=options.ignore_requires_python,
)
versions: Iterable[Version] = (
versions: Iterable[Union[LegacyVersion, Version]] = (
candidate.version for candidate in finder.find_all_candidates(query)
)
@ -139,21 +134,6 @@ class IndexCommand(IndexGroupCommand):
formatted_versions = [str(ver) for ver in sorted(versions, reverse=True)]
latest = formatted_versions[0]
dist = get_installed_distribution(query)
if options.json:
structured_output = {
"name": query,
"versions": formatted_versions,
"latest": latest,
}
if dist is not None:
structured_output["installed_version"] = str(dist.version)
write_output(json.dumps(structured_output))
else:
write_output(f"{query} ({latest})")
write_output("Available versions: {}".format(", ".join(formatted_versions)))
print_dist_installation_info(latest, dist)
write_output(f"{query} ({latest})")
write_output("Available versions: {}".format(", ".join(formatted_versions)))
print_dist_installation_info(query, latest)

View file

@ -1,13 +1,13 @@
import logging
from optparse import Values
from typing import Any
from typing import Any, Dict, List
from pip._vendor.packaging.markers import default_environment
from pip._vendor.rich import print_json
from pip import __version__
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
from pip._internal.cli.req_command import Command
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.metadata import BaseDistribution, get_environment
from pip._internal.utils.compat import stdlib_pkgs
@ -45,7 +45,7 @@ class InspectCommand(Command):
self.cmd_opts.add_option(cmdoptions.list_path())
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
cmdoptions.check_list_path_option(options)
dists = get_environment(options.path).iter_installed_distributions(
local_only=options.local,
@ -62,8 +62,8 @@ class InspectCommand(Command):
print_json(data=output)
return SUCCESS
def _dist_to_dict(self, dist: BaseDistribution) -> dict[str, Any]:
res: dict[str, Any] = {
def _dist_to_dict(self, dist: BaseDistribution) -> Dict[str, Any]:
res: Dict[str, Any] = {
"metadata": dist.metadata_dict,
"metadata_location": dist.info_location,
}

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import errno
import json
import operator
@ -7,32 +5,20 @@ import os
import shutil
import site
from optparse import SUPPRESS_HELP, Values
from pathlib import Path
from typing import List, Optional
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.requests.exceptions import InvalidProxyURL
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
from pip._internal.cli.req_command import (
RequirementCommand,
warn_if_run_as_root,
with_cleanup,
)
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.exceptions import (
CommandError,
InstallationError,
InstallWheelBuildError,
)
from pip._internal.exceptions import CommandError, InstallationError
from pip._internal.locations import get_scheme
from pip._internal.metadata import get_environment
from pip._internal.models.installation_report import InstallationReport
@ -41,6 +27,7 @@ from pip._internal.operations.check import ConflictDetails, check_install_confli
from pip._internal.req import install_given_reqs
from pip._internal.req.req_install import (
InstallRequirement,
check_legacy_setup_py_options,
)
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.filesystem import test_writable_dir
@ -50,7 +37,6 @@ from pip._internal.utils.misc import (
ensure_dir,
get_pip_version,
protect_pip_from_modification_on_windows,
warn_if_run_as_root,
write_output,
)
from pip._internal.utils.temp_dir import TempDirectory
@ -58,7 +44,7 @@ from pip._internal.utils.virtualenv import (
running_under_virtualenv,
virtualenv_no_global,
)
from pip._internal.wheel_builder import build
from pip._internal.wheel_builder import build, should_build_for_install_command
logger = getLogger(__name__)
@ -86,7 +72,6 @@ class InstallCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.constraints())
self.cmd_opts.add_option(cmdoptions.build_constraints())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.pre())
@ -210,10 +195,12 @@ class InstallCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
self.cmd_opts.add_option(cmdoptions.check_build_deps())
self.cmd_opts.add_option(cmdoptions.override_externally_managed())
self.cmd_opts.add_option(cmdoptions.config_settings())
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(
"--compile",
@ -276,7 +263,7 @@ class InstallCommand(RequirementCommand):
)
@with_cleanup
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
if options.use_user_site and options.target_dir is not None:
raise CommandError("Can not combine '--user' and '--target'")
@ -301,7 +288,6 @@ class InstallCommand(RequirementCommand):
if options.upgrade:
upgrade_strategy = options.upgrade_strategy
cmdoptions.check_build_constraints(options)
cmdoptions.check_dist_restriction(options, check_target=True)
logger.verbose("Using %s", get_pip_version())
@ -313,8 +299,8 @@ class InstallCommand(RequirementCommand):
isolated_mode=options.isolated_mode,
)
target_temp_dir: TempDirectory | None = None
target_temp_dir_path: str | None = None
target_temp_dir: Optional[TempDirectory] = None
target_temp_dir_path: Optional[str] = None
if options.target_dir:
options.ignore_installed = True
options.target_dir = os.path.abspath(options.target_dir)
@ -333,6 +319,8 @@ class InstallCommand(RequirementCommand):
target_temp_dir_path = target_temp_dir.path
self.enter_context(target_temp_dir)
global_options = options.global_options or []
session = self.get_default_session(options)
target_python = make_target_python(options)
@ -352,6 +340,7 @@ class InstallCommand(RequirementCommand):
try:
reqs = self.get_requirements(args, options, finder, session)
check_legacy_setup_py_options(options, reqs)
wheel_cache = WheelCache(options.cache_dir)
@ -380,7 +369,7 @@ class InstallCommand(RequirementCommand):
ignore_requires_python=options.ignore_requires_python,
force_reinstall=options.force_reinstall,
upgrade_strategy=upgrade_strategy,
py_version_info=options.python_version,
use_pep517=options.use_pep517,
)
self.trace_basic_info(finder)
@ -398,6 +387,9 @@ class InstallCommand(RequirementCommand):
json.dump(report.to_dict(), f, indent=2, ensure_ascii=False)
if options.dry_run:
# In non dry-run mode, the legacy versions and specifiers check
# will be done as part of conflict detection.
requirement_set.warn_legacy_versions_and_specifiers()
would_install_items = sorted(
(r.metadata["name"], r.metadata["version"])
for r in requirement_set.requirements_to_install
@ -409,13 +401,6 @@ class InstallCommand(RequirementCommand):
)
return SUCCESS
# If there is any more preparation to do for the actual installation, do
# so now. This includes actually downloading the files in the case that
# we have been using PEP-658 metadata so far.
preparer.prepare_linked_requirements_more(
requirement_set.requirements.values()
)
try:
pip_req = requirement_set.get_requirement("pip")
except KeyError:
@ -427,22 +412,31 @@ class InstallCommand(RequirementCommand):
protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
reqs_to_build = [
r for r in requirement_set.requirements_to_install if not r.is_wheel
r
for r in requirement_set.requirements.values()
if should_build_for_install_command(r)
]
_, build_failures = build(
reqs_to_build,
wheel_cache=wheel_cache,
verify=True,
build_options=[],
global_options=global_options,
)
if build_failures:
raise InstallWheelBuildError(build_failures)
raise InstallationError(
"Could not build wheels for {}, which is required to "
"install pyproject.toml-based projects".format(
", ".join(r.name for r in build_failures) # type: ignore
)
)
to_install = resolver.get_installation_order(requirement_set)
# Check for conflicts in the package set we're installing.
conflicts: ConflictDetails | None = None
conflicts: Optional[ConflictDetails] = None
should_warn_about_conflicts = (
not options.ignore_dependencies and options.warn_about_conflicts
)
@ -457,13 +451,13 @@ class InstallCommand(RequirementCommand):
installed = install_given_reqs(
to_install,
global_options,
root=options.root_path,
home=target_temp_dir_path,
prefix=options.prefix_path,
warn_script_location=warn_script_location,
use_user_site=options.use_user_site,
pycompile=options.compile,
progress_bar=options.progress_bar,
)
lib_locations = get_lib_location_guesses(
@ -475,21 +469,17 @@ class InstallCommand(RequirementCommand):
)
env = get_environment(lib_locations)
# Display a summary of installed packages, with extra care to
# display a package name as it was requested by the user.
installed.sort(key=operator.attrgetter("name"))
summary = []
installed_versions = {}
for distribution in env.iter_all_distributions():
installed_versions[distribution.canonical_name] = distribution.version
for package in installed:
display_name = package.name
version = installed_versions.get(canonicalize_name(display_name), None)
if version:
text = f"{display_name}-{version}"
else:
text = display_name
summary.append(text)
items = []
for result in installed:
item = result.name
try:
installed_dist = env.get_distribution(item)
if installed_dist is not None:
item = f"{item}-{installed_dist.version}"
except Exception:
pass
items.append(item)
if conflicts is not None:
self._warn_about_conflicts(
@ -497,7 +487,7 @@ class InstallCommand(RequirementCommand):
resolver_variant=self.determine_resolver_variant(options),
)
installed_desc = " ".join(summary)
installed_desc = " ".join(items)
if installed_desc:
write_output(
"Successfully installed %s",
@ -579,8 +569,8 @@ class InstallCommand(RequirementCommand):
shutil.move(os.path.join(lib_dir, item), target_item_dir)
def _determine_conflicts(
self, to_install: list[InstallRequirement]
) -> ConflictDetails | None:
self, to_install: List[InstallRequirement]
) -> Optional[ConflictDetails]:
try:
return check_install_conflicts(to_install)
except Exception:
@ -597,7 +587,7 @@ class InstallCommand(RequirementCommand):
if not missing and not conflicting:
return
parts: list[str] = []
parts: List[str] = []
if resolver_variant == "legacy":
parts.append(
"pip's legacy dependency resolver does not consider dependency "
@ -643,11 +633,11 @@ class InstallCommand(RequirementCommand):
def get_lib_location_guesses(
user: bool = False,
home: str | None = None,
root: str | None = None,
home: Optional[str] = None,
root: Optional[str] = None,
isolated: bool = False,
prefix: str | None = None,
) -> list[str]:
prefix: Optional[str] = None,
) -> List[str]:
scheme = get_scheme(
"",
user=user,
@ -659,7 +649,7 @@ def get_lib_location_guesses(
return [scheme.purelib, scheme.platlib]
def site_packages_writable(root: str | None, isolated: bool) -> bool:
def site_packages_writable(root: Optional[str], isolated: bool) -> bool:
return all(
test_writable_dir(d)
for d in set(get_lib_location_guesses(root=root, isolated=isolated))
@ -667,10 +657,10 @@ def site_packages_writable(root: str | None, isolated: bool) -> bool:
def decide_user_install(
use_user_site: bool | None,
prefix_path: str | None = None,
target_dir: str | None = None,
root_path: str | None = None,
use_user_site: Optional[bool],
prefix_path: Optional[str] = None,
target_dir: Optional[str] = None,
root_path: Optional[str] = None,
isolated_mode: bool = False,
) -> bool:
"""Determine whether to do a user install based on the input options.
@ -687,7 +677,6 @@ def decide_user_install(
logger.debug("Non-user install by explicit request")
return False
# If we have been asked for a user install explicitly, check compatibility.
if use_user_site:
if prefix_path:
raise CommandError(
@ -699,13 +688,6 @@ def decide_user_install(
"Can not perform a '--user' install. User site-packages "
"are not visible in this virtualenv."
)
# Catch all remaining cases which honour the site.ENABLE_USER_SITE
# value, such as a plain Python installation (e.g. no virtualenv).
if not site.ENABLE_USER_SITE:
raise InstallationError(
"Can not perform a '--user' install. User site-packages "
"are disabled for this Python."
)
logger.debug("User install by explicit request")
return True
@ -773,31 +755,20 @@ def create_os_error_message(
parts.append(permissions_part)
parts.append(".\n")
# Suggest to check "pip config debug" in case of invalid proxy
if type(error) is InvalidProxyURL:
# Suggest the user to enable Long Paths if path length is
# more than 260
if (
WINDOWS
and error.errno == errno.ENOENT
and error.filename
and len(error.filename) > 260
):
parts.append(
'Consider checking your local proxy configuration with "pip config debug"'
"HINT: This error might have occurred since "
"this system does not have Windows Long Path "
"support enabled. You can find information on "
"how to enable this at "
"https://pip.pypa.io/warnings/enable-long-paths\n"
)
parts.append(".\n")
# On Windows, errors like EINVAL or ENOENT may occur
# if a file or folder name exceeds 255 characters,
# or if the full path exceeds 260 characters and long path support isn't enabled.
# This condition checks for such cases and adds a hint to the error output.
if WINDOWS and error.errno in (errno.EINVAL, errno.ENOENT) and error.filename:
if any(len(part) > 255 for part in Path(error.filename).parts):
parts.append(
"HINT: This error might be caused by a file or folder name exceeding "
"255 characters, which is a Windows limitation even if long paths "
"are enabled.\n "
)
if len(error.filename) > 260:
parts.append(
"HINT: This error might have occurred since "
"this system does not have Windows Long Path "
"support enabled. You can find information on "
"how to enable this at "
"https://pip.pypa.io/warnings/enable-long-paths\n"
)
return "".join(parts).strip() + "\n"

View file

@ -1,27 +1,24 @@
from __future__ import annotations
import json
import logging
from collections.abc import Generator, Sequence
from email.parser import Parser
from optparse import Values
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, Generator, List, Optional, Sequence, Tuple, cast
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import InvalidVersion, Version
from pip._internal.cli import cmdoptions
from pip._internal.cli.index_command import IndexGroupCommand
from pip._internal.cli.req_command import IndexGroupCommand
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import CommandError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution, get_environment
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.network.session import PipSession
from pip._internal.utils.compat import stdlib_pkgs
from pip._internal.utils.misc import tabulate, write_output
if TYPE_CHECKING:
from pip._internal.index.package_finder import PackageFinder
from pip._internal.network.session import PipSession
from pip._internal.metadata.base import DistributionVersion
class _DistWithLatestInfo(BaseDistribution):
"""Give the distribution object a couple of extra fields.
@ -30,12 +27,14 @@ if TYPE_CHECKING:
makes the rest of the code much cleaner.
"""
latest_version: Version
latest_version: DistributionVersion
latest_filetype: str
_ProcessedDists = Sequence[_DistWithLatestInfo]
from pip._vendor.packaging.version import parse
logger = logging.getLogger(__name__)
@ -129,7 +128,7 @@ class ListCommand(IndexGroupCommand):
"--include-editable",
action="store_true",
dest="include_editable",
help="Include editable package in output.",
help="Include editable package from output.",
default=True,
)
self.cmd_opts.add_option(cmdoptions.list_exclude())
@ -138,20 +137,12 @@ class ListCommand(IndexGroupCommand):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
def handle_pip_version_check(self, options: Values) -> None:
if options.outdated or options.uptodate:
super().handle_pip_version_check(options)
def _build_package_finder(
self, options: Values, session: PipSession
) -> PackageFinder:
"""
Create a package finder appropriate to this list command.
"""
# Lazy import the heavy index modules as most list invocations won't need 'em.
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
link_collector = LinkCollector.create(session, options=options)
# Pass allow_yanked=False to ignore yanked versions.
@ -165,7 +156,7 @@ class ListCommand(IndexGroupCommand):
selection_prefs=selection_prefs,
)
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
if options.outdated and options.uptodate:
raise CommandError("Options --outdated and --uptodate cannot be combined.")
@ -180,7 +171,7 @@ class ListCommand(IndexGroupCommand):
if options.excludes:
skip.update(canonicalize_name(n) for n in options.excludes)
packages: _ProcessedDists = [
packages: "_ProcessedDists" = [
cast("_DistWithLatestInfo", d)
for d in get_environment(options.path).iter_installed_distributions(
local_only=options.local,
@ -207,26 +198,26 @@ class ListCommand(IndexGroupCommand):
return SUCCESS
def get_outdated(
self, packages: _ProcessedDists, options: Values
) -> _ProcessedDists:
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
return [
dist
for dist in self.iter_packages_latest_infos(packages, options)
if dist.latest_version > dist.version
if parse(str(dist.latest_version)) > parse(str(dist.version))
]
def get_uptodate(
self, packages: _ProcessedDists, options: Values
) -> _ProcessedDists:
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
return [
dist
for dist in self.iter_packages_latest_infos(packages, options)
if dist.latest_version == dist.version
if parse(str(dist.latest_version)) == parse(str(dist.version))
]
def get_not_required(
self, packages: _ProcessedDists, options: Values
) -> _ProcessedDists:
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
dep_keys = {
canonicalize_name(dep.name)
for dist in packages
@ -239,14 +230,14 @@ class ListCommand(IndexGroupCommand):
return list({pkg for pkg in packages if pkg.canonical_name not in dep_keys})
def iter_packages_latest_infos(
self, packages: _ProcessedDists, options: Values
) -> Generator[_DistWithLatestInfo, None, None]:
self, packages: "_ProcessedDists", options: Values
) -> Generator["_DistWithLatestInfo", None, None]:
with self._build_session(options) as session:
finder = self._build_package_finder(options, session)
def latest_info(
dist: _DistWithLatestInfo,
) -> _DistWithLatestInfo | None:
dist: "_DistWithLatestInfo",
) -> Optional["_DistWithLatestInfo"]:
all_candidates = finder.find_all_candidates(dist.canonical_name)
if not options.pre:
# Remove prereleases
@ -277,7 +268,7 @@ class ListCommand(IndexGroupCommand):
yield dist
def output_package_listing(
self, packages: _ProcessedDists, options: Values
self, packages: "_ProcessedDists", options: Values
) -> None:
packages = sorted(
packages,
@ -288,19 +279,17 @@ class ListCommand(IndexGroupCommand):
self.output_package_listing_columns(data, header)
elif options.list_format == "freeze":
for dist in packages:
try:
req_string = f"{dist.raw_name}=={dist.version}"
except InvalidVersion:
req_string = f"{dist.raw_name}==={dist.raw_version}"
if options.verbose >= 1:
write_output("%s (%s)", req_string, dist.location)
write_output(
"%s==%s (%s)", dist.raw_name, dist.version, dist.location
)
else:
write_output(req_string)
write_output("%s==%s", dist.raw_name, dist.version)
elif options.list_format == "json":
write_output(format_for_json(packages, options))
def output_package_listing_columns(
self, data: list[list[str]], header: list[str]
self, data: List[List[str]], header: List[str]
) -> None:
# insert the header first: we need to know the size of column names
if len(data) > 0:
@ -317,8 +306,8 @@ class ListCommand(IndexGroupCommand):
def format_for_columns(
pkgs: _ProcessedDists, options: Values
) -> tuple[list[list[str]], list[str]]:
pkgs: "_ProcessedDists", options: Values
) -> Tuple[List[List[str]], List[str]]:
"""
Convert the package data into something usable
by output_package_listing_columns.
@ -329,40 +318,25 @@ def format_for_columns(
if running_outdated:
header.extend(["Latest", "Type"])
def wheel_build_tag(dist: BaseDistribution) -> str | None:
try:
wheel_file = dist.read_text("WHEEL")
except FileNotFoundError:
return None
return Parser().parsestr(wheel_file).get("Build")
build_tags = [wheel_build_tag(p) for p in pkgs]
has_build_tags = any(build_tags)
if has_build_tags:
header.append("Build")
has_editables = any(x.editable for x in pkgs)
if has_editables:
header.append("Editable project location")
if options.verbose >= 1:
header.append("Location")
if options.verbose >= 1:
header.append("Installer")
has_editables = any(x.editable for x in pkgs)
if has_editables:
header.append("Editable project location")
data = []
for i, proj in enumerate(pkgs):
for proj in pkgs:
# if we're working on the 'outdated' list, separate out the
# latest_version and type
row = [proj.raw_name, proj.raw_version]
row = [proj.raw_name, str(proj.version)]
if running_outdated:
row.append(str(proj.latest_version))
row.append(proj.latest_filetype)
if has_build_tags:
row.append(build_tags[i] or "")
if has_editables:
row.append(proj.editable_project_location or "")
@ -376,16 +350,12 @@ def format_for_columns(
return data, header
def format_for_json(packages: _ProcessedDists, options: Values) -> str:
def format_for_json(packages: "_ProcessedDists", options: Values) -> str:
data = []
for dist in packages:
try:
version = str(dist.version)
except InvalidVersion:
version = dist.raw_version
info = {
"name": dist.raw_name,
"version": version,
"version": str(dist.version),
}
if options.verbose >= 1:
info["location"] = dist.location or ""

View file

@ -1,167 +0,0 @@
import sys
from optparse import Values
from pathlib import Path
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import (
RequirementCommand,
with_cleanup,
)
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.models.pylock import Pylock, is_valid_pylock_file_name
from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.utils.logging import getLogger
from pip._internal.utils.misc import (
get_pip_version,
)
from pip._internal.utils.temp_dir import TempDirectory
logger = getLogger(__name__)
class LockCommand(RequirementCommand):
"""
EXPERIMENTAL - Lock packages and their dependencies from:
- PyPI (and other indexes) using requirement specifiers.
- VCS project urls.
- Local project directories.
- Local or remote source archives.
pip also supports locking from "requirements files", which provide an easy
way to specify a whole environment to be installed.
The generated lock file is only guaranteed to be valid for the current
python version and platform.
"""
usage = """
%prog [options] [-e] <local project path> ...
%prog [options] <requirement specifier> [package-index-options] ...
%prog [options] -r <requirements file> [package-index-options] ...
%prog [options] <archive url/path> ..."""
def add_options(self) -> None:
self.cmd_opts.add_option(
cmdoptions.PipOption(
"--output",
"-o",
dest="output_file",
metavar="path",
type="path",
default="pylock.toml",
help="Lock file name (default=pylock.toml). Use - for stdout.",
)
)
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.constraints())
self.cmd_opts.add_option(cmdoptions.build_constraints())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.editable())
self.cmd_opts.add_option(cmdoptions.src())
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.check_build_deps())
self.cmd_opts.add_option(cmdoptions.config_settings())
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
self.cmd_opts.add_option(cmdoptions.prefer_binary())
self.cmd_opts.add_option(cmdoptions.require_hashes())
self.cmd_opts.add_option(cmdoptions.progress_bar())
index_opts = cmdoptions.make_option_group(
cmdoptions.index_group,
self.parser,
)
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
@with_cleanup
def run(self, options: Values, args: list[str]) -> int:
logger.verbose("Using %s", get_pip_version())
logger.warning(
"pip lock is currently an experimental command. "
"It may be removed/changed in a future release "
"without prior warning."
)
cmdoptions.check_build_constraints(options)
session = self.get_default_session(options)
finder = self._build_package_finder(
options=options,
session=session,
ignore_requires_python=options.ignore_requires_python,
)
build_tracker = self.enter_context(get_build_tracker())
directory = TempDirectory(
delete=not options.no_clean,
kind="install",
globally_managed=True,
)
reqs = self.get_requirements(args, options, finder, session)
wheel_cache = WheelCache(options.cache_dir)
# Only when installing is it permitted to use PEP 660.
# In other circumstances (pip wheel, pip download) we generate
# regular (i.e. non editable) metadata and wheels.
for req in reqs:
req.permit_editable_wheels = True
preparer = self.make_requirement_preparer(
temp_build_dir=directory,
options=options,
build_tracker=build_tracker,
session=session,
finder=finder,
use_user_site=False,
verbosity=self.verbosity,
)
resolver = self.make_resolver(
preparer=preparer,
finder=finder,
options=options,
wheel_cache=wheel_cache,
use_user_site=False,
ignore_installed=True,
ignore_requires_python=options.ignore_requires_python,
upgrade_strategy="to-satisfy-only",
)
self.trace_basic_info(finder)
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
if options.output_file == "-":
base_dir = Path.cwd()
else:
output_file_path = Path(options.output_file)
if not is_valid_pylock_file_name(output_file_path):
logger.warning(
"%s is not a valid lock file name.",
output_file_path,
)
base_dir = output_file_path.parent
pylock_toml = Pylock.from_install_requirements(
requirement_set.requirements.values(), base_dir=base_dir
).as_toml()
if options.output_file == "-":
sys.stdout.write(pylock_toml)
else:
output_file_path.write_text(pylock_toml, encoding="utf-8")
return SUCCESS

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import logging
import shutil
import sys
@ -7,7 +5,7 @@ import textwrap
import xmlrpc.client
from collections import OrderedDict
from optparse import Values
from typing import TypedDict
from typing import TYPE_CHECKING, Dict, List, Optional
from pip._vendor.packaging.version import parse as parse_version
@ -16,17 +14,18 @@ from pip._internal.cli.req_command import SessionCommandMixin
from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS
from pip._internal.exceptions import CommandError
from pip._internal.metadata import get_default_environment
from pip._internal.metadata.base import BaseDistribution
from pip._internal.models.index import PyPI
from pip._internal.network.xmlrpc import PipXmlrpcTransport
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import write_output
if TYPE_CHECKING:
from typing import TypedDict
class TransformedHit(TypedDict):
name: str
summary: str
versions: list[str]
class TransformedHit(TypedDict):
name: str
summary: str
versions: List[str]
logger = logging.getLogger(__name__)
@ -51,7 +50,7 @@ class SearchCommand(Command, SessionCommandMixin):
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
if not args:
raise CommandError("Missing required argument (search query).")
query = args
@ -67,7 +66,7 @@ class SearchCommand(Command, SessionCommandMixin):
return SUCCESS
return NO_MATCHES_FOUND
def search(self, query: list[str], options: Values) -> list[dict[str, str]]:
def search(self, query: List[str], options: Values) -> List[Dict[str, str]]:
index_url = options.index
session = self.get_default_session(options)
@ -77,21 +76,22 @@ class SearchCommand(Command, SessionCommandMixin):
try:
hits = pypi.search({"name": query, "summary": query}, "or")
except xmlrpc.client.Fault as fault:
message = (
f"XMLRPC request failed [code: {fault.faultCode}]\n{fault.faultString}"
message = "XMLRPC request failed [code: {code}]\n{string}".format(
code=fault.faultCode,
string=fault.faultString,
)
raise CommandError(message)
assert isinstance(hits, list)
return hits
def transform_hits(hits: list[dict[str, str]]) -> list[TransformedHit]:
def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]:
"""
The list from pypi is really a list of versions. We want a list of
packages with the list of versions stored inline. This converts the
list from pypi into one we can use.
"""
packages: dict[str, TransformedHit] = OrderedDict()
packages: Dict[str, "TransformedHit"] = OrderedDict()
for hit in hits:
name = hit["name"]
summary = hit["summary"]
@ -113,7 +113,9 @@ def transform_hits(hits: list[dict[str, str]]) -> list[TransformedHit]:
return list(packages.values())
def print_dist_installation_info(latest: str, dist: BaseDistribution | None) -> None:
def print_dist_installation_info(name: str, latest: str) -> None:
env = get_default_environment()
dist = env.get_distribution(name)
if dist is not None:
with indent_log():
if dist.version == latest:
@ -130,15 +132,10 @@ def print_dist_installation_info(latest: str, dist: BaseDistribution | None) ->
write_output("LATEST: %s", latest)
def get_installed_distribution(name: str) -> BaseDistribution | None:
env = get_default_environment()
return env.get_distribution(name)
def print_results(
hits: list[TransformedHit],
name_column_width: int | None = None,
terminal_width: int | None = None,
hits: List["TransformedHit"],
name_column_width: Optional[int] = None,
terminal_width: Optional[int] = None,
) -> None:
if not hits:
return
@ -168,11 +165,10 @@ def print_results(
line = f"{name_latest:{name_column_width}} - {summary}"
try:
write_output(line)
dist = get_installed_distribution(name)
print_dist_installation_info(latest, dist)
print_dist_installation_info(name, latest)
except UnicodeEncodeError:
pass
def highest_version(versions: list[str]) -> str:
def highest_version(versions: List[str]) -> str:
return max(versions, key=parse_version)

View file

@ -1,12 +1,7 @@
from __future__ import annotations
import logging
import string
from collections.abc import Generator, Iterable, Iterator
from optparse import Values
from typing import NamedTuple
from typing import Generator, Iterable, Iterator, List, NamedTuple, Optional
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.cli.base_command import Command
@ -17,13 +12,6 @@ from pip._internal.utils.misc import write_output
logger = logging.getLogger(__name__)
def normalize_project_url_label(label: str) -> str:
# This logic is from PEP 753 (Well-known Project URLs in Metadata).
chars_to_remove = string.punctuation + string.whitespace
removal_map = str.maketrans("", "", chars_to_remove)
return label.translate(removal_map).lower()
class ShowCommand(Command):
"""
Show information about one or more installed packages.
@ -47,7 +35,7 @@ class ShowCommand(Command):
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
if not args:
logger.warning("ERROR: Please provide a package name or names.")
return ERROR
@ -65,24 +53,23 @@ class _PackageInfo(NamedTuple):
name: str
version: str
location: str
editable_project_location: str | None
requires: list[str]
required_by: list[str]
editable_project_location: Optional[str]
requires: List[str]
required_by: List[str]
installer: str
metadata_version: str
classifiers: list[str]
classifiers: List[str]
summary: str
homepage: str
project_urls: list[str]
project_urls: List[str]
author: str
author_email: str
license: str
license_expression: str
entry_points: list[str]
files: list[str] | None
entry_points: List[str]
files: Optional[List[str]]
def search_packages_info(query: list[str]) -> Generator[_PackageInfo, None, None]:
def search_packages_info(query: List[str]) -> Generator[_PackageInfo, None, None]:
"""
Gather details from installed distributions. Print distribution name,
version, location, and installed files. Installed files requires a
@ -113,19 +100,8 @@ def search_packages_info(query: list[str]) -> Generator[_PackageInfo, None, None
except KeyError:
continue
try:
requires = sorted(
# Avoid duplicates in requirements (e.g. due to environment markers).
{req.name for req in dist.iter_dependencies()},
key=str.lower,
)
except InvalidRequirement:
requires = sorted(dist.iter_raw_dependencies(), key=str.lower)
try:
required_by = sorted(_get_requiring_packages(dist), key=str.lower)
except InvalidRequirement:
required_by = ["#N/A"]
requires = sorted((req.name for req in dist.iter_dependencies()), key=str.lower)
required_by = sorted(_get_requiring_packages(dist), key=str.lower)
try:
entry_points_text = dist.read_text("entry_points.txt")
@ -135,27 +111,15 @@ def search_packages_info(query: list[str]) -> Generator[_PackageInfo, None, None
files_iter = dist.iter_declared_entries()
if files_iter is None:
files: list[str] | None = None
files: Optional[List[str]] = None
else:
files = sorted(files_iter)
metadata = dist.metadata
project_urls = metadata.get_all("Project-URL", [])
homepage = metadata.get("Home-page", "")
if not homepage:
# It's common that there is a "homepage" Project-URL, but Home-page
# remains unset (especially as PEP 621 doesn't surface the field).
for url in project_urls:
url_label, url = url.split(",", maxsplit=1)
normalized_label = normalize_project_url_label(url_label)
if normalized_label == "homepage":
homepage = url.strip()
break
yield _PackageInfo(
name=dist.raw_name,
version=dist.raw_version,
version=str(dist.version),
location=dist.location or "",
editable_project_location=dist.editable_project_location,
requires=requires,
@ -164,12 +128,11 @@ def search_packages_info(query: list[str]) -> Generator[_PackageInfo, None, None
metadata_version=dist.metadata_version or "",
classifiers=metadata.get_all("Classifier", []),
summary=metadata.get("Summary", ""),
homepage=homepage,
project_urls=project_urls,
homepage=metadata.get("Home-page", ""),
project_urls=metadata.get_all("Project-URL", []),
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,
)
@ -189,18 +152,13 @@ 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)
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("License: %s", dist.license)
write_output("Location: %s", dist.location)
if dist.editable_project_location is not None:
write_output(

View file

@ -1,11 +1,12 @@
import logging
from optparse import Values
from typing import List
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
from pip._internal.cli.index_command import SessionCommandMixin
from pip._internal.cli.req_command import SessionCommandMixin, warn_if_run_as_root
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import InstallationError
from pip._internal.req import parse_requirements
@ -16,7 +17,6 @@ from pip._internal.req.constructors import (
from pip._internal.utils.misc import (
check_externally_managed,
protect_pip_from_modification_on_windows,
warn_if_run_as_root,
)
logger = logging.getLogger(__name__)
@ -61,7 +61,7 @@ class UninstallCommand(Command, SessionCommandMixin):
self.cmd_opts.add_option(cmdoptions.override_externally_managed())
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
session = self.get_default_session(options)
reqs_to_uninstall = {}

View file

@ -2,6 +2,7 @@ import logging
import os
import shutil
from optparse import Values
from typing import List
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
@ -11,10 +12,11 @@ from pip._internal.exceptions import CommandError
from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.req.req_install import (
InstallRequirement,
check_legacy_setup_py_options,
)
from pip._internal.utils.misc import ensure_dir, normalize_path
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.wheel_builder import build
from pip._internal.wheel_builder import build, should_build_for_wheel_command
logger = logging.getLogger(__name__)
@ -56,9 +58,9 @@ class WheelCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.prefer_binary())
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
self.cmd_opts.add_option(cmdoptions.check_build_deps())
self.cmd_opts.add_option(cmdoptions.constraints())
self.cmd_opts.add_option(cmdoptions.build_constraints())
self.cmd_opts.add_option(cmdoptions.editable())
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.src())
@ -75,6 +77,8 @@ class WheelCommand(RequirementCommand):
)
self.cmd_opts.add_option(cmdoptions.config_settings())
self.cmd_opts.add_option(cmdoptions.build_options())
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(
"--pre",
@ -97,9 +101,7 @@ class WheelCommand(RequirementCommand):
self.parser.insert_option_group(0, self.cmd_opts)
@with_cleanup
def run(self, options: Values, args: list[str]) -> int:
cmdoptions.check_build_constraints(options)
def run(self, options: Values, args: List[str]) -> int:
session = self.get_default_session(options)
finder = self._build_package_finder(options, session)
@ -116,6 +118,7 @@ class WheelCommand(RequirementCommand):
)
reqs = self.get_requirements(args, options, finder, session)
check_legacy_setup_py_options(options, reqs)
wheel_cache = WheelCache(options.cache_dir)
@ -136,26 +139,30 @@ class WheelCommand(RequirementCommand):
options=options,
wheel_cache=wheel_cache,
ignore_requires_python=options.ignore_requires_python,
use_pep517=options.use_pep517,
)
self.trace_basic_info(finder)
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
reqs_to_build: list[InstallRequirement] = []
reqs_to_build: List[InstallRequirement] = []
for req in requirement_set.requirements.values():
if req.is_wheel:
preparer.save_linked_requirement(req)
else:
elif should_build_for_wheel_command(req):
reqs_to_build.append(req)
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
requirement_set.warn_legacy_versions_and_specifiers()
# build wheels
build_successes, build_failures = build(
reqs_to_build,
wheel_cache=wheel_cache,
verify=(not options.no_verify),
build_options=options.build_options or [],
global_options=options.global_options or [],
)
for req in build_successes:
assert req.link and req.link.is_wheel

View file

@ -11,14 +11,11 @@ Some terminology:
A single word describing where the configuration key-value pair came from
"""
from __future__ import annotations
import configparser
import locale
import os
import sys
from collections.abc import Iterable
from typing import Any, NewType
from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple
from pip._internal.exceptions import (
ConfigurationError,
@ -53,11 +50,12 @@ logger = getLogger(__name__)
def _normalize_name(name: str) -> str:
"""Make a name consistent regardless of source (environment or file)"""
name = name.lower().replace("_", "-")
name = name.removeprefix("--") # only prefer long opts
if name.startswith("--"):
name = name[2:] # only prefer long opts
return name
def _disassemble_key(name: str) -> list[str]:
def _disassemble_key(name: str) -> List[str]:
if "." not in name:
error_message = (
"Key does not contain dot separated section and key. "
@ -67,7 +65,7 @@ def _disassemble_key(name: str) -> list[str]:
return name.split(".", 1)
def get_configuration_files() -> dict[Kind, list[str]]:
def get_configuration_files() -> Dict[Kind, List[str]]:
global_config_files = [
os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip")
]
@ -100,7 +98,7 @@ class Configuration:
and the data stored is also nice.
"""
def __init__(self, isolated: bool, load_only: Kind | None = None) -> None:
def __init__(self, isolated: bool, load_only: Optional[Kind] = None) -> None:
super().__init__()
if load_only is not None and load_only not in VALID_LOAD_ONLY:
@ -113,13 +111,13 @@ class Configuration:
self.load_only = load_only
# Because we keep track of where we got the data from
self._parsers: dict[Kind, list[tuple[str, RawConfigParser]]] = {
self._parsers: Dict[Kind, List[Tuple[str, RawConfigParser]]] = {
variant: [] for variant in OVERRIDE_ORDER
}
self._config: dict[Kind, dict[str, dict[str, Any]]] = {
self._config: Dict[Kind, Dict[str, Any]] = {
variant: {} for variant in OVERRIDE_ORDER
}
self._modified_parsers: list[tuple[str, RawConfigParser]] = []
self._modified_parsers: List[Tuple[str, RawConfigParser]] = []
def load(self) -> None:
"""Loads configuration from configuration files and environment"""
@ -127,7 +125,7 @@ class Configuration:
if not self.isolated:
self._load_environment_vars()
def get_file_to_edit(self) -> str | None:
def get_file_to_edit(self) -> Optional[str]:
"""Returns the file with highest priority in configuration"""
assert self.load_only is not None, "Need to be specified a file to be editing"
@ -136,7 +134,7 @@ class Configuration:
except IndexError:
return None
def items(self) -> Iterable[tuple[str, Any]]:
def items(self) -> Iterable[Tuple[str, Any]]:
"""Returns key-value pairs like dict.items() representing the loaded
configuration
"""
@ -147,10 +145,7 @@ class Configuration:
orig_key = key
key = _normalize_name(key)
try:
clean_config: dict[str, Any] = {}
for file_values in self._dictionary.values():
clean_config.update(file_values)
return clean_config[key]
return self._dictionary[key]
except KeyError:
# disassembling triggers a more useful error message than simply
# "No such key" in the case that the key isn't in the form command.option
@ -173,8 +168,7 @@ class Configuration:
parser.add_section(section)
parser.set(section, name, value)
self._config[self.load_only].setdefault(fname, {})
self._config[self.load_only][fname][key] = value
self._config[self.load_only][key] = value
self._mark_as_modified(fname, parser)
def unset_value(self, key: str) -> None:
@ -184,14 +178,11 @@ class Configuration:
self._ensure_have_load_only()
assert self.load_only
fname, parser = self._get_parser_to_modify()
if (
key not in self._config[self.load_only][fname]
and key not in self._config[self.load_only]
):
if key not in self._config[self.load_only]:
raise ConfigurationError(f"No such key - {orig_key}")
fname, parser = self._get_parser_to_modify()
if parser is not None:
section, name = _disassemble_key(key)
if not (
@ -206,10 +197,8 @@ class Configuration:
if not parser.items(section):
parser.remove_section(section)
self._mark_as_modified(fname, parser)
try:
del self._config[self.load_only][fname][key]
except KeyError:
del self._config[self.load_only][key]
del self._config[self.load_only][key]
def save(self) -> None:
"""Save the current in-memory state."""
@ -241,7 +230,7 @@ class Configuration:
logger.debug("Will be working with %s variant only", self.load_only)
@property
def _dictionary(self) -> dict[str, dict[str, Any]]:
def _dictionary(self) -> Dict[str, Any]:
"""A dictionary representing the loaded configuration."""
# NOTE: Dictionaries are not populated if not loaded. So, conditionals
# are not needed here.
@ -281,8 +270,7 @@ class Configuration:
for section in parser.sections():
items = parser.items(section)
self._config[variant].setdefault(fname, {})
self._config[variant][fname].update(self._normalized_keys(section, items))
self._config[variant].update(self._normalized_keys(section, items))
return parser
@ -309,14 +297,13 @@ class Configuration:
def _load_environment_vars(self) -> None:
"""Loads configuration from environment variables"""
self._config[kinds.ENV_VAR].setdefault(":env:", {})
self._config[kinds.ENV_VAR][":env:"].update(
self._config[kinds.ENV_VAR].update(
self._normalized_keys(":env:", self.get_environ_vars())
)
def _normalized_keys(
self, section: str, items: Iterable[tuple[str, Any]]
) -> dict[str, Any]:
self, section: str, items: Iterable[Tuple[str, Any]]
) -> Dict[str, Any]:
"""Normalizes items to construct a dictionary with normalized keys.
This routine is where the names become keys and are made the same
@ -328,7 +315,7 @@ class Configuration:
normalized[key] = val
return normalized
def get_environ_vars(self) -> Iterable[tuple[str, str]]:
def get_environ_vars(self) -> Iterable[Tuple[str, str]]:
"""Returns a generator with all environmental vars with prefix PIP_"""
for key, val in os.environ.items():
if key.startswith("PIP_"):
@ -337,13 +324,13 @@ class Configuration:
yield name, val
# XXX: This is patched in the tests.
def iter_config_files(self) -> Iterable[tuple[Kind, list[str]]]:
def iter_config_files(self) -> Iterable[Tuple[Kind, List[str]]]:
"""Yields variant and configuration files associated with it.
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 ergonomic to display
displayed to the user. It's probably most ergononmic to display
things in the same order as OVERRIDE_ORDER
"""
# SMELL: Move the conditions out of this function
@ -369,11 +356,11 @@ class Configuration:
else:
yield kinds.ENV, []
def get_values_in_config(self, variant: Kind) -> dict[str, Any]:
def get_values_in_config(self, variant: Kind) -> Dict[str, Any]:
"""Get values present in a config file"""
return self._config[variant]
def _get_parser_to_modify(self) -> tuple[str, RawConfigParser]:
def _get_parser_to_modify(self) -> Tuple[str, RawConfigParser]:
# Determine which parser to modify
assert self.load_only
parsers = self._parsers[self.load_only]

View file

@ -1,14 +1,10 @@
from __future__ import annotations
import abc
from typing import TYPE_CHECKING
from typing import Optional
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata.base import BaseDistribution
from pip._internal.req import InstallRequirement
if TYPE_CHECKING:
from pip._internal.build_env import BuildEnvironmentInstaller
class AbstractDistribution(metaclass=abc.ABCMeta):
"""A base class for handling installable artifacts.
@ -34,7 +30,7 @@ class AbstractDistribution(metaclass=abc.ABCMeta):
self.req = req
@abc.abstractproperty
def build_tracker_id(self) -> str | None:
def build_tracker_id(self) -> Optional[str]:
"""A string that uniquely identifies this requirement to the build tracker.
If None, then this dist has no work to do in the build tracker, and
@ -48,7 +44,7 @@ class AbstractDistribution(metaclass=abc.ABCMeta):
@abc.abstractmethod
def prepare_distribution_metadata(
self,
build_env_installer: BuildEnvironmentInstaller,
finder: PackageFinder,
build_isolation: bool,
check_build_deps: bool,
) -> None:

View file

@ -1,13 +1,9 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Optional
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution
if TYPE_CHECKING:
from pip._internal.build_env import BuildEnvironmentInstaller
class InstalledDistribution(AbstractDistribution):
"""Represents an installed package.
@ -17,7 +13,7 @@ class InstalledDistribution(AbstractDistribution):
"""
@property
def build_tracker_id(self) -> str | None:
def build_tracker_id(self) -> Optional[str]:
return None
def get_metadata_distribution(self) -> BaseDistribution:
@ -26,7 +22,7 @@ class InstalledDistribution(AbstractDistribution):
def prepare_distribution_metadata(
self,
build_env_installer: BuildEnvironmentInstaller,
finder: PackageFinder,
build_isolation: bool,
check_build_deps: bool,
) -> None:

View file

@ -1,18 +1,13 @@
from __future__ import annotations
import logging
from collections.abc import Iterable
from typing import TYPE_CHECKING
from typing import Iterable, Optional, Set, Tuple
from pip._internal.build_env import BuildEnvironment
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.exceptions import InstallationError
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution
from pip._internal.utils.subprocess import runner_with_spinner_message
if TYPE_CHECKING:
from pip._internal.build_env import BuildEnvironmentInstaller
logger = logging.getLogger(__name__)
@ -20,11 +15,11 @@ class SourceDistribution(AbstractDistribution):
"""Represents a source distribution.
The preparation step for these needs metadata for the packages to be
generated.
generated, either using PEP 517 or using the legacy `setup.py egg_info`.
"""
@property
def build_tracker_id(self) -> str | None:
def build_tracker_id(self) -> Optional[str]:
"""Identify this requirement uniquely by its link."""
assert self.req.link
return self.req.link.url_without_fragment
@ -34,31 +29,32 @@ class SourceDistribution(AbstractDistribution):
def prepare_distribution_metadata(
self,
build_env_installer: BuildEnvironmentInstaller,
finder: PackageFinder,
build_isolation: bool,
check_build_deps: bool,
) -> None:
# Load pyproject.toml
# Load pyproject.toml, to determine whether PEP 517 is to be used
self.req.load_pyproject_toml()
# Set up the build isolation, if this requirement should be isolated
if build_isolation:
should_isolate = self.req.use_pep517 and build_isolation
if should_isolate:
# Setup an isolated environment and install the build backend static
# requirements in it.
self._prepare_build_backend(build_env_installer)
# Check that the build backend supports PEP 660. This cannot be done
# earlier because we need to setup the build backend to verify it
# supports build_editable, nor can it be done later, because we want
# to avoid installing build requirements needlessly.
self.req.editable_sanity_check()
self._prepare_build_backend(finder)
# Check that if the requirement is editable, it either supports PEP 660 or
# has a setup.py or a setup.cfg. This cannot be done earlier because we need
# to setup the build backend to verify it supports build_editable, nor can
# it be done later, because we want to avoid installing build requirements
# needlessly. Doing it here also works around setuptools generating
# UNKNOWN.egg-info when running get_requires_for_build_wheel on a directory
# without setup.py nor setup.cfg.
self.req.isolated_editable_sanity_check()
# Install the dynamic build requirements.
self._install_build_reqs(build_env_installer)
else:
# When not using build isolation, we still need to check that
# the build backend supports PEP 660.
self.req.editable_sanity_check()
self._install_build_reqs(finder)
# Check if the current environment provides build dependencies
if check_build_deps:
should_check_deps = self.req.use_pep517 and check_build_deps
if should_check_deps:
pyproject_requires = self.req.pyproject_requires
assert pyproject_requires is not None
conflicting, missing = self.req.build_env.check_requirements(
@ -70,17 +66,15 @@ class SourceDistribution(AbstractDistribution):
self._raise_missing_reqs(missing)
self.req.prepare_metadata()
def _prepare_build_backend(
self, build_env_installer: BuildEnvironmentInstaller
) -> None:
def _prepare_build_backend(self, finder: PackageFinder) -> None:
# Isolate in a BuildEnvironment and install the build-time
# requirements.
pyproject_requires = self.req.pyproject_requires
assert pyproject_requires is not None
self.req.build_env = BuildEnvironment(build_env_installer)
self.req.build_env = BuildEnvironment()
self.req.build_env.install_requirements(
pyproject_requires, "overlay", kind="build dependencies", for_req=self.req
finder, pyproject_requires, "overlay", kind="build dependencies"
)
conflicting, missing = self.req.build_env.check_requirements(
self.req.requirements_to_check
@ -116,16 +110,14 @@ class SourceDistribution(AbstractDistribution):
with backend.subprocess_runner(runner):
return backend.get_requires_for_build_editable()
def _install_build_reqs(
self, build_env_installer: BuildEnvironmentInstaller
) -> None:
def _install_build_reqs(self, finder: PackageFinder) -> None:
# Install any extra build dependencies that the backend requests.
# This must be done in a second pass, as the pyproject.toml
# dependencies must be installed before we can call the backend.
if (
self.req.editable
and self.req.permit_editable_wheels
and self.req.supports_pyproject_editable
and self.req.supports_pyproject_editable()
):
build_reqs = self._get_build_requires_editable()
else:
@ -134,11 +126,11 @@ class SourceDistribution(AbstractDistribution):
if conflicting:
self._raise_conflicts("the backend dependencies", conflicting)
self.req.build_env.install_requirements(
missing, "normal", kind="backend dependencies", for_req=self.req
finder, missing, "normal", kind="backend dependencies"
)
def _raise_conflicts(
self, conflicting_with: str, conflicting_reqs: set[tuple[str, str]]
self, conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
) -> None:
format_string = (
"Some build dependencies for {requirement} "
@ -154,7 +146,7 @@ class SourceDistribution(AbstractDistribution):
)
raise InstallationError(error_message)
def _raise_missing_reqs(self, missing: set[str]) -> None:
def _raise_missing_reqs(self, missing: Set[str]) -> None:
format_string = (
"Some build dependencies for {requirement} are missing: {missing}."
)

View file

@ -1,19 +1,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Optional
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import (
BaseDistribution,
FilesystemWheel,
get_wheel_distribution,
)
if TYPE_CHECKING:
from pip._internal.build_env import BuildEnvironmentInstaller
class WheelDistribution(AbstractDistribution):
"""Represents a wheel distribution.
@ -22,7 +18,7 @@ class WheelDistribution(AbstractDistribution):
"""
@property
def build_tracker_id(self) -> str | None:
def build_tracker_id(self) -> Optional[str]:
return None
def get_metadata_distribution(self) -> BaseDistribution:
@ -37,7 +33,7 @@ class WheelDistribution(AbstractDistribution):
def prepare_distribution_metadata(
self,
build_env_installer: BuildEnvironmentInstaller,
finder: PackageFinder,
build_isolation: bool,
check_build_deps: bool,
) -> None:

View file

@ -5,8 +5,6 @@ operate. This is expected to be importable from any/all files within the
subpackage and, thus, should not depend on them.
"""
from __future__ import annotations
import configparser
import contextlib
import locale
@ -14,23 +12,19 @@ import logging
import pathlib
import re
import sys
from collections.abc import Iterator
from itertools import chain, groupby, repeat
from typing import TYPE_CHECKING, Literal
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.version import InvalidVersion
from pip._vendor.requests.models import Request, Response
from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
from pip._vendor.rich.markup import escape
from pip._vendor.rich.text import Text
if TYPE_CHECKING:
from hashlib import _Hash
from pip._vendor.requests.models import Request, Response
from typing import Literal
from pip._internal.metadata import BaseDistribution
from pip._internal.network.download import _FileDownload
from pip._internal.req.req_install import InstallRequirement
logger = logging.getLogger(__name__)
@ -44,7 +38,7 @@ def _is_kebab_case(s: str) -> bool:
def _prefix_with_indent(
s: Text | str,
s: Union[Text, str],
console: Console,
*,
prefix: str,
@ -80,13 +74,13 @@ class DiagnosticPipError(PipError):
def __init__(
self,
*,
kind: Literal["error", "warning"] = "error",
reference: str | None = None,
message: str | Text,
context: str | Text | None,
hint_stmt: str | Text | None,
note_stmt: str | Text | None = None,
link: str | None = None,
kind: 'Literal["error", "warning"]' = "error",
reference: Optional[str] = None,
message: Union[str, Text],
context: Optional[Union[str, Text]],
hint_stmt: Optional[Union[str, Text]],
note_stmt: Optional[Union[str, Text]] = None,
link: Optional[str] = None,
) -> None:
# Ensure a proper reference is provided.
if reference is None:
@ -190,21 +184,8 @@ class InstallationError(PipError):
"""General exception during installation"""
class FailedToPrepareCandidate(InstallationError):
"""Raised when we fail to prepare a candidate (i.e. fetch and generate metadata).
This is intentionally not a diagnostic error, since the output will be presented
above this error, when this occurs. This should instead present information to the
user.
"""
def __init__(
self, *, package_name: str, requirement_chain: str, failed_step: str
) -> None:
super().__init__(f"Failed to build '{package_name}' when {failed_step.lower()}")
self.package_name = package_name
self.requirement_chain = requirement_chain
self.failed_step = failed_step
class UninstallationError(PipError):
"""General exception during uninstallation"""
class MissingPyProjectBuildRequires(DiagnosticPipError):
@ -252,7 +233,7 @@ class NoneMetadataError(PipError):
def __init__(
self,
dist: BaseDistribution,
dist: "BaseDistribution",
metadata_name: str,
) -> None:
"""
@ -313,8 +294,8 @@ class NetworkConnectionError(PipError):
def __init__(
self,
error_msg: str,
response: Response | None = None,
request: Request | None = None,
response: Optional[Response] = None,
request: Optional[Request] = None,
) -> None:
"""
Initialize NetworkConnectionError with `request` and `response`
@ -363,7 +344,7 @@ class MetadataInconsistent(InstallationError):
"""
def __init__(
self, ireq: InstallRequirement, field: str, f_val: str, m_val: str
self, ireq: "InstallRequirement", field: str, f_val: str, m_val: str
) -> None:
self.ireq = ireq
self.field = field
@ -377,17 +358,6 @@ class MetadataInconsistent(InstallationError):
)
class MetadataInvalid(InstallationError):
"""Metadata is invalid."""
def __init__(self, ireq: InstallRequirement, error: str) -> None:
self.ireq = ireq
self.error = error
def __str__(self) -> str:
return f"Requested {self.ireq} has invalid metadata: {self.error}"
class InstallationSubprocessError(DiagnosticPipError, InstallationError):
"""A subprocess call failed."""
@ -398,10 +368,10 @@ class InstallationSubprocessError(DiagnosticPipError, InstallationError):
*,
command_description: str,
exit_code: int,
output_lines: list[str] | None,
output_lines: Optional[List[str]],
) -> None:
if output_lines is None:
output_prompt = Text("No available output.")
output_prompt = Text("See above for output.")
else:
output_prompt = (
Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
@ -429,7 +399,7 @@ class InstallationSubprocessError(DiagnosticPipError, InstallationError):
return f"{self.command_description} exited with {self.exit_code}"
class MetadataGenerationFailed(DiagnosticPipError, InstallationError):
class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
reference = "metadata-generation-failed"
def __init__(
@ -437,7 +407,7 @@ class MetadataGenerationFailed(DiagnosticPipError, InstallationError):
*,
package_details: str,
) -> None:
super().__init__(
super(InstallationSubprocessError, self).__init__(
message="Encountered error while generating package metadata.",
context=escape(package_details),
hint_stmt="See above for details.",
@ -452,9 +422,9 @@ class HashErrors(InstallationError):
"""Multiple HashError instances rolled into one for reporting"""
def __init__(self) -> None:
self.errors: list[HashError] = []
self.errors: List["HashError"] = []
def append(self, error: HashError) -> None:
def append(self, error: "HashError") -> None:
self.errors.append(error)
def __str__(self) -> str:
@ -488,7 +458,7 @@ class HashError(InstallationError):
"""
req: InstallRequirement | None = None
req: Optional["InstallRequirement"] = None
head = ""
order: int = -1
@ -610,7 +580,7 @@ class HashMismatch(HashError):
"someone may have tampered with them."
)
def __init__(self, allowed: dict[str, list[str]], gots: dict[str, _Hash]) -> None:
def __init__(self, allowed: Dict[str, List[str]], gots: Dict[str, "_Hash"]) -> None:
"""
:param allowed: A dict of algorithm names pointing to lists of allowed
hex digests
@ -635,12 +605,12 @@ class HashMismatch(HashError):
"""
def hash_then_or(hash_name: str) -> chain[str]:
def hash_then_or(hash_name: str) -> "chain[str]":
# For now, all the decent hashes have 6-char names, so we can get
# away with hard-coding space literals.
return chain([hash_name], repeat(" or"))
lines: list[str] = []
lines: List[str] = []
for hash_name, expecteds in self.allowed.items():
prefix = hash_then_or(hash_name)
lines.extend((f" Expected {next(prefix)} {e}") for e in expecteds)
@ -661,8 +631,8 @@ class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
def __init__(
self,
reason: str = "could not be loaded",
fname: str | None = None,
error: configparser.Error | None = None,
fname: Optional[str] = None,
error: Optional[configparser.Error] = None,
) -> None:
super().__init__(error)
self.reason = reason
@ -697,7 +667,7 @@ class ExternallyManagedEnvironment(DiagnosticPipError):
reference = "externally-managed-environment"
def __init__(self, error: str | None) -> None:
def __init__(self, error: Optional[str]) -> None:
if error is None:
context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR)
else:
@ -724,7 +694,7 @@ class ExternallyManagedEnvironment(DiagnosticPipError):
try:
category = locale.LC_MESSAGES
except AttributeError:
lang: str | None = None
lang: Optional[str] = None
else:
lang, _ = locale.getlocale(category)
if lang is not None:
@ -739,8 +709,8 @@ class ExternallyManagedEnvironment(DiagnosticPipError):
@classmethod
def from_config(
cls,
config: pathlib.Path | str,
) -> ExternallyManagedEnvironment:
config: Union[pathlib.Path, str],
) -> "ExternallyManagedEnvironment":
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read(config, encoding="utf-8")
@ -756,143 +726,3 @@ class ExternallyManagedEnvironment(DiagnosticPipError):
exc_info = logger.isEnabledFor(VERBOSE)
logger.warning("Failed to read %s", config, exc_info=exc_info)
return cls(None)
class UninstallMissingRecord(DiagnosticPipError):
reference = "uninstall-no-record-file"
def __init__(self, *, distribution: BaseDistribution) -> None:
installer = distribution.installer
if not installer or installer == "pip":
dep = f"{distribution.raw_name}=={distribution.version}"
hint = Text.assemble(
"You might be able to recover from this via: ",
(f"pip install --force-reinstall --no-deps {dep}", "green"),
)
else:
hint = Text(
f"The package was installed by {installer}. "
"You should check if it can uninstall the package."
)
super().__init__(
message=Text(f"Cannot uninstall {distribution}"),
context=(
"The package's contents are unknown: "
f"no RECORD file was found for {distribution.raw_name}."
),
hint_stmt=hint,
)
class LegacyDistutilsInstall(DiagnosticPipError):
reference = "uninstall-distutils-installed-package"
def __init__(self, *, distribution: BaseDistribution) -> None:
super().__init__(
message=Text(f"Cannot uninstall {distribution}"),
context=(
"It is a distutils installed project and thus we cannot accurately "
"determine which files belong to it which would lead to only a partial "
"uninstall."
),
hint_stmt=None,
)
class InvalidInstalledPackage(DiagnosticPipError):
reference = "invalid-installed-package"
def __init__(
self,
*,
dist: BaseDistribution,
invalid_exc: InvalidRequirement | InvalidVersion,
) -> None:
installed_location = dist.installed_location
if isinstance(invalid_exc, InvalidRequirement):
invalid_type = "requirement"
else:
invalid_type = "version"
super().__init__(
message=Text(
f"Cannot process installed package {dist} "
+ (f"in {installed_location!r} " if installed_location else "")
+ f"because it has an invalid {invalid_type}:\n{invalid_exc.args[0]}"
),
context=(
"Starting with pip 24.1, packages with invalid "
f"{invalid_type}s can not be processed."
),
hint_stmt="To proceed this package must be uninstalled.",
)
class IncompleteDownloadError(DiagnosticPipError):
"""Raised when the downloader receives fewer bytes than advertised
in the Content-Length header."""
reference = "incomplete-download"
def __init__(self, download: _FileDownload) -> None:
# Dodge circular import.
from pip._internal.utils.misc import format_size
assert download.size is not None
download_status = (
f"{format_size(download.bytes_received)}/{format_size(download.size)}"
)
if download.reattempts:
retry_status = f"after {download.reattempts + 1} attempts "
hint = "Use --resume-retries to configure resume attempt limit."
else:
# Download retrying is not enabled.
retry_status = ""
hint = "Consider using --resume-retries to enable download resumption."
message = Text(
f"Download failed {retry_status}because not enough bytes "
f"were received ({download_status})"
)
super().__init__(
message=message,
context=f"URL: {download.link.redacted_url}",
hint_stmt=hint,
note_stmt="This is an issue with network connectivity, not pip.",
)
class ResolutionTooDeepError(DiagnosticPipError):
"""Raised when the dependency resolver exceeds the maximum recursion depth."""
reference = "resolution-too-deep"
def __init__(self) -> None:
super().__init__(
message="Dependency resolution exceeded maximum depth",
context=(
"Pip cannot resolve the current dependencies as the dependency graph "
"is too complex for pip to solve efficiently."
),
hint_stmt=(
"Try adding lower bounds to constrain your dependencies, "
"for example: 'package>=2.0.0' instead of just 'package'. "
),
link="https://pip.pypa.io/en/stable/topics/dependency-resolution/#handling-resolution-too-deep-errors",
)
class InstallWheelBuildError(DiagnosticPipError):
reference = "failed-wheel-build-for-install"
def __init__(self, failed: list[InstallRequirement]) -> None:
super().__init__(
message=(
"Failed to build installable wheels for some "
"pyproject.toml based projects"
),
context=", ".join(r.name for r in failed), # type: ignore
hint_stmt=None,
)

View file

@ -1 +1,2 @@
"""Index interaction code"""
"""Index interaction code
"""

View file

@ -2,8 +2,6 @@
The main purpose of this module is to expose LinkCollector.collect_sources().
"""
from __future__ import annotations
import collections
import email.message
import functools
@ -13,14 +11,20 @@ import logging
import os
import urllib.parse
import urllib.request
from collections.abc import Iterable, MutableMapping, Sequence
from dataclasses import dataclass
from html.parser import HTMLParser
from optparse import Values
from typing import (
TYPE_CHECKING,
Callable,
Dict,
Iterable,
List,
MutableMapping,
NamedTuple,
Protocol,
Optional,
Sequence,
Tuple,
Union,
)
from pip._vendor import requests
@ -38,12 +42,17 @@ from pip._internal.vcs import vcs
from .sources import CandidatesFromPage, LinkSource, build_source
if TYPE_CHECKING:
from typing import Protocol
else:
Protocol = object
logger = logging.getLogger(__name__)
ResponseHeaders = MutableMapping[str, str]
def _match_vcs_scheme(url: str) -> str | None:
def _match_vcs_scheme(url: str) -> Optional[str]:
"""Look for VCS schemes in the URL.
Returns the matched VCS scheme, or None if there's no match.
@ -168,7 +177,7 @@ def _get_simple_response(url: str, session: PipSession) -> Response:
return resp
def _get_encoding_from_headers(headers: ResponseHeaders) -> str | None:
def _get_encoding_from_headers(headers: ResponseHeaders) -> Optional[str]:
"""Determine if we have any encoding information in our headers."""
if headers and "Content-Type" in headers:
m = email.message.Message()
@ -180,7 +189,7 @@ def _get_encoding_from_headers(headers: ResponseHeaders) -> str | None:
class CacheablePageContent:
def __init__(self, page: IndexContent) -> None:
def __init__(self, page: "IndexContent") -> None:
assert page.cache_link_parsing
self.page = page
@ -192,7 +201,8 @@ class CacheablePageContent:
class ParseLinks(Protocol):
def __call__(self, page: IndexContent) -> Iterable[Link]: ...
def __call__(self, page: "IndexContent") -> Iterable[Link]:
...
def with_cached_index_content(fn: ParseLinks) -> ParseLinks:
@ -202,12 +212,12 @@ def with_cached_index_content(fn: ParseLinks) -> ParseLinks:
`page` has `page.cache_link_parsing == False`.
"""
@functools.cache
def wrapper(cacheable_page: CacheablePageContent) -> list[Link]:
@functools.lru_cache(maxsize=None)
def wrapper(cacheable_page: CacheablePageContent) -> List[Link]:
return list(fn(cacheable_page.page))
@functools.wraps(fn)
def wrapper_wrapper(page: IndexContent) -> list[Link]:
def wrapper_wrapper(page: "IndexContent") -> List[Link]:
if page.cache_link_parsing:
return wrapper(CacheablePageContent(page))
return list(fn(page))
@ -216,7 +226,7 @@ def with_cached_index_content(fn: ParseLinks) -> ParseLinks:
@with_cached_index_content
def parse_links(page: IndexContent) -> Iterable[Link]:
def parse_links(page: "IndexContent") -> Iterable[Link]:
"""
Parse a Simple API's Index Content, and yield its anchor elements as Link objects.
"""
@ -244,22 +254,29 @@ def parse_links(page: IndexContent) -> Iterable[Link]:
yield link
@dataclass(frozen=True)
class IndexContent:
"""Represents one response (or page), along with its URL.
"""Represents one response (or page), along with its URL"""
:param encoding: the encoding to decode the given content.
:param url: the URL from which the HTML was downloaded.
:param cache_link_parsing: whether links parsed from this page's url
should be cached. PyPI index urls should
have this set to False, for example.
"""
content: bytes
content_type: str
encoding: str | None
url: str
cache_link_parsing: bool = True
def __init__(
self,
content: bytes,
content_type: str,
encoding: Optional[str],
url: str,
cache_link_parsing: bool = True,
) -> None:
"""
:param encoding: the encoding to decode the given content.
:param url: the URL from which the HTML was downloaded.
:param cache_link_parsing: whether links parsed from this page's url
should be cached. PyPI index urls should
have this set to False, for example.
"""
self.content = content
self.content_type = content_type
self.encoding = encoding
self.url = url
self.cache_link_parsing = cache_link_parsing
def __str__(self) -> str:
return redact_auth_from_url(self.url)
@ -275,10 +292,10 @@ class HTMLLinkParser(HTMLParser):
super().__init__(convert_charrefs=True)
self.url: str = url
self.base_url: str | None = None
self.anchors: list[dict[str, str | None]] = []
self.base_url: Optional[str] = None
self.anchors: List[Dict[str, Optional[str]]] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
if tag == "base" and self.base_url is None:
href = self.get_href(attrs)
if href is not None:
@ -286,7 +303,7 @@ class HTMLLinkParser(HTMLParser):
elif tag == "a":
self.anchors.append(dict(attrs))
def get_href(self, attrs: list[tuple[str, str | None]]) -> str | None:
def get_href(self, attrs: List[Tuple[str, Optional[str]]]) -> Optional[str]:
for name, value in attrs:
if name == "href":
return value
@ -295,8 +312,8 @@ class HTMLLinkParser(HTMLParser):
def _handle_get_simple_fail(
link: Link,
reason: str | Exception,
meth: Callable[..., None] | None = None,
reason: Union[str, Exception],
meth: Optional[Callable[..., None]] = None,
) -> None:
if meth is None:
meth = logger.debug
@ -316,7 +333,7 @@ def _make_index_content(
)
def _get_index_content(link: Link, *, session: PipSession) -> IndexContent | None:
def _get_index_content(link: Link, *, session: PipSession) -> Optional["IndexContent"]:
url = link.url.split("#", 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
@ -378,11 +395,12 @@ def _get_index_content(link: Link, *, session: PipSession) -> IndexContent | Non
class CollectedSources(NamedTuple):
find_links: Sequence[LinkSource | None]
index_urls: Sequence[LinkSource | None]
find_links: Sequence[Optional[LinkSource]]
index_urls: Sequence[Optional[LinkSource]]
class LinkCollector:
"""
Responsible for collecting Link objects from all configured locations,
making network requests as needed.
@ -404,7 +422,7 @@ class LinkCollector:
session: PipSession,
options: Values,
suppress_no_index: bool = False,
) -> LinkCollector:
) -> "LinkCollector":
"""
:param session: The Session to use to make requests.
:param suppress_no_index: Whether to ignore the --no-index option
@ -433,10 +451,10 @@ class LinkCollector:
return link_collector
@property
def find_links(self) -> list[str]:
def find_links(self) -> List[str]:
return self.search_scope.find_links
def fetch_response(self, location: Link) -> IndexContent | None:
def fetch_response(self, location: Link) -> Optional[IndexContent]:
"""
Fetch an HTML page containing package links.
"""

View file

@ -1,24 +1,16 @@
"""Routines related to PyPI, indexes"""
from __future__ import annotations
import enum
import functools
import itertools
import logging
import re
from collections.abc import Iterable
from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
Optional,
Union,
)
from typing import TYPE_CHECKING, FrozenSet, Iterable, List, Optional, Set, Tuple, Union
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.tags import Tag
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import InvalidVersion, _BaseVersion
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.packaging.version import parse as parse_version
from pip._internal.exceptions import (
@ -45,20 +37,20 @@ from pip._internal.utils.packaging import check_requires_python
from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
if TYPE_CHECKING:
from typing_extensions import TypeGuard
from pip._vendor.typing_extensions import TypeGuard
__all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
logger = getLogger(__name__)
BuildTag = Union[tuple[()], tuple[int, str]]
CandidateSortingKey = tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
BuildTag = Union[Tuple[()], Tuple[int, str]]
CandidateSortingKey = Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
def _check_link_requires_python(
link: Link,
version_info: tuple[int, int, int],
version_info: Tuple[int, int, int],
ignore_requires_python: bool = False,
) -> bool:
"""
@ -114,6 +106,7 @@ class LinkType(enum.Enum):
class LinkEvaluator:
"""
Responsible for evaluating links for a particular project.
"""
@ -127,11 +120,11 @@ class LinkEvaluator:
def __init__(
self,
project_name: str,
canonical_name: NormalizedName,
formats: frozenset[str],
canonical_name: str,
formats: FrozenSet[str],
target_python: TargetPython,
allow_yanked: bool,
ignore_requires_python: bool | None = None,
ignore_requires_python: Optional[bool] = None,
) -> None:
"""
:param project_name: The user supplied package name.
@ -161,7 +154,7 @@ class LinkEvaluator:
self.project_name = project_name
def evaluate_link(self, link: Link) -> tuple[LinkType, str]:
def evaluate_link(self, link: Link) -> Tuple[LinkType, str]:
"""
Determine whether a link is a candidate for installation.
@ -201,7 +194,7 @@ class LinkEvaluator:
LinkType.format_invalid,
"invalid wheel filename",
)
if wheel.name != self._canonical_name:
if canonicalize_name(wheel.name) != self._canonical_name:
reason = f"wrong project name (not {self.project_name})"
return (LinkType.different_project, reason)
@ -248,19 +241,7 @@ class LinkEvaluator:
ignore_requires_python=self._ignore_requires_python,
)
if not supports_python:
requires_python = link.requires_python
if requires_python:
def get_version_sort_key(v: str) -> tuple[int, ...]:
return tuple(int(s) for s in v.split(".") if s.isdigit())
requires_python = ",".join(
sorted(
(str(s) for s in specifiers.SpecifierSet(requires_python)),
key=get_version_sort_key,
)
)
reason = f"{version} Requires-Python {requires_python}"
reason = f"{version} Requires-Python {link.requires_python}"
return (LinkType.requires_python_mismatch, reason)
logger.debug("Found link %s, version: %s", link, version)
@ -269,10 +250,10 @@ class LinkEvaluator:
def filter_unallowed_hashes(
candidates: list[InstallationCandidate],
hashes: Hashes | None,
candidates: List[InstallationCandidate],
hashes: Optional[Hashes],
project_name: str,
) -> list[InstallationCandidate]:
) -> List[InstallationCandidate]:
"""
Filter out candidates whose hashes aren't allowed, and return a new
list of candidates.
@ -342,44 +323,67 @@ def filter_unallowed_hashes(
return filtered
@dataclass
class CandidatePreferences:
"""
Encapsulates some of the preferences for filtering and sorting
InstallationCandidate objects.
"""
prefer_binary: bool = False
allow_all_prereleases: bool = False
def __init__(
self,
prefer_binary: bool = False,
allow_all_prereleases: bool = False,
) -> None:
"""
:param allow_all_prereleases: Whether to allow all pre-releases.
"""
self.allow_all_prereleases = allow_all_prereleases
self.prefer_binary = prefer_binary
@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.
"""
all_candidates: list[InstallationCandidate]
applicable_candidates: list[InstallationCandidate]
best_candidate: InstallationCandidate | None
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)
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
if best_candidate is None:
assert not applicable_candidates
else:
assert self.best_candidate in self.applicable_candidates
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)
class CandidateEvaluator:
"""
Responsible for filtering and sorting candidates for installation based
on what tags are valid.
@ -389,12 +393,12 @@ class CandidateEvaluator:
def create(
cls,
project_name: str,
target_python: TargetPython | None = None,
target_python: Optional[TargetPython] = None,
prefer_binary: bool = False,
allow_all_prereleases: bool = False,
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
) -> CandidateEvaluator:
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
) -> "CandidateEvaluator":
"""Create a CandidateEvaluator object.
:param target_python: The target Python interpreter to use when
@ -424,11 +428,11 @@ class CandidateEvaluator:
def __init__(
self,
project_name: str,
supported_tags: list[Tag],
supported_tags: List[Tag],
specifier: specifiers.BaseSpecifier,
prefer_binary: bool = False,
allow_all_prereleases: bool = False,
hashes: Hashes | None = None,
hashes: Optional[Hashes] = None,
) -> None:
"""
:param supported_tags: The PEP 425 tags supported by the target
@ -449,31 +453,32 @@ class CandidateEvaluator:
def get_applicable_candidates(
self,
candidates: list[InstallationCandidate],
) -> list[InstallationCandidate]:
candidates: List[InstallationCandidate],
) -> List[InstallationCandidate]:
"""
Return the applicable candidates from a list of candidates.
"""
# Using None infers from the specifier instead.
allow_prereleases = self._allow_all_prereleases or None
specifier = self._specifier
# We turn the version object into a str here because otherwise
# when we're debundled but setuptools isn't, Python will see
# packaging.version.Version and
# pkg_resources._vendor.packaging.version.Version as different
# types. This way we'll use a str as a common data interchange
# format. If we stop using the pkg_resources provided specifier
# and start using our own, we can drop the cast to str().
candidates_and_versions = [(c, str(c.version)) for c in candidates]
versions = set(
specifier.filter(
(v for _, v in candidates_and_versions),
versions = {
str(v)
for v in specifier.filter(
# We turn the version object into a str here because otherwise
# when we're debundled but setuptools isn't, Python will see
# packaging.version.Version and
# pkg_resources._vendor.packaging.version.Version as different
# types. This way we'll use a str as a common data interchange
# format. If we stop using the pkg_resources provided specifier
# and start using our own, we can drop the cast to str().
(str(c.version) for c in candidates),
prereleases=allow_prereleases,
)
)
}
# Again, converting version to str to deal with debundling.
applicable_candidates = [c for c in candidates if str(c.version) in versions]
applicable_candidates = [c for c, v in candidates_and_versions if v in versions]
filtered_applicable_candidates = filter_unallowed_hashes(
candidates=applicable_candidates,
hashes=self._hashes,
@ -533,7 +538,11 @@ class CandidateEvaluator:
)
if self._prefer_binary:
binary_preference = 1
build_tag = wheel.build_tag
if wheel.build_tag is not None:
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
assert match is not None, "guaranteed by filename validation"
build_tag_groups = match.groups()
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
else: # sdist
pri = -(support_num)
has_allowed_hash = int(link.is_hash_allowed(self._hashes))
@ -549,8 +558,8 @@ class CandidateEvaluator:
def sort_best_candidate(
self,
candidates: list[InstallationCandidate],
) -> InstallationCandidate | None:
candidates: List[InstallationCandidate],
) -> Optional[InstallationCandidate]:
"""
Return the best candidate per the instance's sort order, or None if
no candidate is acceptable.
@ -562,7 +571,7 @@ class CandidateEvaluator:
def compute_best_candidate(
self,
candidates: list[InstallationCandidate],
candidates: List[InstallationCandidate],
) -> BestCandidateResult:
"""
Compute and return a `BestCandidateResult` instance.
@ -590,9 +599,9 @@ class PackageFinder:
link_collector: LinkCollector,
target_python: TargetPython,
allow_yanked: bool,
format_control: FormatControl | None = None,
candidate_prefs: CandidatePreferences | None = None,
ignore_requires_python: bool | None = None,
format_control: Optional[FormatControl] = None,
candidate_prefs: Optional[CandidatePreferences] = None,
ignore_requires_python: Optional[bool] = None,
) -> None:
"""
This constructor is primarily meant to be used by the create() class
@ -618,14 +627,7 @@ class PackageFinder:
self.format_control = format_control
# These are boring links that have already been logged somehow.
self._logged_links: set[tuple[Link, LinkType, str]] = set()
# Cache of the result of finding candidates
self._all_candidates: dict[str, list[InstallationCandidate]] = {}
self._best_candidates: dict[
tuple[str, specifiers.BaseSpecifier | None, Hashes | None],
BestCandidateResult,
] = {}
self._logged_links: Set[Tuple[Link, LinkType, str]] = set()
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@ -636,8 +638,8 @@ class PackageFinder:
cls,
link_collector: LinkCollector,
selection_prefs: SelectionPreferences,
target_python: TargetPython | None = None,
) -> PackageFinder:
target_python: Optional[TargetPython] = None,
) -> "PackageFinder":
"""Create a PackageFinder.
:param selection_prefs: The candidate selection preferences, as a
@ -676,36 +678,18 @@ class PackageFinder:
self._link_collector.search_scope = search_scope
@property
def find_links(self) -> list[str]:
def find_links(self) -> List[str]:
return self._link_collector.find_links
@property
def index_urls(self) -> list[str]:
def index_urls(self) -> List[str]:
return self.search_scope.index_urls
@property
def proxy(self) -> str | None:
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) -> str | None:
# 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) -> str | None:
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
@ -720,7 +704,7 @@ class PackageFinder:
def set_prefer_binary(self) -> None:
self._candidate_prefs.prefer_binary = True
def requires_python_skipped_reasons(self) -> list[str]:
def requires_python_skipped_reasons(self) -> List[str]:
reasons = {
detail
for _, result, detail in self._logged_links
@ -741,13 +725,13 @@ class PackageFinder:
ignore_requires_python=self._ignore_requires_python,
)
def _sort_links(self, links: Iterable[Link]) -> list[Link]:
def _sort_links(self, links: Iterable[Link]) -> List[Link]:
"""
Returns elements of links in order, non-egg links first, egg links
second, while eliminating duplicates
"""
eggs, no_eggs = [], []
seen: set[Link] = set()
seen: Set[Link] = set()
for link in links:
if link not in seen:
seen.add(link)
@ -767,7 +751,7 @@ class PackageFinder:
def get_install_candidate(
self, link_evaluator: LinkEvaluator, link: Link
) -> InstallationCandidate | None:
) -> Optional[InstallationCandidate]:
"""
If the link is a candidate for install, convert it to an
InstallationCandidate and return it. Otherwise, return None.
@ -777,18 +761,15 @@ class PackageFinder:
self._log_skipped_link(link, result, detail)
return None
try:
return InstallationCandidate(
name=link_evaluator.project_name,
link=link,
version=detail,
)
except InvalidVersion:
return None
return InstallationCandidate(
name=link_evaluator.project_name,
link=link,
version=detail,
)
def evaluate_links(
self, link_evaluator: LinkEvaluator, links: Iterable[Link]
) -> list[InstallationCandidate]:
) -> List[InstallationCandidate]:
"""
Convert links that are candidates to InstallationCandidate objects.
"""
@ -802,7 +783,7 @@ class PackageFinder:
def process_project_url(
self, project_url: Link, link_evaluator: LinkEvaluator
) -> list[InstallationCandidate]:
) -> List[InstallationCandidate]:
logger.debug(
"Fetching project page and analyzing links: %s",
project_url,
@ -821,7 +802,8 @@ class PackageFinder:
return package_links
def find_all_candidates(self, project_name: str) -> list[InstallationCandidate]:
@functools.lru_cache(maxsize=None)
def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]:
"""Find all available InstallationCandidate for project_name
This checks index_urls and find_links.
@ -830,9 +812,6 @@ class PackageFinder:
See LinkEvaluator.evaluate_link() for details on which files
are accepted.
"""
if project_name in self._all_candidates:
return self._all_candidates[project_name]
link_evaluator = self.make_link_evaluator(project_name)
collected_sources = self._link_collector.collect_sources(
@ -874,15 +853,13 @@ class PackageFinder:
logger.debug("Local files found: %s", ", ".join(paths))
# This is an intentional priority ordering
self._all_candidates[project_name] = file_candidates + page_candidates
return self._all_candidates[project_name]
return file_candidates + page_candidates
def make_candidate_evaluator(
self,
project_name: str,
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
) -> CandidateEvaluator:
"""Create a CandidateEvaluator object to use."""
candidate_prefs = self._candidate_prefs
@ -895,11 +872,12 @@ class PackageFinder:
hashes=hashes,
)
@functools.lru_cache(maxsize=None)
def find_best_candidate(
self,
project_name: str,
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
) -> BestCandidateResult:
"""Find matches for the given project and specifier.
@ -909,42 +887,32 @@ class PackageFinder:
:return: A `BestCandidateResult` instance.
"""
if (project_name, specifier, hashes) in self._best_candidates:
return self._best_candidates[project_name, specifier, hashes]
candidates = self.find_all_candidates(project_name)
candidate_evaluator = self.make_candidate_evaluator(
project_name=project_name,
specifier=specifier,
hashes=hashes,
)
self._best_candidates[project_name, specifier, hashes] = (
candidate_evaluator.compute_best_candidate(candidates)
)
return self._best_candidates[project_name, specifier, hashes]
return candidate_evaluator.compute_best_candidate(candidates)
def find_requirement(
self, req: InstallRequirement, upgrade: bool
) -> InstallationCandidate | None:
) -> Optional[InstallationCandidate]:
"""Try to find a Link matching req
Expects req, an InstallRequirement and upgrade, a boolean
Returns a InstallationCandidate if found,
Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise
"""
name = req.name
assert name is not None, "find_requirement() called with no name"
hashes = req.hashes(trust_internet=False)
best_candidate_result = self.find_best_candidate(
name,
req.name,
specifier=req.specifier,
hashes=hashes,
)
best_candidate = best_candidate_result.best_candidate
installed_version: _BaseVersion | None = None
installed_version: Optional[_BaseVersion] = None
if req.satisfied_by is not None:
installed_version = req.satisfied_by.version
@ -968,14 +936,14 @@ class PackageFinder:
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
req,
_format_versions(best_candidate_result.all_candidates),
_format_versions(best_candidate_result.iter_all()),
)
raise DistributionNotFound(f"No matching distribution found for {req}")
def _should_install_candidate(
candidate: InstallationCandidate | None,
) -> TypeGuard[InstallationCandidate]:
candidate: Optional[InstallationCandidate],
) -> "TypeGuard[InstallationCandidate]":
if installed_version is None:
return True
if best_candidate is None:
@ -1002,7 +970,7 @@ class PackageFinder:
logger.debug(
"Using version %s (newest of versions: %s)",
best_candidate.version,
_format_versions(best_candidate_result.applicable_candidates),
_format_versions(best_candidate_result.iter_applicable()),
)
return best_candidate
@ -1010,7 +978,7 @@ class PackageFinder:
logger.debug(
"Installed version (%s) is most up-to-date (past versions: %s)",
installed_version,
_format_versions(best_candidate_result.applicable_candidates),
_format_versions(best_candidate_result.iter_applicable()),
)
raise BestVersionAlreadyInstalled
@ -1041,7 +1009,7 @@ def _find_name_version_sep(fragment: str, canonical_name: str) -> int:
raise ValueError(f"{fragment} does not match {canonical_name}")
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> str | None:
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> Optional[str]:
"""Parse the version string from a <package>+<version> filename
"fragment" (stem) or egg fragment.

View file

@ -1,14 +1,12 @@
from __future__ import annotations
import logging
import mimetypes
import os
from collections import defaultdict
from collections.abc import Iterable
from typing import Callable
from typing import Callable, Dict, Iterable, List, Optional, Tuple
from pip._vendor.packaging.utils import (
InvalidSdistFilename,
InvalidVersion,
InvalidWheelFilename,
canonicalize_name,
parse_sdist_filename,
@ -30,7 +28,7 @@ PageValidator = Callable[[Link], bool]
class LinkSource:
@property
def link(self) -> Link | None:
def link(self) -> Optional[Link]:
"""Returns the underlying link, if there's one."""
raise NotImplementedError()
@ -52,8 +50,8 @@ class _FlatDirectoryToUrls:
def __init__(self, path: str) -> None:
self._path = path
self._page_candidates: list[str] = []
self._project_name_to_urls: dict[str, list[str]] = defaultdict(list)
self._page_candidates: List[str] = []
self._project_name_to_urls: Dict[str, List[str]] = defaultdict(list)
self._scanned_directory = False
def _scan_directory(self) -> None:
@ -70,24 +68,24 @@ class _FlatDirectoryToUrls:
# otherwise not worth considering as a package
try:
project_filename = parse_wheel_filename(entry.name)[0]
except InvalidWheelFilename:
except (InvalidWheelFilename, InvalidVersion):
try:
project_filename = parse_sdist_filename(entry.name)[0]
except InvalidSdistFilename:
except (InvalidSdistFilename, InvalidVersion):
continue
self._project_name_to_urls[project_filename].append(url)
self._scanned_directory = True
@property
def page_candidates(self) -> list[str]:
def page_candidates(self) -> List[str]:
if not self._scanned_directory:
self._scan_directory()
return self._page_candidates
@property
def project_name_to_urls(self) -> dict[str, list[str]]:
def project_name_to_urls(self) -> Dict[str, List[str]]:
if not self._scanned_directory:
self._scan_directory()
@ -103,7 +101,7 @@ class _FlatDirectorySource(LinkSource):
* ``file_candidates``: Archives in the directory.
"""
_paths_to_urls: dict[str, _FlatDirectoryToUrls] = {}
_paths_to_urls: Dict[str, _FlatDirectoryToUrls] = {}
def __init__(
self,
@ -122,7 +120,7 @@ class _FlatDirectorySource(LinkSource):
self._paths_to_urls[path] = self._path_to_urls
@property
def link(self) -> Link | None:
def link(self) -> Optional[Link]:
return None
def page_candidates(self) -> FoundCandidates:
@ -153,7 +151,7 @@ class _LocalFileSource(LinkSource):
self._link = link
@property
def link(self) -> Link | None:
def link(self) -> Optional[Link]:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -187,7 +185,7 @@ class _RemoteFileSource(LinkSource):
self._link = link
@property
def link(self) -> Link | None:
def link(self) -> Optional[Link]:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -215,7 +213,7 @@ class _IndexDirectorySource(LinkSource):
self._link = link
@property
def link(self) -> Link | None:
def link(self) -> Optional[Link]:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -233,9 +231,9 @@ def build_source(
expand_dir: bool,
cache_link_parsing: bool,
project_name: str,
) -> tuple[str | None, LinkSource | None]:
path: str | None = None
url: str | None = None
) -> Tuple[Optional[str], Optional[LinkSource]]:
path: Optional[str] = None
url: Optional[str] = None
if os.path.exists(location): # Is a local path.
url = path_to_url(location)
path = location

View file

@ -1,12 +1,10 @@
from __future__ import annotations
import functools
import logging
import os
import pathlib
import sys
import sysconfig
from typing import Any
from typing import Any, Dict, Generator, Optional, Tuple
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
from pip._internal.utils.compat import WINDOWS
@ -89,7 +87,7 @@ def _looks_like_bpo_44860() -> bool:
return unix_user_platlib == "$usersite"
def _looks_like_red_hat_patched_platlib_purelib(scheme: dict[str, str]) -> bool:
def _looks_like_red_hat_patched_platlib_purelib(scheme: Dict[str, str]) -> bool:
platlib = scheme["platlib"]
if "/$platlibdir/" in platlib:
platlib = platlib.replace("/$platlibdir/", f"/{_PLATLIBDIR}/")
@ -99,7 +97,7 @@ def _looks_like_red_hat_patched_platlib_purelib(scheme: dict[str, str]) -> bool:
return unpatched.replace("$platbase/", "$base/") == scheme["purelib"]
@functools.cache
@functools.lru_cache(maxsize=None)
def _looks_like_red_hat_lib() -> bool:
"""Red Hat patches platlib in unix_prefix and unix_home, but not purelib.
@ -114,7 +112,7 @@ def _looks_like_red_hat_lib() -> bool:
)
@functools.cache
@functools.lru_cache(maxsize=None)
def _looks_like_debian_scheme() -> bool:
"""Debian adds two additional schemes."""
from distutils.command.install import INSTALL_SCHEMES
@ -122,7 +120,7 @@ def _looks_like_debian_scheme() -> bool:
return "deb_system" in INSTALL_SCHEMES and "unix_local" in INSTALL_SCHEMES
@functools.cache
@functools.lru_cache(maxsize=None)
def _looks_like_red_hat_scheme() -> bool:
"""Red Hat patches ``sys.prefix`` and ``sys.exec_prefix``.
@ -142,7 +140,7 @@ def _looks_like_red_hat_scheme() -> bool:
)
@functools.cache
@functools.lru_cache(maxsize=None)
def _looks_like_slackware_scheme() -> bool:
"""Slackware patches sysconfig but fails to patch distutils and site.
@ -158,7 +156,7 @@ def _looks_like_slackware_scheme() -> bool:
return "/lib64/" in paths["purelib"] and "/lib64/" not in user_site
@functools.cache
@functools.lru_cache(maxsize=None)
def _looks_like_msys2_mingw_scheme() -> bool:
"""MSYS2 patches distutils and sysconfig to use a UNIX-like scheme.
@ -176,7 +174,23 @@ def _looks_like_msys2_mingw_scheme() -> bool:
)
@functools.cache
def _fix_abiflags(parts: Tuple[str]) -> Generator[str, None, None]:
ldversion = sysconfig.get_config_var("LDVERSION")
abiflags = getattr(sys, "abiflags", None)
# LDVERSION does not end with sys.abiflags. Just return the path unchanged.
if not ldversion or not abiflags or not ldversion.endswith(abiflags):
yield from parts
return
# Strip sys.abiflags from LDVERSION-based path components.
for part in parts:
if part.endswith(ldversion):
part = part[: (0 - len(abiflags))]
yield part
@functools.lru_cache(maxsize=None)
def _warn_mismatched(old: pathlib.Path, new: pathlib.Path, *, key: str) -> None:
issue_url = "https://github.com/pypa/pip/issues/10151"
message = (
@ -194,13 +208,13 @@ def _warn_if_mismatch(old: pathlib.Path, new: pathlib.Path, *, key: str) -> bool
return True
@functools.cache
@functools.lru_cache(maxsize=None)
def _log_context(
*,
user: bool = False,
home: str | None = None,
root: str | None = None,
prefix: str | None = None,
home: Optional[str] = None,
root: Optional[str] = None,
prefix: Optional[str] = None,
) -> None:
parts = [
"Additional context:",
@ -216,10 +230,10 @@ def _log_context(
def get_scheme(
dist_name: str,
user: bool = False,
home: str | None = None,
root: str | None = None,
home: Optional[str] = None,
root: Optional[str] = None,
isolated: bool = False,
prefix: str | None = None,
prefix: Optional[str] = None,
) -> Scheme:
new = _sysconfig.get_scheme(
dist_name,
@ -283,13 +297,14 @@ def get_scheme(
continue
# On Python 3.9+, sysconfig's posix_user scheme sets platlib against
# sys.platlibdir, but distutils's unix_user incorrectly continues
# sys.platlibdir, but distutils's unix_user incorrectly coninutes
# using the same $usersite for both platlib and purelib. This creates a
# mismatch when sys.platlibdir is not "lib".
skip_bpo_44860 = (
user
and k == "platlib"
and not WINDOWS
and sys.version_info >= (3, 9)
and _PLATLIBDIR != "lib"
and _looks_like_bpo_44860()
)
@ -321,6 +336,17 @@ def get_scheme(
if skip_linux_system_special_case:
continue
# On Python 3.7 and earlier, sysconfig does not include sys.abiflags in
# the "pythonX.Y" part of the path, but distutils does.
skip_sysconfig_abiflag_bug = (
sys.version_info < (3, 8)
and not WINDOWS
and k in ("headers", "platlib", "purelib")
and tuple(_fix_abiflags(old_v.parts)) == new_v.parts
)
if skip_sysconfig_abiflag_bug:
continue
# MSYS2 MINGW's sysconfig patch does not include the "site-packages"
# part of the path. This is incorrect and will be fixed in MSYS.
skip_msys2_mingw_bug = (

View file

@ -9,8 +9,6 @@
#
# See https://github.com/pypa/pip/issues/8761 for the original discussion and
# rationale for why this is done within pip.
from __future__ import annotations
try:
__import__("_distutils_hack").remove_shim()
except (ImportError, AttributeError):
@ -23,6 +21,7 @@ from distutils.cmd import Command as DistutilsCommand
from distutils.command.install import SCHEME_KEYS
from distutils.command.install import install as distutils_install_command
from distutils.sysconfig import get_python_lib
from typing import Dict, List, Optional, Union, cast
from pip._internal.models.scheme import Scheme
from pip._internal.utils.compat import WINDOWS
@ -36,19 +35,19 @@ logger = logging.getLogger(__name__)
def distutils_scheme(
dist_name: str,
user: bool = False,
home: str | None = None,
root: str | None = None,
home: Optional[str] = None,
root: Optional[str] = None,
isolated: bool = False,
prefix: str | None = None,
prefix: Optional[str] = None,
*,
ignore_config_files: bool = False,
) -> dict[str, str]:
) -> Dict[str, str]:
"""
Return a distutils install scheme
"""
from distutils.dist import Distribution
dist_args: dict[str, str | list[str]] = {"name": dist_name}
dist_args: Dict[str, Union[str, List[str]]] = {"name": dist_name}
if isolated:
dist_args["script_args"] = ["--no-user-cfg"]
@ -62,10 +61,10 @@ def distutils_scheme(
"Ignore distutils configs in %s due to encoding errors.",
", ".join(os.path.basename(p) for p in paths),
)
obj: DistutilsCommand | None = None
obj: Optional[DistutilsCommand] = None
obj = d.get_command_obj("install", create=True)
assert obj is not None
i: distutils_install_command = obj
i = cast(distutils_install_command, obj)
# NOTE: setting user or home has the side-effect of creating the home dir
# or user base for installations during finalize_options()
# ideally, we'd prefer a scheme class that has no side-effects.
@ -79,7 +78,7 @@ def distutils_scheme(
i.root = root or i.root
i.finalize_options()
scheme: dict[str, str] = {}
scheme = {}
for key in SCHEME_KEYS:
scheme[key] = getattr(i, "install_" + key)
@ -116,10 +115,10 @@ def distutils_scheme(
def get_scheme(
dist_name: str,
user: bool = False,
home: str | None = None,
root: str | None = None,
home: Optional[str] = None,
root: Optional[str] = None,
isolated: bool = False,
prefix: str | None = None,
prefix: Optional[str] = None,
) -> Scheme:
"""
Get the "scheme" corresponding to the input parameters. The distutils

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